调用约定规定了哪些寄存器由调用者保存、哪些寄存器由被调用者恢复、参数如何传递、栈由谁清除等。有很多调用约定如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, 4var:    .long   10    .text    .global func    .type       func, @functionfunc:    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.outvar 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.outvar 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.outa 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.outa is 3 | 
使用push和pop相关寄存器即可,如果没有用到这些寄存器,可不需要压栈。
示例:略