调用约定

调用约定

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的两个重要修改

  1. 64 位寻址能力
  2. 一组通用展开 64 位寄存器(共16个)

2.3 传递参数

​ 使用寄存器(对前四个变量)和堆栈帧传递其他参数。

  1. 前4个参数使用 rcx rdx r8 r9传递

  2. 如果参数是浮点/双精度型,则它们在 XMM0L、XMM1L、XMM2L 和 XMM3L 中传递。

  3. 16 字节的参数由引用传递。

    image-20230131012806787不再像32位一样push ebp mov ebp,esp.而是只保存了rdi就退出

2.4 寄存器传参

  1. 1个参数,采用ecx传参

    image-20230131014320342

  2. 参数大于32位采用rcx传参

    image-20230131014550347

    函数里面把参数复制到了rsp+8

    image-20230131015305269

  3. 4个参数,采用ecx,edx,r8d,r9d

    image-20230131015454781

  4. 参数大于32位采用的是rcx,rdx,r8,r9寄存器

    image-20230131015803762

    在函数里,把参数复制到rsp+0x20 +0x18 +0x10 +0x8的位置

    image-20230131015813678

  5. 大于4个参数,传参多了一个rsp+0x20内存.加上call对esp+8的位置,实际在函数里这个内存地址是rsp+0x28。
    寄存器传参也要分配栈

    首先看一下没有调用函数时提升的栈空间是0x10

    image-20230131015842995

    调用一个没有参数的函数,栈空间提升了0x10. 这是给函数的返回地址和多给了8字节的栈空间。
    这8字节是给函数内部保存寄存器参数的值的。mov [rsp+8],rcx

    image-20230131020004241

2.5 易变寄存器和非异变寄存器操作

​ 异变寄存器:rax rcx rdx r8 r9 r10 r11 其余为非异变寄存器
​ push pop指令仅用来保存非易变寄存器

image-20230131101251859

2.6 汇编调用函数的问题

​ 即使函数只有一个参数,也得分配0x20的栈空间 sub rsp,0x20

image-20230131101502366

2.7 通常不使用rbp寻址栈内存

​ 通常不使用rbp寻址栈内存,所以rsp在函数中尽量保持稳定(一次性分配参数和变量空间)

​ 如下图:连续调用4次函数,并没有像32位那样add esp,xxx,而是直接在函数main函数头部sub rsp,0x20 ,在尾部add sup,0x20
​ 对于调用的函数参数少于4个字节的情况下栈空间就够了。多余4个会分配更多空间。

image-20230131101619807

​ 像下面这样其中有一个函数的参数是5个会出现什么情况呢?
​ 可以看出现在是sub rsp,0x30 也就是说编译器并不是按照最多参数5个分配0x28而是对其了0x10。
​ 直接分配的0x30字节

image-20230131101632761

2.8 x64调用约定特性

  1. 前四个整型或指针类型参数由RCX,RDX,R8,R9依次传递,前四个浮点类型参数由XMM0,XMM1,XMM2,XMM3依次传递。
  2. 调用函数为前四个参数在调用栈上保留相应的空间,称作shadow space或spill slot。即使被调用方没有或小于4个参数,调用函数仍然保留那么多的栈空间,这有助于在某些特殊情况下简化调用约定。
  3. 除前四个参数以外的任何其他参数通过栈来传递,从右至左依次入栈。
  4. 由调用函数负责清理调用栈。
  5. 小于等于64位的整型或指针类型返回值由RAX传递。
  6. 浮点返回值由XMM0传递。
  7. 更大的返回值(比如结构体),由调用方在栈上分配空间,并有RCX持有该空间的指针并传递给被调用函数,因此整型参数使用的寄存器依次右移一格,实际只可以利用3个寄存器,其余参数入栈。函数调用结束后,RAX返回该空间的指针。

注意

  • 除RCX,RDX,R8,R9以外,RAX、R10、R11、XMM4 和 XMM5也是易变化的(volatile)寄存器。
  • RBX, RBP, RDI, RSI, R12, R14, R14, and R15寄存器则必须在使用时进行保护。
  • 在寄存器中,所有参数都是右对齐的。小于64位的参数并不进行高位零扩展,也就是高位是无法预测的垃圾数据。
posted @ 2023-01-31 10:20  修竹Kirakira  阅读(50)  评论(0编辑  收藏  举报