调用约定规定了哪些寄存器由调用者保存、哪些寄存器由被调用者恢复、参数如何传递、栈由谁清除等。有很多调用约定如cdecl、fastcall、stdcall等。通常调用约定是Application Binary Interface (ABI) 标准的一部分。
在Linux世界里,gcc是事实上的标准。gcc约定:
调用者:caller-saved register包括%EAX, %ECX, %EDX,他们被首先压入栈中。参数以按调用顺序相反的顺序压入栈中(第一个参数最后入栈),参数是以32位存储的。64位参数用两个32位存储,其它参数一律占32位。返回地址入栈。
被调用者:EBP入栈,ESP->EBP,局部变量分配栈,如果有临时数据,也分配栈,如果使用了callee saved registers (EBX, EDI, ESI),把它们也压入栈。
32位及以下整数返回值存储到EAX寄存器中。
栈是从上向下生长的,栈的分布如下:
图1 stack内容排列
栈中的上述内容,只有返回地址和调用者的ebp是必须的,其它是可选的,例如,如果调用者没有用到%edx,则调用者无需将其压入栈,不管被调用者将其改成啥,我都不关心。
gcc约定栈对齐到16字节。
添加汇编文件func.s,其包含一个全局变量var和一个函数func。func设置全局变量var的值为1。直接贴代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | .globl var .data .align 4 .type var, @object .size var, 4 var: .long 10 .text .global func .type func, @function func: pushl %ebp movl %esp, %ebp movl $0x1, var leave ret |
func函数汇编指令解释:
前两行,保存父函数的栈。
movl $0x1, var,将立即数1移动到var。
leave,恢复父函数的栈。
ret,返回到父函数执行。
添加调试main.c代码:
1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h> extern int var; extern void func(); int main( int argc, char **argv) { func(); printf ( "var changed to %d\n" , var); return 0; } |
编译: cc -m32 func.s main.c。运行./a.out结果:
1 2 | herbert@Lenovo: /work/assembly/i386/func $ . /a .out var changed to 1 |
由于我的linux系统是64位,所以使用-m32选项,编译32位可执行文件。
参数可以通过ebp找到,参数n(0是第一个参数)的地址为:ebp + 8 + 4 * n;
下面将传入的参数写入到var。
1 2 3 4 5 6 7 8 | func: pushl %ebp movl %esp, %ebp mov 8(%ebp), %eax movl %eax, var nop popl %ebp ret |
参考gcc的编译结果, 这里有一点改进。leave替换成了nop和popl %ebp。因为没有局部变量,ebp和esp相等。所以没有必要将ebp拷贝到esp。
修改main.c
1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h> extern int var; extern void func(int n); int main(int argc, char **argv) { func(255); printf ( "var changed to %d\n" , var); return 0; } |
编译后执行:
1 2 | herbert@Lenovo: /work/assembly/i386/func $ . /a .out var changed to 255 |
返回结果,只将结果存储到EAX寄存器即可。
下面,将var作为返回结果。
1 2 3 4 5 6 | func: pushl %ebp movl %esp, %ebp movl var, %eax leave ret |
修改main.c
1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h> extern int var; extern int func(); int main( int argc, char **argv) { int a = func(); printf ( "a is %d\n" , a); return 0; } |
编译后执行结果
1 2 | herbert@Lenovo: /work/assembly/i386/func $ . /a .out a is 10 |
局部变量,在紧跟着ebp指针指向的地址下面。cc编译结果显示,先定义的变量后入栈。使用局部变量时,需要维护栈,使其向下增长,且需要对齐到16字节。
下面的定义两个局部变量,返回它们的和。
1 2 3 4 5 6 7 8 9 10 | func: pushl %ebp movl %esp, %ebp subl $16, %esp movl $1, -4(%ebp) movl $2, -8(%ebp) movl -4(%ebp), %eax addl -8(%ebp), %eax leave ret |
main.c不变,运行结果如下:
1 2 | herbert@Lenovo: /work/assembly/i386/func $ . /a .out a is 3 |
使用push和pop相关寄存器即可,如果没有用到这些寄存器,可不需要压栈。
示例:略