Chapter 1: General Context of Assembler Code

Successfully using assembler code inside your Delphi projects means that in the first place 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 how it should be structured. It also explains the compiler's behaviour in generating entry and exit code. The next chapter will then discuss passing parameters to the routine.

Note: In this document, we will always specifically indicate the calling convention used in the examples. Although in the case of register this is actually 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) because it acts like a reminder for the reader that parameters might be located in registers. Credit for this tip goes to Christen Fihl, High Speed Pascal (link opens in a new window).

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 these blocks inside a Pascal function or procedure, but that approach is not recommended. You should isolate your assembler code inside its own functions or procedures. First of all, inserting assembler blocks 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 will likely be pushed out of their register, needing saving on the stack or reloading afterwards. Also, you are in fact forcing the compiler to adapt the generated code to your inserted assembler code. That interferes with the optimisation logic and the result will be quite inefficient. So, the rule is to put assembler code in its own functions/procedures. It is also a question of design. The readability and maintainability of your code will benefit greatly when all the assembler is clearly isolated in dedicated, well-commented blocks.

1.2. Labels

Labels are tags that mark places in your code. The most common reason for having labels is to have a point of reference for writing jumps. 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. You must follow the label itself with a colon:

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

The example above illustrates how to declare a label MyLabel, use to to mark a point in your program (MyLabel:) and how to jump 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 code as above, but with 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 fading away and is in fact a remainder of the past. As a consequence, even Pascal labels are "local" in the sense that you can not jump to a label outside the current function or procedure. That is just as well, since that would be a perfect scenario for disaster. Unlike Goto in higher level languages, jumping is acceptable, even needed, in assembler programming. However, assembler code ought to remain localised to the context of its own routine, delimited by an asm...end block. Jumping outside that block would be a recipe for disaster.

1.3. Loops

Often, the assembler code will be designed for speed. And quite often, processing large amounts of data inside loops will be part of that. When loops are involved, you should implement the loop itself in assembler as well. That is not difficult and otherwise you will be loosing lots of time in the actual calling overhead. 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. Because of that, 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 of with ecx=0 and then count upwards, you will need an additional compare instruction to check whether or not the loop has ended:

  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

An important thing 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 on the stack, or if local data is stored on the stack. You'll find that very often this is the case, and so 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. As such, ebp can now be used as the base register to access the information on the stack frame. The sub esp line reserves space on the stack for local variables as appropriate. The exit code that is generated will look like:

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

This exit code will first clean up the space allocated for local parameters by copying ebp (pointing at the start of the stack frame) to the stack pointer. That effectively cleans up the space allocated to 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 on the stack, then there will be no entry and exit code, 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 modified without having to be restored to their previous content upon exit. All other registers must be preserved. That means that if you use any of the other registers inside your routine, you must save them on the stack and restore them before returning.

You should never 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 (opens in a new window) gives an overview of register use in Delphi.

Apart from the register context, you can assume that the direction flag is cleared upon entry and if you change that (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 of the floating point unit 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 part: Chapter 2: Passing Parameters
Previous part: Introduction: to write assembler or not to write assembler?
Table of Contents


This page was last updated 2 January 2007.