聊聊调用约定

调用约定

不同的调用约定需要定义参数传递的方式,堆栈平衡的方式以及返回值保存在何处。不同cpu架构使用的调用约定也不相同。

x86调用约定

_stdcall

_stdcall调用约定是windows API使用的调用约定,其规定了函数在调用时参数从右向左入栈,被调用者进行堆栈平衡,返回值保存在eax中。

_cdecl

如果单单只有_stdcall的话会有一个问题,对于可变参数的函数而言,因为函数本身不知道会有几个参数传递,所以自身无法进行堆栈的平衡。这就产生了_cdecl调用约定,此调用约定会与_stdcall唯一的区别就是其规定由函数调用者平衡堆栈。

_fastcall

_fastcall调用约定是一种快速调用约定,其比上述两种调用约定快速的原因就是其使用寄存器传参,相对于使用内存进行传参要快。其规定函数的前两个参数使用ecx和edx,而其余参数从右向左依次入栈,由被调用者进行堆栈平衡。返回值保存在eax中。

_thiscall

_thiscall调用约定是唯一一种不能显示指定的调用约定,c++在进行面向对象编程时会使用这种调用约定。其最大的特点就是使用ecx寄存器传递this指针,然后其他参数从右到左依次入栈,由被调用者进行堆栈平衡。

x64调用约定

x64调用约定只有一种调用约定,这种调用有点类似与x86的fastcall,但又不完全相同,看一下官方的解释。

x64使用rcx,rdx,r8,r9来传递函数的前四个参数,然后其余的参数通过堆栈传递,结果保存在rax中。这里面有一个重点就是x64下参数传递超过4个后通过堆栈传递,但是并不进行压栈操作,而是由调用者预留出来参数保存在堆栈上所需要的空间,直接将参数mov到对应的堆栈中。同时被调用者可以将寄存器的值溢出到堆栈。

例如在x64中测试函数test_func只有4个参数,在main函数中调用他并查看反汇编可以看到main函数一开始申请了0xE8大小的栈空间,然后将四个参数分别利用rcx, rdx, r8, r9进行传递。

查看test_func函数的反汇编可以发现其会将寄存器中保存的参数值溢出存放到栈上,而这些栈空间是main函数在一开始预留的。

如果test_func函数有5个参数的话,main函数一开始会多申请0x10大小的栈空间(0xF8),并在调用test_func函数的时候将第5个参数其放在指定的栈空间中,所以test_func无论有几个参数,都不需要自己去平衡堆栈,因为保存参数的堆栈空间是由调用者main函数预留申请的。

所以x64上子函数无需对参数进行堆栈平衡。这也就解决了x86上含有可变参数的函数调用过程中的堆栈平衡问题。例如下图中main函数调用printf函数时,除了前四个参数之外的其余参数都是通过直接操作堆栈传递给printf函数的,这些堆栈空间是main函数在一开始预留的,所以printf函数无需考虑有几个参数并对其对应的栈空间进行平衡,这些栈空间是由main函数在返回时进行平衡的。

x64函数调用时整个堆栈视图如下图所示,所以我认为对于x64中的调用约定而言没有谁平衡堆栈这一说,原因是在函数调用的过程中参数的传递并不涉及到压栈和出栈的操作,就算有也应该是调用者平衡的堆栈,因为参数使用的堆栈空间是调用者预留的。

如果在x64中显式指定函数的调用约定,编译器会直接忽略。

arm调用约定

  • arm32调用约定是通过r0-r3传递前四个参数,其余参数从右到左依次入栈,返回值保存在r0中,返回地址保存在LR(r15)中。
  • arm64调用约定是通过x0-x7传递前8个参数,其余参数从右到左依次入栈,返回值保存在x0中,返回地址保存在X30中。

那arm中是调用者平衡堆栈还是被调用者平衡堆栈呢?实际arm采用的是和x64架构一样的方式,调用者申请预留空间用来存放超过寄存器传参限制的其他参数(例如arm32中传递超过4个参数,arm64中传递超过8个参数),对于可变参数的函数而言其也会将寄存器参数的值通过溢出保存在调用者预留的堆栈中。

堆栈视图分两种情况,分别是被调用函数不存在可变参数和存在可变参数,下面是arm32中被调用函数不存在可变参数的堆栈视图。

对于被调用函数存在可变参数的情况,被调用函数会将通过寄存器传递的参数保存在与通过堆栈传递的参数连续的堆栈内存处,目的是方便获取所有参数。

无论被调用者有无可变参数,被调用函数只需要考虑平衡自己申请的堆栈空间,而无需考虑超过寄存器传参数量限制之外的其他参数的堆栈,因为这些堆栈空间是由调用者预留申请的与被调用者无关。

posted @ 2022-09-23 15:55  怎么可以吃突突  阅读(244)  评论(0编辑  收藏  举报