Home > Writings > Programming > Using Assembler in Delphi > Chapter 1: General Context of Assembler Code

Using Assembler in Delphi

Chapter 1: General Context of Assembler Code

To successfully use assembler code inside your Delphi projects, you need to understand how to call routines written in assembler, have access to the parameters passed and be able to return a result. This chapter discusses where assembler code should be located and what its basic structure should look like. It also explains the compiler's behaviour in generating entry and exit code.

Note: In this document, I will always specifically indicate the calling convention used in the examples. Although in the case of register this is superfluous (since it is the default convention that is used when no calling convention is specifically indicated), it contributes to the readability (look at it as an additional comment if you want) as it reminds the reader that parameters might be located in registers. Credit for this tip goes to Christen Fihl.

1.1. Where to locate the assembler code

Delphi uses the asm statement to indicate an assembler block. The end of the block is indicated by an end statement:

procedure SomeProcedure; register;
asm
  {Code goes here}
end;

It is possible to nest asm blocks inside a Pascal function or procedure, but that approach is not recommended. You should isolate your assembler code inside separate function or procedure blocks. First of all, inserting assembler inside a regular Pascal function will interfere with the compiler's optimisation and variable management activities. As a result, the generated code will be far from optimal. Variables are likely to be pushed out of their register, requiring saving on the stack and reloading afterwards. Also, nesting inside a Pascal block forces the compiler to adapt its generated code to your assembler code. Again, this interferes with the optimisation logic and the result will be quite inefficient. So, the rule is to put assembler code in its own separate function/procedure block. There is also a design aspect: the readability and maintainability of your code will benefit greatly when all assembler is clearly isolated in dedicated, well-commented blocks.

1.2. Labels

Labels are tags that mark locations in your code. The most common reason for having labels is to have a point of reference for branching. There are two kinds of labels you can use in your assembler code: Pascal-style labels and local assembly labels. The former type requires you to declare them in a label section first. Once declared, you can use the label in your code. The label must be followed by a colon:

label
 MyLabel;
asm
 ...
 mov ecx, {Counter}
MyLabel:
 ...  {Loop statements}
 dec ecx
 jnz MyLabel
 ...
end;

The above example illustrates how to declare a label MyLabel, marking a position in your program (MyLabel:) and moving to the label's location in a jump instruction (jnz MyLabel).

The same can be achieved in a slightly simpler way, by using local labels in your assembler code. Local labels do not require a declaration, rather you simply insert the label as a separate statement. Local labels must start with the @ sign, and are again followed by a colon. Because @ can't be part of an Pascal identifier, you can use local labels only within an asm...end block. Sometimes, you will see labels prefixed by a double @ sign in code on this website and elsewhere. This is a convention I use a lot and it draws attention to the labels immediately, but it is not required (some assemblers use the @@ to identify special purpose labels, like @@: for an anonymous label). Below is an example of the same logic as above, but using a local label:

asm
 ...
 mov ecx, {Counter}
@MyLabel:
 ...  {Loop statements}
 dec ecx
 jnz MyLabel
 ...
end;

Neither kind of label is intrinsically better than the other. There is no advantage in code size or speed of course, since labels are only reference points for the compiler to calculate offsets and jumps. The difference between Pascal-style and local labels in assembler blocks is a relic from the past and is fading away. As a consequence, even Pascal-style labels are "local" in the sense that it is not possible to jump to a label outside the current function or procedure block. That is just as well, since that would be a perfect scenario for disaster.

1.3. Loops

Often, the assembler code will be designed to achieve the highest speed possible. And quite often also, processing large amounts of data inside loops will be the task. When loops are involved, you should implement the loop itself in assembler too. That is not difficult and otherwise you will be wasting a lot of execution time because of calling overheads. So, instead of doing:

function DoThisFast(...): ...; register;
asm
  ...
  {Here comes your assembler code}
   ...
end;

procedure SomeRoutine;
var
  I: Integer;
begin
  I:=0;
  ...
  while I<{NumberOfTimes} do begin
    DoThisFast(...);
    inc(I);
  end;
  ...
end;

You should implement the loop inside the assembler routine:

function DoThisFast(...): ...; register;
asm
  ...
  mov ecx,{NumberOfTimes}
 @@loop:
  ...
  {Here comes your main assembler code}
  ...
  dec ecx
  jnz @@loop
  ...
end;

procedure SomeRoutine;
begin
  ...
  DoThisFast(...);
  ...
end;

Note that in the example above, the loop counter counts downwards. That is because in this way, you can simply check the zero flag after decrementing to see if the end of the loop has been reached. By contrast, if you simply start off with ecx=0 and then count upwards, you will need an additional compare instruction to check whether or not to continue the loop:

  mov ecx,0
 @@loop:
  ...
  inc ecx
  cmp ecx,{NumberOfTimes}
  jne @@loop

Alternatively, you can subtract the NumberOfTimes from 0 and then increase the loop index until zero is reached. This approach is especially useful if you use the loop index register simultaneously as an index to some table or array in memory, since cache performance is better when accessing data in forward direction:

  xor ecx,ecx
  sub ecx,{NumberOfTimes}
 @@loop:
  ...
  inc ecx
  jnz @@loop

Remember however that in this case, your base register or address should point to the end of the array or table, rather than to the beginning, and you will be iterating through the elements in reverse order.

1.4. Entry and exit code

Another important aspect to be aware of, is that the compiler will automatically generate entry and exit code for your assembler block. This will only happen if there is a need for a stack frame, i.e. if parameters are passed to the routine via the stack, or if local data is stored on the stack. You'll find that very often this is indeed the case, and consequently entry and exit code will be generated. The compiler produces the following entry code:

push ebp
mov  ebp,esp
sub  esp, {Size of stack space for local variables}

This code preserves ebp, and then copies the stack pointer into the ebp register. Subsequently ebp can be used as the base register to access information on the stack frame. The sub esp line reserves space on the stack for local variables as required. The exit code pattern is as follows:

mov esp,ebp
pop ebp
ret {Size of stack space reserved for parameters}

This exit sequence will clean up the space allocated for local parameters by copying ebp (pointing at the beginning of the stack frame) back to the stack pointer. This deallocates the space used for local variables. Next, ebp is restored to the value it had upon entry of the routine. Finally, control is returned to the caller, adjusting the stack again for any space allocated for parameters passed to the routine. This parameter cleanup in the ret instruction is required for all calling conventions except cdecl. In all cases except cdecl, the called function is responsible for cleaning up the stack space allocated for parameters, and thus the ret instruction will include the necessary adjustment. In case of cdecl, however, the caller performs the cleanup.

If your function or procedure has neither local variables nor parameters passed to it via the stack, then no entry and exit code will be produced, except for the ret instruction that is always generated.

1.5. Register preservation

When using registers inside your function or procedure, please note that only the registers eax, ecx and edx can be freely modified. All other registers must be preserved. That means that if you use any of the other registers inside your routine, you must save them and restore them before returning from your routine.

You must not change the contents of any of the segment selectors: ds, es and ss all point to the same segment; cs has its own value; fs refers to the Thread Information Block (TIB) and gs is reserved. The esp register points to the top of the stack, of course, and ebp is made upon entry to point to the current stack frame as a result of the default entry code generated by the compiler. Since each pop and push operation will change the content of the esp register, it is usually not a good idea to access the stack frame directly through esp. Rather, you should reserve ebp for that purpose. Table 1 summarises register usage.

Apart from the register context, you can assume that the direction flag is cleared upon entry and if you change it (which I don't recommend), you should restore its cleared state prior to returning (by using the cld instruction). Finally, you should be careful about changing the FPU control word. Although this allows you to change the precision and rounding mode for floating point arithmetic, and permits you to mask certain exceptions, you will be drastically influencing the way calculations in your entire application are performed. Whenever you decide it is necessary to change the FPU control word, make sure you restore it as soon as possible. When you are using Comp or Currency types, make sure you don't reduce floating point precision.

Next: Chapter 2: Passing Parameters