Skip to content

x86 calling convention

zarlo edited this page Feb 9, 2023 · 4 revisions

Our Intel x86 output uses a calling convention that's similar to the old pascal calling convention. Method arguments are pushed left-to-right and the callee does stack cleanup.

Arguments are passed on the stack only. Result values are put on the stack as well. Exception state is returned in the ECX register.

If the total argument size (including the $this for instance methods) is less than the result size, we push extra dword values onto the stack.

Arguments and local variables inside methods

Inside a method, the method arguments and local variables are access relative to the EBP register. Because the stack on Intel architecture grows up (towards zero), local variables are EBP - X and arguments are EBP + X


Examples

This section contains a number of assembler samples. The shown assembler code is might differ in spots compared to the actual compiler output, but it's purely to show the calling convention. Besides that, label names used are not valid assembler language labels, so we can better explain what's going on.

Calling a simple method with 1 Int32 argument.

The method itself has a single local variable or type Int32

Calling method:
    push ebp  // Save original EBP
    mov ebp, esp // Set new method base pointer

    push 0x42   // push argument value 0x42 on the stack

    call PrintValue(int) // call the actual method

    // there's no result on the stack

    test dword ECX, 0x2 // if second bit of the ECX register is set, an exception occurred in the method (directly or indirectly)

    je NextInstructionLabel
    jne MethodFooter_Exception // if the bit was set, jump to method footer, or catch/finally block

  NextInstructionLabel:
    // Continue method

  MethodFooter_Normal:
    mov ecx, 0  // explicitly set ECX to 0, meaning no exception happened
  MethodFooter_Exception:
    // cleanup our locals
    pop EBP

    ret // return to caller

PrintValue(int):
    push ebp  // Save original EBP
    mov ebp, esp // Set new method base pointer

    mov EAX, [EBP + 8] // the argument value is now in EAX

    // do something here.

  MethodFooter_Normal:
    mov ecx, 0  // explicitly set ECX to 0, meaning no exception happened
  MethodFooter_Exception:
    // cleanup our locals
    pop EBP

    ret 4 // return to caller, cleaning up 4 bytes from the stack.

Calling a simple method with 1 Int32 argument, which returns an Int32 value

Calling method:
    push ebp  // Save original EBP
    mov ebp, esp // Set new method base pointer

    push 0x42   // push argument value 0x42 on the stack

    call CalculateValue(int) // call the actual method

    test dword ECX, 0x2 // if second bit of the ECX register is set, an exception occurred in the method (directly or indirectly)

    je NextInstructionLabel
    add esp, 4 // clean up the return value from the stack.
    jne MethodFooter_Exception // if bit was set, jump to method footer, or catch/finally block

  NextInstructionLabel:
    // Continue method
    pop EAX // value was on the stack.

  MethodFooter_Normal:
    mov ecx, 0  // explicitly set ECX to 0, meaning no exception happened
  MethodFooter_Exception:
    // cleanup our locals
    pop EBP

    ret // return to caller

CalculateValue(int):
    push ebp  // Save original EBP
    mov ebp, esp // Set new method base pointer

    mov EAX, [EBP + 8] // the argument value is now in EAX

    // do something here.

  MethodFooter_Normal:
    mov ecx, 0  // explicitly set ECX to 0, meaning no exception happened
  MethodFooter_Exception:
    // cleanup our locals
    pop EBP

    ret // return to caller. Don't cleanup here.

Calling a simple method with no arguments, which returns an Int32 value

Calling method:
    push ebp  // Save original EBP
    mov ebp, esp // Set new method base pointer

    push dword 0  // make room on the stack for the return value
    call GetValue() // call the actual method

    test dword ECX, 0x2 // if second bit of the ECX register is set, an exception occurred in the method (directly or indirectly)

    je NextInstructionLabel
    add esp, 4 // clean up the return value from the stack.
    jne MethodFooter_Exception // if bit was set, jump to method footer, or catch/finally block

  NextInstructionLabel:
    // Continue method
    pop EAX // value was on the stack.

  MethodFooter_Normal:
    mov ecx, 0  // explicitly set ECX to 0, meaning no exception happened
  MethodFooter_Exception:
    // cleanup our locals
    pop EBP

    ret // return to caller

GetValue():
    push ebp  // Save original EBP
    mov ebp, esp // Set new method base pointer

    // do something here

    push 0x42  // push value 0x42 on the stack, which is our return value.

  MethodFooter_Normal:
    mov ecx, 0  // explicitly set ECX to 0, meaning no exception happened

    pop eax // pop result from stack
    mov [EBP + 8], eax  // store result on stack, but below the return pointer.
  MethodFooter_Exception:
    // cleanup our locals
    pop EBP

    ret // return to caller. Don't clean up here.
Clone this wiki locally