Chapter 3: Local Variables

Just as in any Object Pascal routine, you can use local variables inside your assembler code. Local variables are declared in the same way as for Pascal routines, by a var section. In this chapter, we will give a detailed overview of how local variables work for the various Delphi types.

3.1. Local variables and the stack frame

Storage space for local variables is allocated on the stack, as part of the compiler' generated entry code (and released again in the exit code). Please note that for some structured types like AnsiStrings, the space allocated is for a pointer to the actual data (strings reside on the heap and the string variable is a mere pointer to the data). To continue with the examples of the previous chapter, just look at the following code:

procedure DoSomething(First, Second, Third: Integer); pascal;
var
  SomeTemp: Integer
asm
  ...
end;

From the previous chapter, we already know that using the pascal calling convention will result in the parameters being pushed onto the stack prior to calling the procedure. The call to the subroutine will push the return address onto the stack and the entry code will push the previous value of ebp onto the stack and initialise this register to become the base pointer for accessing information on the stack frame. So far, the stack looks therefore as follows: Picture showing stack with parameters, return address and saved ebp value and esp and ebp pointing to the latter.

Because we have declared a local variable, SomeTemp, the compiler will generate code (for instance push ecx) to reserve space on the stack for that variable. As a result, the stack now looks as follows: Picture showing stack with parameters, return address, saved ebp and space for SomeValue

As explained before, ebp contains the base pointer for accessing information on the stack. As the stack grows downwards, higher addresses will contain parameters, while lower addresses contain the local variables. For this particular example, the following slots have been created:

Parameters:
First  = ebp + $10 (ebp + 16)
Second = ebp + $0C (ebp + 12)
Third  = ebp + $08 (ebp + 8)

Local Variables:
SomeTemp = ebp - $04 (ebp - 4)

The next local variable will be allocated at ebp -8 and so on. Just as with parameters on the stack, you can (and should) use the variable name to refer to the actual location on the stack:

mov eax, SomeTemp

will then be translated by the compiler into:

mov eax, [ebp-4]

Please note that the content of these variables is generally not initialised, so you should treat their contents as being undefined. It is your task to initialise them when and if required.

Because using local variables results in overhead to create and manage the stack frame, you must analyse your algorithm carefully to decide whether or not you will need such local storage. Clever use of the available registers might mean you do not even need local variables at all, and apart from avoiding overhead to set up the local variable space, moving data between registers is significantly faster than accessing data in main memory. When you are writing Object Pascal code, the Delphi compiler will perform optimisations that aim at avoiding allocation of stack space and use registers instead. Loop counter variables are a particular case where this happens quite often. Of course, inside an asm..end block, you are on your own and the compiler will not perform such optimisations! Well structured code will therefore aim to use registers as much as possible, especially for the data that is used most often.

3.2. Simple types as local variables

Quite a number of variable types will simply result in allocation of the appropriate amount of space on the stack when you declare a local variable of such a type. ShortInt, SmallInt, LongInt, Byte, Word, DWord, Boolean, ByteBool, WordBool, LongBool, Char, Ansichar and Widechar all belong to this category.

The space allocated will be used to store the variable's contents. While not all of these types are 32-bits wide, reservation of stack space will always happen in chunks of 32-bits at a time. That means that if you use smaller types, like for instance byte or word, they will be padded to fill the remaining bytes. This padding space has undefined content. For instance, if you declare a local variable as follows:

var
  AValue: ShortInt;

While AValue only requires one byte, the entry code for this procedure will still allocate 32 bits. This behaviour ensures that data on the stack is always aligned on a dword boundary, which is important for performance on a 32-bit processor. You should however not use the remainder part of the allocated space (the padding) since this is a pure implementation issue on current versions of the compiler. You can never be sure that future versions behave in the same way. If you need additional storage space, simply use a bigger type.

Please note that with regard to alignment padding, local record type variables, even though they are also stored on the stack frame, behave in a different way. Their member fields' alignment depends on the state of the alignment switch ({$A} directive) and the use of the packed modifier. This is discussed in the next paragraph.

Of course, this padding has consequences for accessing the data. But if you use the variable name, the compiler will generate the correct offset for you. In the example above, AValue occupies only one byte. Hence, only the lowest byte of the allocated dword is used. So, the following code:

mov al, AValue

will result in the compiler generating the following:

mov al, [epb-$01]

You should use the local variable by name, not by offset. Using the name will leave the task to compile offsets on the stack frame to the compiler. Compilers are good at that sort of thing!

In Pascal routines, so those not identified by asm...end blocks, the compiler might optimise a local variable into a register, in which case no space for it will be allocated on the stack frame. While such optimisation does not happen inside your own asm...end blocks, you should be aware of this behaviour when observing compiler generated code through the CPU window. Similarly, sometimes the compiler will generate code that uses esp directly, rather than an offset from ebp, thus saving the need for initialisation of ebp. As argued before, it's not generally advisable to use esp directly in your assembler code, it makes it extremely hard to maintain the code and it is prone to introducing subtle coding errors that will be hard to find and debug. While observation of the code that the compiler generates can be very educational, remember that you are not a machine, but a human programmer. Machines are good in making sure they calculate the right offsets and the like - humans are not! In 99.9% of the cases, your bottleneck will not be the initialisation of the stack frame, and if it is, then you should probably reconsider your entire algorithm. You should not waste time and effort in optimising code that doesn't have a noticeable impact on your application performance.

3.3. Records as local variables

Just like for simple types, local record variables are stored on the stack. In that respect, they are not fundamentally different in their usage from simple variables (see previous paragraph). However, the compiler's record alignment mechanism has significant impact on the actual allocation of stack space. This can seriously complicate things for the programmer if he/she is coding offsets into the record directly.

There are two factors that define Delphi's record alignment behaviour: the alignment directive ({$A} or {$ALIGN}) and the packed modifier. The actual alignment of the record member fields is dependent on the field type. For example, let's assume a record declaration as follows:

TMyRecord = record
  FirstValue:  DWord;
  SecondValue: Byte;
  ThirdValue:  DWord;
  FourthValue: Byte;
end;

The alignment boundary for each member field of the record depends on its type and its size. In the example above, Firstvalue and ThirdValue are of type DWord, which is a 32-bit type. With alignment on, they will be aligned to dword boundaries. Since in between those two members, there is a byte-sized field, SecondValue, the compiler will add three padding bytes, thus ensuring that ThirdValue is properly aligned. The following picture shows the memory usage of this record in the aligned state: Picture showing stack usage for aligned record, showing three unused bytes between SecondValue and ThirdValue

By introducing the packed modifier in the record declaration, the record's member fields are no longer being aligned. You can see the result in the following picture, where the padding bytes are now no longer present: Picture showing stack usage for non-aligned record, without any padding bytes

Similarly, when alignment is turned off by using the {$A-} directive, even without the packed modifier there will be no padding between record fields. Fortunately, just as for simple types, you can simply refer to record member fields by their names, and the compiler will calculate the correct offsets for you. However, always make sure you use operands of the proper size, i.e. specify the operand size explicitly. In that way, your code will continue to work correctly even when alignment is changed or the packed modifier is introduced at a later stage:

mov eax, DWORD PTR [ARecord.FirstValue]
mov  al, BYTE  PTR [ARecord.Byte]

3.4. Heap allocated types as local variables

Dynamic variables, long strings, wide strings, dynamic arrays, variants, and interfaces in Delphi are all variable types that are stored in heap memory. In order to use them, you use a reference variable, i.e. a pointer to the actual variable itself. In Pascal, this reference can be explicit by the use of a reference variable or implicit, the latter for example by using a with statement. In assembler, you must make sure you store the pointer to the variable memory somewhere in order to be able to access it.

Consequently, if you use heap allocated types as local variables, memory will be allocated for the reference (the pointer) to that variable, but you are responsible for the actual allocation and deallocation of the memory. In Pascal, most of these types are automatically managed, so allocation and deallocation happens behind the scenes. In assembler, you obviously are responsible yourself for taking care of that.

You can call GetMem to allocate memory and return a pointer to the newly allocated memory. You need to pass the amount of memory needed in eax and upon return from GetMem the eax register will contain the pointer, which you can then store in the relevant place.

(more content to be added shortly...)

Next part: Chapter 4: Returning Results
Previous part: Chapter 2: Passing Parameters
Table of Contents


This page was last updated 30 December 2006.