Function Calling Conventions #
The details are system dependent, i.e. Linux on x86-64.
We should start off by understanding how function calls are implemented. Let’s consider the following function
static double add(double a, double b) {
double c = a + b;
return c;
}
which we compile and disassemble:
$ g++ -g -c addition.cpp -o addition.o
$ objdump -S -Mintel addition.o
...
static double add(double a, double b) {
0: f2 0f 11 44 24 e8 movsd QWORD PTR [rsp-0x18],xmm0
6: f2 0f 11 4c 24 e0 movsd QWORD PTR [rsp-0x20],xmm1
double c = a + b;
c: f2 0f 10 44 24 e8 movsd xmm0,QWORD PTR [rsp-0x18]
12: f2 0f 58 44 24 e0 addsd xmm0,QWORD PTR [rsp-0x20]
18: f2 0f 11 44 24 f8 movsd QWORD PTR [rsp-0x8],xmm0
return c;
1e: f2 0f 10 44 24 f8 movsd xmm0,QWORD PTR [rsp-0x8]
24: 66 48 0f 7e c0 movq rax,xmm0
}
29: 66 48 0f 6e c0 movq xmm0,rax
2e: c3 ret
Calling conventions on x86_64 are such that the first eight floating-point
arguments are passed in via the registers xmm0, …, xmm7. Hence the first
act is to copy a and b into their assigned places on the stack. Their
respective places are 0x18 and 0x20 down from the stack pointer. Naturally,
for adding the two we want them in a register, so in line c: we copy the value
back to xmm0; and then add the other value from the stack into xmm0.
There’s space at 0x08 bytes down from the stack pointer. This is where c
was allocated on the stack. Therefore, we copy the result of a + b into
it’s assigned place on the stack. The calling conventions on x86_64 are such
that floating-point values are to be returned in xmm0. What follows is a
little detour via rax. I think this is just for compatibility, since rax is
the register return values.
While this may seem utterly pointless, let’s turn on optimizations and it’ll instantly make sense:
$ g++ -O2 -g -c addition.cpp -o addition.o
$ objdump -S -Mintel addition.o
...
0000000000000000 <_Z3adddd>:
double add(double a, double b) {
double c = a + b;
0: f2 0f 58 c1 addsd xmm0,xmm1
return c;
}
Let’s turn our attention to the caller by adding the function:
double call_add(double a, double b) {
double c = add(a, b);
return c;
}
Unfortunately, the disassembly is a tad messy, it again needs to prepare first
copy xmm0 and xmm1 to their respective places in the stack then prepare
the new (same) values of xmm0 and xmm1. But once it’s done it’ll get to the
instruction call.
$ g++ -g -c addition.cpp -o addition.o
$ objdump -S -Mintel addition.o
...
000000000000002f <_Z8call_adddd>:
double call_add(double a, double b) {
...
51: e8 aa ff ff ff call 0 <_ZL3adddd>
...
References #
- AMD Architecture Programmer Manual Volume 1: Application Programming
- https://people.freebsd.org/~obrien/amd64-elf-abi.pdf
- http://www.egr.unlv.edu/~ed/assembly64.pdf