© Jo Van Hoey 2019
J. Van HoeyBeginning x64 Assembly Programminghttps://doi.org/10.1007/978-1-4842-5076-1_15

15. Calling Conventions

Jo Van Hoey1 
(1)
Hamme, Belgium
 

Calling conventions describe how you transfer variables to and from functions. If you will be using only functions that you have built yourself, you do not have to care about calling conventions. But when you are using C functions from the C library, you need to know in which registers you have to put the values to be used by that function. Also, if you write assembly functions for building a library that will be used by other developers, you’d better follow some convention for which registers to use for which function arguments. Otherwise, you will have lots of conflicts with arguments.

You already noticed that with the function printf, we put an argument in rdi, another in rsi, and yet another argument in xmm0. We were using a calling convention.

To avoid conflicts and the resulting crashes, smart developers designed calling conventions, a standardized way to call functions. It is a nice idea, but as you may expect, not everybody agrees with everybody else, so there are several different calling conventions. Up until now in this book we have used the System V AMD64 ABI calling convention, which is the standard on Linux platforms. But there is also another calling convention worth knowing: the Microsoft x64 calling convention to be used in Windows programming.

These calling conventions allow you to use external functions built with assembly, as well as functions compiled from languages such as C, without having access to the source code. Just put the correct arguments in the registers specified in the calling convention.

You can find out more about the System V AMD64 ABI calling convention at https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf . This Intel document has an overwhelming amount of detailed information about the System V application binary interface. In this chapter, we will show what you have to know to start calling functions in the standard way.

Function Arguments

Look back at the previous source files: for the circle calculations, we used xmm0 to transfer floating-point values from the main program to the circle function, and we used xmm0 to return the floating-point result of the function to the main program. For the rectangle calculation, we used rdi and rsi to transfer integer values to the function, and the integer result was returned in rax. This way of passing arguments and results is dictated by the calling convention.

Non-floating-point arguments, such as integers and addresses, are passed as follows:
  • The 1st argument goes into rdi.

  • The 2nd argument goes into rsi.

  • The 3rd argument goes into rdx.

  • The 4th argument goes into rcx.

  • The 5th argument goes into r8.

  • The 6th argument goes into r9.

Additional arguments are passed via the stack and in reverse order so that we can pop off in the right order. For instance, with 10 arguments, we have this:
  • The 10th argument is pushed first.

  • Then the 9th argument is pushed.

  • Then the 8th argument is pushed.

  • The 7th argument is pushed.

Once you are in the function, it is just a matter of getting the values from the registers. When popping the values from the stack, you have to be careful; remember that when a function is called, the return address is pushed on the stack, just after the arguments.
  • When you push the 10th argument, you decrease the stack pointer rsp by 8 bytes.

  • When you push the 9th argument, rsp decreases by 8 bytes.

  • When you push the 8th argument, rsp decreases by 8 bytes.

  • With the 7th argument, rsp decreases by 8 bytes.

  • Then the function is called; rip is pushed on the stack, and rsp decreases by 8 bytes.

  • Then rbp is pushed at the beginning of the function; as part of the prologue, rsp decreases by 8 bytes.

  • Then align the stack on a 16-byte boundary, so maybe another push is needed to decrease rsp by 8 bytes.

Thus, after we pushed the function’s arguments, at least two additional registers are pushed on the stack, i.e., 16 additional bytes. So, when you are in the function, to access the arguments, you have to skip the first 16 bytes on the stack, maybe more if you had to align the stack.

Floating-point arguments are passed via xmm registers as follows:
  • The 1st argument goes into xmm0.

  • The 2nd argument goes into xmm1.

  • The 3rd argument goes into xmm2.

  • The 4th argument goes into xmm3.

  • The 5th argument goes into xmm4.

  • The 6th argument goes into xmm5.

  • The 7th argument goes into xmm6.

  • The 8th argument goes into xmm7.

Additional arguments are passed via the stack; this is not accomplished with a push instruction as you might expect. We will show later how to do that, in the more advanced SIMD chapters.

A function returns a floating-point result in xmm0, and an integer number or address is returned in rax.

Complicated? Listing 15-1 shows an example that prints a number of arguments with printf.
; function5.asm
extern printf
section .data
      first      db    "A",0
      second     db    "B",0
      third      db    "C",0
      fourth     db    "D",0
      fifth      db    "E",0
      sixth      db    "F",0
      seventh    db    "G",0
      eighth     db    "H",0
      ninth      db    "I",0
      tenth      db    "J",0
      fmt1  db   "The string is: %s%s%s%s%s%s%s%s%s%s",10,0
      fmt2       db    "PI = %f",10,0
      pi         dq    3.14
section .bss
section .text
      global main
main:
push  rbp
mov   rbp,rsp
      mov   rdi,fmt1   ;first use the registers
      mov   rsi, first
      mov   rdx, second
      mov   rcx, third
      mov   r8, fourth
      mov   r9, fifth
      push  tenth      ; now start pushing in
      push  ninth      ; reverse order
      push  eighth
      push  seventh
      push  sixth
      mov   rax, 0
      call  printf
      and   rsp, 0xfffffffffffffff0 ; 16-byte align the stack
      movsd       xmm0,[pi] ; now print a floating-point
      mov   rax, 1           ; 1 float to print
      mov   rdi, fmt2
      call  printf
leave
ret
Listing 15-1

function5.asm

In this example, we pass all arguments in the correct order to printf. Note the reverse order of pushing the arguments.

Use your debugger to check rsp just before the call printf. The stack is not 16-byte aligned! The program did not crash because we did not ask printf to print a floating-point number. But the next printf does exactly that. Thus, before using printf, we have to align the stack, so we use the following instruction:
      and      rsp, 0xfffffffffffffff0

This instruction leaves all the bytes in rsp intact, except the last one: the last four bits in rsp are changed to 0, thus decreasing the number in rsp and aligning rsp on a 16-byte boundary. If the stack had been aligned to start with, the and instruction would do nothing. Be careful, though. If you want to pop values from the stack after this and instruction, you have a problem: you have to find out if the and instruction changed rsp and eventually adjust rsp again to its value before the execution of the and instruction.

Figure 15-1 shows the output.
../images/483996_1_En_15_Chapter/483996_1_En_15_Fig1_HTML.jpg
Figure 15-1

Output of function5

Stack Layout

Let’s look at an example where we can see what happens on the stack when we push function arguments. Listing 15-2 shows a program that uses a function to build a string, and when the function returns, the string is printed.
; function6.asm
extern printf
section .data
      first      db    "A"
      second     db    "B"
      third      db    "C"
      fourth     db    "D"
      fifth      db    "E"
      sixth      db    "F"
      seventh    db    "G"
      eighth     db    "H"
      ninth      db    "I"
      tenth      db    "J"
      fmt        db    "The string is: %s",10,0
section .bss
      flist      resb 11    ;length of string + terminating 0
section .text
      global main
main:
push  rbp
mov   rbp, rsp
      mov   rdi, flist      ; length
      mov   rsi, first      ; fill the registers
      mov   rdx, second
      mov   rcx, third
      mov   r8, fourth
      mov   r9, fifth
      push  tenth      ; now start pushing in
      push  ninth      ; reverse order
      push  eighth
      push  seventh
      push  sixth
      call  lfunc           ;call the function
      ; print the result
           mov   rdi, fmt
           mov   rsi, flist
           mov   rax, 0
           call  printf
leave
ret
;-----------------------------------------------
lfunc:
push  rbp
mov   rbp,rsp
      xor   rax,rax    ;clear rax (especially higher bits)
      mov   al,byte[rsi]     ; move content 1st argument to al
      mov   [rdi], al        ; store al to memory
      mov   al, byte[rdx]    ; move content 2nd argument to al
      mov   [rdi+1], al      ; store al to memory
      mov   al, byte[rcx]    ; etc for the other arguments
      mov   [rdi+2], al
      mov   al, byte[r8]
      mov   [rdi+3], al
      mov   al, byte[r9]
      mov   [rdi+4], al
; now fetch the arguments from the stack
      push  rbx              ; callee saved
      xor   rbx,rbx
      mov   rax, qword [rbp+16]   ; first value: initial stack
                                  ; + rip + rbp
      mov   bl, byte[rax]         ; extract the character
      mov   [rdi+5], bl     ; store the character to memory
      mov   rax, qword [rbp+24]   ; continue with next value
      mov   bl, byte[rax]
      mov   [rdi+6], bl
      mov   rax, qword [rbp+32]
      mov   bl, byte[rax]
      mov   [rdi+7], bl
      mov   rax, qword [rbp+40]
      mov   bl, byte[rax]
      mov   [rdi+8], bl
      mov   rax, qword [rbp+48]
      mov   bl, byte[rax]
      mov   [rdi+9], bl
      mov   bl,0
      mov   [rdi+10], bl
pop   rbx                   ; callee saved
mov   rsp,rbp
pop   rbp
ret
Listing 15-2

function6.asm

Here, instead of printing with printf immediately after we provide all the arguments, as we did in the previous section, we call the function lfunc. This function takes all the arguments and builds a string in memory (flist); that string will be printed after returning to main.

Look at the lfunc function. We take only the lower byte of the argument registers, which is where the characters are, using an instruction such as the following:
      mov       al,byte[rsi]

We store these characters one by one in memory, starting at the address in rdi, which is the address of flist, with the instruction: mov [rdi], al. Using the byte keyword is not necessary, but it improves the readability of the code.

It gets interesting when we start popping values from the stack. At the start of lfunc, the value of rsp, which is the stack address, is saved into rbp. However, between this instruction and the end of pushing the values in main, rsp was modified twice. First, when lfunc was called, the return address was pushed onto the stack. Then we pushed rbp as part of the prologue. In total, rsp was decreased by 16 bytes. To access our pushed values, we have to augment the value of the addresses by 16 bytes. That is why we used this to access the variable sixth:
      mov rax, qword [rbp+16]

The other variables are each 8 bytes higher than the previous one. We used rbx as a temporary register for building the string in flist. Before using rbx, we saved the content of rbx to the stack. You never know if rbx is used in main for other purposes, so we preserve rbx and restore it before leaving the function.

Figure 15-2 shows the output.
../images/483996_1_En_15_Chapter/483996_1_En_15_Fig2_HTML.jpg
Figure 15-2

Output of function6

Preserving Registers

We will now explain the instructions.
push      rbx                  ; callee saved
and
pop       rbx                  ; callee saved

It should be clear that you have to keep track of with happens with the registers during a function call. Some registers will be altered during the execution of a function, and some will be kept intact. You need to take precautions in order to avoid unexpected results caused by functions modifying registers you are using in the main (calling) program.

Table 15-1 shows an overview of what is specified in the calling convention.
Table 15-1

Calling Conventions

Register

Usage

Save

rax

Return value

Caller

rbx

Callee saved

Callee

rcx

4th argument

Caller

rdx

3rd argument

Caller

rsi

2nd argument

Caller

rdi

1st argument

Caller

rbp

Callee saved

Callee

rsp

Stack pointer

Callee

r8

5th argument

Caller

r9

6th argument

Caller

r10

Temporary

Caller

r11

Temporary

Caller

r12

Callee saved

Callee

r13

Callee saved

Callee

r14

Callee saved

Callee

r15

Callee saved

Callee

xmm0

First arg and return

Caller

xmm1

Second arg and return

Caller

xmm2-7

Arguments

Caller

xmm8-15

Temporary

Caller

The function called is the callee. When a function uses a callee-saved register, the function needs to push that register on the stack before using it and pop it in the right order afterward. The caller expects that a callee-saved register should remain intact after the function call. The argument registers can be changed during execution of a function, so it is the responsibility of the caller to push/pop them if they have to be preserved. Similarly, the temporary registers can be changed in the function, so they need to be pushed/popped by the caller if needed. Needless to say, rax, the returning value, needs to be pushed/popped by the caller!

Problems can start popping up when you modify an existing function and start using a caller-saved register. If you do not add a push/pop of that register in the caller, you will have unexpected results.

Registers that are callee saved are also called nonvolatile. Registers that the caller has to save are also called volatile.

The xmm registers can all be changed by a function; the caller will be responsible for preserving them if necessary.

Of course, if you are sure you are not going to use the changed registers, you can skip the saving of these registers. However, if you change the code in the future, you may get in trouble if you start using these registers without saving them. Believe it or not, after a couple of weeks or months, assembly code is difficult to read, even if you coded everything yourself.

One last note: syscall is also a function and will modify registers, so keep an eye on what a syscall is doing.

Summary

In this chapter, you learned about the following:
  • Calling conventions

  • Stack alignment

  • Callee/caller-saved registers

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.216.75