Chapter 2: Passing Parameters

The overall majority of routines that you will write will need to accept one or more parameters as input. This chapter gives an overview of how such parameters can be passed to your routine for processing. Many routines will be functions that also have to return a result to the caller, but the process of returning a result is discussed in Chapter 4.

2.1. Calling Conventions

One of the most common operations in the processing of code is calling specific subroutines that carry out a particular task. In order to do that, the main code needs to hand over control to the subroutine, allow the subroutine to execute, and then return to where it left the main execution path. Various conventions exist to deal with invoking subroutines, passing parameters to them and returning results back to the main code path. These are appropriately called calling conventions and Delphi supports a wide set of them: register, pascal, cdecl, stdcall, and safecall. Obviously, caller and callee need to use the same calling convention in order for this to work properly.

Calling conventions define a number of subroutine invocation aspects:
- Where parameters are located: in registers or on the stack
- In which order parameters are passed: right-to-left or left-to-right
- Who is responsible for cleaning up the parameters afterwards, the caller or the callee

Table 2 gives an overview of each of the Delphi supported calling conventions.

2.2. Passing parameters in registers

There is only one calling convention, register, that uses the CPU registers to pass parameters. There are three registers available to pass information: eax, edx and ecx and they are filled in this order (because ecx is used last, that register remains available as long as possible, which is quite convenient since you will often want to use it as a loop counter variable). If you are passing more parameters than there are registers available, the remaining ones will be passed on the stack as described in the next section. When you use the register calling convention for methods, however, eax will contain Self and thus only two registers are available for parameter passing. Also, not all types are suitable for passing in a register. Table 3 gives a clear overview of which types can be passed in a register. Please note that when passing parameters by reference, you are in fact passing a pointer to the variable in question and since pointer types qualify for passing in a register, types that are passed by reference always qualify for passing in a register, with the exception of method pointers.

If the number of parameters passed is lower than or equal to the number of registers available (three for standalone routines, two for methods), than there is no need to set up a stack frame for parameter passing. This can save considerable time when calling the routine. Be careful however, because parameter passing is not the only reason for setting up stack frames: if you declare local variables, a stack frame is also required and thus extra overhead to manage the stack frame is incurred.

In addition, for many structured types, the data itself actually resides on the stack or on the heap and the variable is nothing more than a pointer to the actual data. Such a pointer only occupies 32-bits and therefore will fit into a register. This means that most parameter types will qualify for passing through registers, although method pointers (consisting of two 32-bit pointers, one to the object instance and one to the method entry point) will always be passed on the stack.

For the current generation of processors on which these articles are based, namely the Intel Pentium processors, each register is 32 bits wide. When passing information that doesn't occupy the whole register (byte- and word-sized values), the normal rules apply: bytes go in the lowest 8 bits (for example al) and words in the lower word of the register (for example ax). Pointers are always 32-bit values (at least until 64-bit processors become common and a 64-bit version of Delphi will become available) and thus occupy the whole register (for example eax). In case of byte- or word-sized variables, the content of the rest of the register is actually unknown and you should not rely on any specific behaviour. For instance, when passing a byte to a function in al, the remaining 24 bits of eax are unknown and you certainly can not expect them to be zeroed out. You could simply use an and operation to make sure the remaining bits of the register are reset:

and eax,$FF {Unsigned byte value in AL, clears 24 highest bits}

or

and eax,$FFFF {unsigned word value in AX, clears 16 highest bits}

When you are getting passed signed values (shortint and smallint), you might want to expand them to a 32-bit value for easier computation, and you will have to do that while retaining the sign. To expand a signed byte to a signed double word, you need two instructions:

cbw  {extends al to ax}
cwde {extends ax to eax}

The importance of not relying on the remainder bits having a specific value can be easily demonstrated. Write the following test routine:

function Test(Value: ShortInt): LongInt; register;
asm

end;

Next, drop a button and a label on a form and put the following code in the button's OnClick event:

var
  I: ShortInt;
begin
  I:=-7;
  Label1.Caption:=IntToStr(Test(I));
end;

Run the project and click the button. The Test routine receives a ShortInt through al. It returns an integer in the eax register (returning results is discussed in Chapter 4), which is unchanged since the subroutine returns immediately. You can easily observe that eax has an undefined content upon return. Now change the test function as follows and run the project again:

function Test(Value: ShortInt): LongInt; register;
asm
  cbw
  cwde
end;

You will see that the Test routine now returns the correct value.

In summary, when using the register calling convention, up to three parameters can be passed in the eax, edx and ecx registers. So, the following declaration:

procedure DoSomething(First: Integer; Second: ShortInt; Third: Pointer); register;
asm
  ...
end;

will put First in eax, Second in dl and Third in ecx. And to give an example of a method declaration:

procedure TSomeClass.DoSomething(First, Second: Integer); register;
asm
   ...
end;

In this case, eax will contain Self, edx contains First, while Second is stored in ecx.

Please note that since register will result in parameters being stored in registers, you will lose the parameter information as soon as you override the register contents. Take the following code:

procedure DoSomething(AValue: Integer); register;
asm
  {eax will contain AValue}
  ...
  mov eax, [edx+ecx*4] {eax gets overwritten here}
  ...
end;

After eax gets overwritten, you no longer have access to the AValue parameter! If you need to preserve that parameter, make sure to save the contents of eax on the stack or in local storage for use afterwards. And don't fall into the classic trap to do the following later on in your code:

mov eax, AValue

because the above will generate simply the following:

mov eax, eax

as a result of the fact that AValue is passed in eax.

2.3. Using the stack for parameter passing

All calling conventions can use the stack for passing parameters. While the register convention tries to use CPU registers first, not all variable types qualify for passing in a register and sometimes you will need to pass more parameters than there are registers available. All the other calling conventions will pass all their parameters on the stack to the called routine.

As explained in the previous chapter, the compiler will generate entry and exit code to manage the stack frame. As a result, ebp is initialised as a base pointer to the stack frame, allowing easy access to parameters and other information on the stack (including local variables as explained in Chapter 3). When you refer to parameters that reside on the stack, the compiler will generate the appropriate offset from ebp. Have a look at the following declaration:

function Test(First, Second, Third: Integer): Integer; pascal;

The calling convention is pascal, which means that prior to the call to the subroutine, the three parameters are pushed on the stack in the order that they are declared (remember that the stack grows downwards, which means the first parameter is pushed at the highest address!): Picture showing stack with First, Second, Third parameters and esp pointing to Third.

Next, the call instruction will push the return address onto the stack and then hands over execution, so upon entry of the subroutine, the stack looks as follows: Picture showing stack with parameters and return address and esp pointing ti the return address.

The compiler' generated entry code (see Chapter 1) saves the current value of ebp and subsequently copies the value of esp to ebp so that the latter can from now on be used to access the various elements on the stack frame: Picture showing stack with parameters, return address and saved ebp value and esp and ebp pointing to the latter.

From this point on, we can actually access the parameters on the stack frame as offsets from ebp. Because the return address sits on the stack between the current top-of-stack and the actual parameters, we can now access the parameters as follows:

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

However, you can simply refer to these parameters by name. the compiler will replace each parameter with the correct offset from ebp. So, in the example above, writing the following:

mov eax, First

will be translated by the compiler into:

mov eax,[ebp+0x10]

This will save you the headache from calculating the offsets yourself and it is also much more readable, so you should use the names of the parameters that are passed on the stack in your code wherever possible (practically always) instead of hard coding the offsets. Be careful however: if you use the register calling convention, the first set of parameters will be passed in registers. For those parameters that are passed in registers, you probably want to use the correct register to refer to the variable, to prevent ambiguities in your code. Take the following example:

procedure DoSomething(AValue: Integer); register;

Since this declaration uses the register calling convention the AValue parameter will be passed into the eax register. It is probably wise to explicitely write eax in your code to refer to this parameter. It will help you to spot the following situations:

mov eax, AValue

which with the declaration above would result in the following code to be generated:

mov eax, eax

In other words, you are doing something completely superfluous, cluttering up the code with pointless operations.

To summarise: for parameters passed in a register, use the register to refer to it. For parameters passed onto the stack, use the variable name to refer to it (and keep the ebp register free to access that information).

Stack space is always used in chunks of 32-bits, and thus the data passed on the stack will always occupy a multiple of 32 bits. Even if you pass a byte to the procedure, there will be 4 bytes allocated on the stack with the three most significant bytes having undefined content. You should never rely on the undefined bytes having a specific value.

2.4. Passing by value versus passing by reference

Earlier on, the difference between passing by value and passing by reference was already mentioned. When passing by reference (using the var or the const directives), you are not handing over a localised copy of the data to the routine in question, but rather you are passing a pointer to the original data. This difference is quite important. Take for instance the following function declaration:

function MyFunction(I: Integer): Integer; register;

We are using the register calling convention, so the value of the I parameter will be passed in the eax register (see table 3). So, given I=254, eax will upon entry contain the value $000000FE, thus effectively passing I by value. However, if we change declaration as follows:

function MyFunction(var I: Integer): Integer; register;

the eax register no longer contains the value of I (254 in our example), but rather a pointer to the memory location where I is stored, for example $0066F8BC. When passing parameters by reference using var or const, you will be passing a 32-bit pointer. That means that passing a variable by reference means you will follow the rules for passing pointers, regardless of what the actual type of the variable is.

When you use const to indicate that you are passing a variable for read-only access, the compiler can use either of these methods to pass the information: either by value or by reference. The wording in Delphi's online help can be misleading and many programmers are known to believe that const always results in passing a 32-bit pointer to the actual value. You should use table 3 as a guideline to determine what is actually being passed and where.

Using const informs the compiler that the information is only going to be read. Please note however that within an asm..end block, the compiler will not prevent you from writing code that violates the read-only character of const parameters if these are actually pointers to structured data like AnsiStrings or Records. Be careful to honour the read-only character of the information passed using const in your assembler code, otherwise you could introduce nasty bugs. And of course, it would be extremely poor design to label information read-only and despite that still change it. This is especially important when you are using reference counted types like AnsiString that use copy-on-write semantics.

All of the above means that you will have to deal differently with parameters passed by value as opposed to by reference. To give a simple example, imagine a function that calculates the sum of an integer parameter and 12. In case of passing the integer parameter by value, the code will look as follows:

function MyFunction(I: Integer): Integer; register;
asm
  add eax, 12
end;

We will discuss returning results in Chapter 4, for now it is sufficient to know that we will need to put the result in this case into the eax register. As you can see, the value of I is taken directly from the eax register. But if we change the function to pass the information by reference, we would get something like this:

function MyFunction(var I: Integer): Integer; register;
asm
  mov eax, [eax] {Load the value of I through the pointer}
  add eax, 12
end;

Because eax does not contain the value of I, but rather a pointer to the memory location where I is stored, we need to first retrieve the actual value from that location through the pointer that we received.

Next part: Chapter 3: Local Variables
Previous part: Chapter 1: General Context of Assembler Code
Table of Contents


This page was last updated 30 December 2006.