cdecl是c语言的函数默认调用约定,这种调用约定要求函数参数的压栈顺序是从右向左。
假设函数是 int func(int arg1, int arg2, int arg3),使用c的默认调用约定,那么参数3先压入栈,参数1最后压入栈,栈的生长方向是向低地址方向生长,所以参数3的地址最大。
函数的调用栈如下图所示:
esp是栈指针,总是指向栈顶,ebp是栈帧指针,指向为函数分配的栈帧(stack frame)的起始位置。
函数调用涉及的指令如下:
call指令
call指令用于函数调用,相当于如下两条指令:
push %eip(eip保存call指令的下一条指令,也就是函数的返回地址)
jmp被调用函数的起始地址。
call Label:直接跳转到函数地址
call *Operand:间接跳转到函数地址
leave指令用于在函数返回前清理调用栈,相当于如下两条指令:
mov %ebp %esp
pop %ebp
ret指令用于从当前的函数调用栈返回,相当于如下指令:
pop %eip //把函数的返回地址从栈中弹出,放入eip
然后处理器根据eip的值,执行函数返回地址指向的那条指令。
IA32寄存器使用惯例:
eax,ecx和edx是调用者保存寄存器,被调用函数可以修改这些寄存器的值。
ebx,esi和edi是被调用者保存寄存器,被调用函数不能破坏寄存器的值,如果要使用,先要保存原始值,在函数返回前恢复回去。
示例:
int swap_add(int *xp, int *yp)
{
int x = *xp;
int y = *yp;
*xp = y;
*yp = x;
return x + y;
}
int caller()
{
int arg1 = 534;
int arg2 = 1057;
int sum = swap_add(&arg1, &arg2);
int diff = arg1 - arg2;
return sum * diff;
}
gcc -O1 -c -m32 swapadd.c
objdump -d swapadd.o
swapadd.o: file format elf32-i386
Disassembly of section .text:
00000000 <swap_add>:
0: 55 push %ebp //把旧的ebp压人栈
1: 89 e5 mov %esp,%ebp//把当前栈指针值设置为ebp值
3: 53 push %ebx//保存ebx
4: 8b 55 08 mov 0x8(%ebp),%edx//edx = &arg1
7: 8b 4d 0c mov 0xc(%ebp),%ecx // ecx = &arg2
a: 8b 1a mov (%edx),%ebx //ebx=arg1
c: 8b 01 mov (%ecx),%eax //eax=arg2
e: 89 02 mov %eax,(%edx) //arg1=旧的arg2
10: 89 19 mov %ebx,(%ecx) //arg2=旧的arg1
12: 01 d8 add %ebx,%eax //eax = arg1+arg2
14: 5b pop %ebx //恢复ebx
15: 5d pop %ebp //恢复ebp
16: c3 ret
00000017 <caller>:
17: 55 push %ebp //把旧的ebp压入堆栈
18: 89 e5 mov %esp,%ebp //把当前栈指针值设置为ebp值
1a: 83 ec 18 sub $0x18,%esp //esp=esp-0x18=esp-24
1d: c7 45 fc 16 02 00 00 movl $0x216,0xfffffffc(%ebp) //ebp-4的地址装入534
24: c7 45 f8 21 04 00 00 movl $0x421,0xfffffff8(%ebp)ebp-8的地址装入1057
2b: 8d 45 f8 lea 0xfffffff8(%ebp),%eax//eax=ebp-8
2e: 89 44 24 04 mov %eax,0x4(%esp)//arg2的地址
32: 8d 45 fc lea 0xfffffffc(%ebp),%eax//eax=ebp-4
35: 89 04 24 mov %eax,(%esp)//arg1的地址
38: e8 fc ff ff ff call 39 <caller+0x22> //调用swap_add函数
3d: 8b 55 fc mov 0xfffffffc(%ebp),%edx//edx=arg1
40: 2b 55 f8 sub 0xfffffff8(%ebp),%edx//edx = arg1 –arg2
43: 0f af c2 imul %edx,%eax //eax = (arg1-arg2)*sum
46: c9 leave
47: c3 ret
caller函数调用swap_add函数执行pop %ebx指令前的栈帧如下图所示:
gcc 在分配栈空间时有 8 个字节未使用,存在浪费,这是为了保证每个函数使用的栈空间都是 16 字节的整数倍, 24 个字节加上保存 ebp 的 4 个字节再加上保存函数返回地址的 4 个字节是 32 个字节。这样做的好处是保证访问数据的严格对齐。