进入函数在汇编中其实就是callq,出函数就是retq。

而callq和retq的功能之前已经提到过

参数的传递

在x86-64位系统中,有六个寄存器负责传入参数,分别是rdi,rsi,rdx,rcx,r8,r9,同时程序返回时返回值放在rax中。如果大于6个,多的部分就要放在栈上。如果我们想操作在栈上的参数,就要通过 x(%rsp)来操作了。

例如有7个参数,此时第7个参数就放在栈上那么 8(%rsp)就可以访问这个参数了,pushq操作分为两部分,首先是rsp-8,然后把数据放进去。这个时候其实数据就是在rsp到rsp+8的区域中,也就是说我们直接movq (%rsp)…就可以访问到我们刚才放进去的元素了

我们这里不采用(%rsp)的原因是因为调用函数是callq,在最后应该是把rip放到栈中,所以要加8到下一个元素。

如果有8个参数,那么栈中要先存第8个,然后再存第7个。

栈上的局部储存

由于现代编译器的不断优化和寄存器的增多,我们一般不把局部变量放到栈中,但是有的时候我们不得不把他们放到栈中。

  1. 寄存器不足时
  2. 某一变量用了&(取地址),因此这个时候不得不把它放到栈中,这样才有一个地址
  3. 当局部变量是数组或结构体时

例如:

long caller()
{
long x = 534;
long y = 1057;
long sum = swap_add(&x, &y);
...
}
汇编代码为:
caller:
subq $16, %rsp
movq $534, (%rsp)
movq $1057, 8(%rsp)
leaq 8(%rsp), %rsi
movq %rsp, %rdi
callq swap_add
...
addq $16,%rsp

汇编中第一行代码为rsp-16,这是为局部变量分配内存空间。534存在最近的一个,1057存在较远的一个,这与前面多个参数存储符合。

注意在这个函数结束时,分配的栈空间也要清除,也就是最后的addq

例2:

long call_proc()
{
long x1 = 1;
int x2 = 2;
short x3 = 3;
char x4 = 4;
proc(x1,&x1,x2,&x2,x3,&x3,x4,&x4);
return x1-x2;
}
汇编:
call_proc:
subq $32,%rsp
movq $1, 24(%rsp)
movq $2, 20(%rsp)
movq $3, 18(%rsp)
movq $4, 17(%rsp)
leaq 17(%rsp), %rax

movq %rax, 8(%rsp)
movl $4, (%rsp)
leaq 18(%rsp), %r9
movl $3, %r8d
leaq 20(%rsp), %rcx
movl $2,%edx
leaq 24(%rsp), %rsi
movl $1, %edi
call proc
...
add $32,%rsp

注意空行前面是存储地址,因为后面函数的参数中有使用。空行后面就是把8个参数赋值。可能这里会有疑问就是这里把参数都赋了值,那么函数返回到原函数时那些参数值不都变了吗?

按照惯例,寄存器 %rbx,%rbp 和 %r12到%15被划分成被调用者保存寄存器(表示她会在被调用者函数中被保护好)。其他寄存器[1]
。叫做调用者保存寄存器,这些寄存器由调用者自己去保护,例如rdi,rsi等,被调用者并不会保护这些寄存器,也就是说传入参数时,要先把这些寄存器压入栈中,以防止被改变。

一般来说,被调用者保存寄存器中存储的是局部变量,而调用者保存寄存器中存储的是计算过程的中间值。

例:

long p(long x, long y)
{
long u = q(y);
long v = q(x);
return u+v;
}
汇编:
p:
pushq %rbp
pushq %rbx
subq $8,%rsp
movq %rdi, %rbp
movq %rsi, %rdi
call q
movq %rax, %rbx//保存得到的函数值
movq %rbp, %rdi
calll q
addq %rbx, %rax
addq $8, %rsp
popq %rbx
popq %rbp
ret

这段代码开始把rbp和rbx拉进栈,是因为后面要改变rbx和rbp,而p本身也是一个函数,要保证被调用者保存寄存器不会被改变,之后在q函数传递参数前先把p的参数保存到rbp和rbx中,这样就保证了参数在函数中不会被破坏。之后传递参数时也可以直接从rbp中传入,

栈中的内容

从上面我们可以看到,栈中的内容可能会有寄存器,局部变量,下一个函数所需要的参数等。

有三类函数,一种是只有调用者的身份,一种是既有调用者又有被调用者的身份,第三种是只有被调用者的身份。

对于第一种。首先保存参数,之后保存调用者保存寄存器和局部变量,然后保存返回地址。

对于第二种,同样首先保存参数,之后保存局部变量,但是这里还可能要保存被调用者保存寄存器。同样保存返回地址

对于第三种,其他大致相同,但是不需要保存被调用者保存寄存器了。

对于函数的大致过程,首先传入参数,然后运用call进入函数。然后里面可能有一些局部变量需要保存或者寄存器需要保存,这时我们需要分配一个栈帧。之后如果进入函数,首先我们要传递参数(如果进入多个函数且这些函数运动了调用者函数的参数那此时先要把调用者参数放到被调用者保存寄存器中,防止调用者的参数被破坏)。然后call进入函数,被调用者函数返回前先要把被调用者保存寄存器中的内容弹出,和把栈帧释放。之后在调用者函数返回之前要先把调用者保存寄存器弹出,最后返回。


  1. 1.rsp除外
</div>