调用约定
1. x86 体系
x86 体系下有四种调用约定
1.1 四种调用约定的区别
调用约定 | __cdecl | __stdcall | __fastcall | __thiscall |
---|---|---|---|---|
调用规范 | int __cdecl function(int a,int b) | int __stdcall function(int a,int b) | int __fastcall function(int a,int b) | |
传参顺序 | 右至左 | 右至左 | 右至左 | 右至左 |
堆栈平衡 | 主调函数平衡堆栈 | 被调函数平衡堆栈 | 被调函数平衡堆栈 | 对参数个数不定的,调用者清理栈,否则函数自己清理栈 |
参数种类 | 参数靠堆栈传递,参数个数不固定 | 参数放在堆栈中 | x86:从左到右的前两个参数放在ecx,edx中进行push,剩余参数从右至左依次压参 x64:从左到右的参数放在ecx,edc,r8,r9中进行push,剩余参数从右至左依次压参 |
如果参数个数确定,this指针通过ecx传递给被调用者; 如果参数个数不确定,this指针在所有参数压栈后被压入堆栈; |
函数种类 | c,c++库函数(默认) | WINAPI,NTAPI,CALLBACK,PASCAL宏 | C++调用类成员函数 |
1.2 汇编角度分析
1)__cdecl 函数名在符号表中被记录为_function
push 1
push 2
call function
add esp,8 ; 注意:这里调用者在恢复堆栈
; 函数汇编:
push ebp ; 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复
mov ebp,esp ; 保存堆栈指针
mov eax,[ebp + 8H] ; 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] ; 堆栈中ebp + 12处保存了b
mov esp,ebp ; 恢复esp
pop ebp
ret ; 注意,这里没有修改堆栈
2)__stdcall 编译时,这个函数的名字被翻译成_function@8
push 1 ; 第一个参数入栈
call function ; 调用参数,注意此时自动把cs:eip入栈
; 函数汇编:
push ebp ; 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复
mov ebp,esp ; 保存堆栈指针
mov eax,[ebp + 8H] ; 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] ; 堆栈中ebp + 12处保存了b
mov esp,ebp ; 恢复esp
pop ebp
ret 8
2. x64 体系
2.1 x64调用约定
在64位下只有一种调用约定,类似于fastcall,要写其他的也不会出错,这主要是为了兼容以前定义的32的头文件
2.2 x86与x64的区别
x86到x64的两个重要修改:
- 64 位寻址能力
- 一组通用展开 64 位寄存器(共16个)
2.3 传递参数
使用寄存器(对前四个变量)和堆栈帧传递其他参数。
-
前4个参数使用 rcx rdx r8 r9传递
-
如果参数是浮点/双精度型,则它们在 XMM0L、XMM1L、XMM2L 和 XMM3L 中传递。
-
16 字节的参数由引用传递。
不再像32位一样push ebp mov ebp,esp.而是只保存了rdi就退出
2.4 寄存器传参
-
1个参数,采用ecx传参
-
参数大于32位采用rcx传参
函数里面把参数复制到了rsp+8
-
4个参数,采用ecx,edx,r8d,r9d
-
参数大于32位采用的是rcx,rdx,r8,r9寄存器
在函数里,把参数复制到rsp+0x20 +0x18 +0x10 +0x8的位置
-
大于4个参数,传参多了一个rsp+0x20内存.加上call对esp+8的位置,实际在函数里这个内存地址是rsp+0x28。
寄存器传参也要分配栈首先看一下没有调用函数时提升的栈空间是0x10
调用一个没有参数的函数,栈空间提升了0x10. 这是给函数的返回地址和多给了8字节的栈空间。
这8字节是给函数内部保存寄存器参数的值的。mov [rsp+8],rcx
2.5 易变寄存器和非异变寄存器操作
异变寄存器:rax rcx rdx r8 r9 r10 r11 其余为非异变寄存器
push pop指令仅用来保存非易变寄存器
2.6 汇编调用函数的问题
即使函数只有一个参数,也得分配0x20的栈空间 sub rsp,0x20
2.7 通常不使用rbp寻址栈内存
通常不使用rbp寻址栈内存,所以rsp在函数中尽量保持稳定(一次性分配参数和变量空间)
如下图:连续调用4次函数,并没有像32位那样add esp,xxx,而是直接在函数main函数头部sub rsp,0x20 ,在尾部add sup,0x20
对于调用的函数参数少于4个字节的情况下栈空间就够了。多余4个会分配更多空间。
像下面这样其中有一个函数的参数是5个会出现什么情况呢?
可以看出现在是sub rsp,0x30 也就是说编译器并不是按照最多参数5个分配0x28而是对其了0x10。
直接分配的0x30字节
2.8 x64调用约定特性
- 前四个整型或指针类型参数由RCX,RDX,R8,R9依次传递,前四个浮点类型参数由XMM0,XMM1,XMM2,XMM3依次传递。
- 调用函数为前四个参数在调用栈上保留相应的空间,称作shadow space或spill slot。即使被调用方没有或小于4个参数,调用函数仍然保留那么多的栈空间,这有助于在某些特殊情况下简化调用约定。
- 除前四个参数以外的任何其他参数通过栈来传递,从右至左依次入栈。
- 由调用函数负责清理调用栈。
- 小于等于64位的整型或指针类型返回值由RAX传递。
- 浮点返回值由XMM0传递。
- 更大的返回值(比如结构体),由调用方在栈上分配空间,并有RCX持有该空间的指针并传递给被调用函数,因此整型参数使用的寄存器依次右移一格,实际只可以利用3个寄存器,其余参数入栈。函数调用结束后,RAX返回该空间的指针。
注意:
- 除RCX,RDX,R8,R9以外,RAX、R10、R11、XMM4 和 XMM5也是易变化的(volatile)寄存器。
- RBX, RBP, RDI, RSI, R12, R14, R14, and R15寄存器则必须在使用时进行保护。
- 在寄存器中,所有参数都是右对齐的。小于64位的参数并不进行高位零扩展,也就是高位是无法预测的垃圾数据。