huolong blog

x86和x64汇编传参问题

先看看x86的传参

push eax
call xxx

xxx fun proc

push        ebp          保存栈底
mov         ebp,esp      设置ebp
sub         esp,0C0h     开辟局部变量空间
push        ebx          保存寄存器环境
push        esi  
push        edi  

pop         edi          恢复寄存器环境
pop         esi  
pop         ebx          
mov         esp,ebp      释放局部变量空间
pop         ebp          恢复栈底
ret                      返回,平栈,如果是c的调用方式 则add esp,xxx在外部平栈, stdcall 则内部平栈 ret 4

总结一下就是:

  1. 往栈中存放参数
  2. 将返回地址入栈
  3. 保存栈底
  4. 栈内部进行自己的 申请空间 保存环境 以及释放.

再看看x64下的参数传递

调用者部分:

sub rsp,0x28
mov r9,1
mov r8,2
mov rdx,3
mov rcx,4
call xxx
add rsp,0x28

被调函数:

xxx                 

mov qword ptr [rsp + 0x20],r9
mov qword ptr [rsp + 0x18],r8
mov qword ptr [rsp + 0x10],rdx
mov qword ptr [rsp + 8],rcx
push rbp

xxx 

注意:

  1. 在 64位 以前,参数是把所有参数从右到左依次压栈。在 64 位后,Windows 下是依次使用 rcx、rdx、r8、r9 这四个寄存器传递从左到右四个参数,如果参数大于 4 个,再从右到左依次压栈,最后还有留下 32 字节的空间才能调用函数(空着的这32字节,通过看编译 C 语言生成的汇编代码,发现被用来存放那四个在寄存器里的参数。参数不足 4 个,也会预留 32 字节的空间).
  2. 而在64位linux下对于普通函数调用是把前 6 个参数,依次用 rdi、rsi、rdx、rcx、r8、r9。对于系统调用是把前 6 个参数,依次用 rdi、rsi、rdx、r10、r8、r9。然后剩下的参数从右到左依次压栈。
  3. Windows下调用者在每个栈帧中提供了一个“寄存器参数区”。在调用一个函数时,在返回地址之前,在栈上最后分配的是用于至少4个寄存器(每个8字节)的空间。这个区域对被调用者可用,而无需显式地分配它。这对可变参数函数以及调试(提供参数已知的位置,与此同时寄存器可能重用于其他目的)是有用的。尽管这个区域最初的设想是用作存储在寄存器中传递的4个参数,现在编译器也为其他优化目的而使用它(例如,如果函数的局部变量需要少于32字节的栈空间,可以使用这个区域,而无需触碰rsp)。
  4. 栈按照16字节对齐,所以需要sub rsp,0x28。

可有可无的帧指针

64位的大部分的程序,都加了优化编译选项:-O2,这几乎是普遍的选择。在这种优化级别,甚至更低的优化级别-O1,都已经去除了帧指针,也就是%ebp中再也不是保存帧指针,而且另作他途。

在x86-32时代,当前栈帧总是从保存%ebp开始,空间由运行时决定,通过不断push和pop改变当前栈帧空间;x86-64开始,GCC有了新的选择,优化编译选项-O1,可以让GCC不再使用栈帧指针,下面引用 gcc manual 一段话 :

-O also turns on -fomit-frame-pointer on machines where doing so does not interfere with debugging.

这样一来,所有空间在函数开始处就预分配好,不需要栈帧指针;通过%rsp的偏移就可以访问所有的局部变量。