C/C++ 反汇编:关于函数调用约定

2022-12-28 18:12:32 浏览数 (1)

函数是任何一门高级语言中必须要存在的,使用函数式编程可以让程序可读性更高,充分发挥了模块化设计思想的精髓,今天我将带大家一起来探索函数的实现机理,探索编译器到底是如何对函数这个关键字进行实现的,并使用汇编语言模拟实现函数编程中的参数传递调用规范等。

说到函数我们必须要提起调用约定这个名词,而调用约定离不开栈的支持,栈在内存中是一块特殊的存储空间,遵循先进后出原则,使用push与pop指令对栈空间执行数据压入和弹出操作。栈结构在内存中占用一段连续存储空间,通过esp与ebp这两个栈指针寄存器来保存当前栈起始地址与结束地址,每4个字节保存一个数据。

一般编译器实现调用调用约定无外乎以下这几种:

  • CDECL:C/C 默认的调用约定,调用方平栈,不定参数的函数可以使用,参数通过堆栈传递.
  • STDCALL:被调方平栈,不定参数的函数无法使用,参数默认全部通过堆栈传递.
  • FASTCALL32:被调方平栈,不定参数的函数无法使用,前两个参数放入(ECX, EDX),剩下的参数压栈保存.
  • FASTCALL64:被调方平栈,不定参数的函数无法使用,前四个参数放入(RCX, RDX, R8, R9),剩下的参数压栈保存.
  • System V:类Linux系统默认约定,前八个参数放入(RDI,RSI, RDX, RCX, R8, R9),剩下的参数压栈保存.

当栈顶指针esp小于栈底指针ebp时,就形成了栈帧,栈帧中可以寻址的数据有局部变量,函数返回地址,函数参数等。不同的两次函数调用,所形成的栈帧也不相同,当由一个函数进入另一个函数时,就会针对调用的函数开辟出其所需的栈空间,形成此函数的独有栈帧,而当调用结束时,则清除掉它所使用的栈空间,关闭栈帧,该过程通俗的讲叫做栈平衡。而如果栈在使用结束后没有恢复或过度恢复,则会造成栈的上溢或下溢,给程序带来致命错误。

cdecl 调用者平栈: cdecl是C/C 默认调用约定,该调用方式在函数内不进行任何平衡参数操作,而是在退出函数后对esp执行加4操作,从而实现栈平衡。

该约定会采用复写传播优化,将每次参数平衡的操作进行归并,在函数结束后一次性平衡栈顶指针esp,且不定参数函数可使用此约定。

stdcall 被调用者平栈: stdcall与cdecl只在参数平衡上有所不同,其余部分都一样,但该约定不定参数函数无法使用。

cdecl调用方式的函数在同一作用域内多次被调用,会在效率上比stdcall高一些,因为它可以使用复写传播优化,而stdcall在函数内平衡栈,无法使用复写传播优化。

fastcall 被调用者平栈: fastcall效率最高,它可利用寄存器传递参数,一般前两个或前四个参数用寄存器传递,其余参数传递则转换为栈传递,此约定不定参数函数无法使用。

对于32位来说使用ecx,edx传递前两个参数,后面的用堆栈传递。 对于64位则会使用RCX,RDX,R8,R9传递前四个参数,后面的用堆栈传递。

使用esp寻址: 在O2编译器选项中,为了提高程序执行效率,只要栈顶是稳定的,就可以不再使用ebp指针,而是利用esp指针直接访问局部变量,这样可节省一个寄存器资源。

如下一段汇编代码,我们找到当前ESP基地址。

可以看到,esp 18就是第一个传入参数,那么程序在编译时,其实已经算出来了。

使用esp寻址后,不必每次进入函数后都调整栈底ebp,从而减少了ebp的使用,因此可以有效提升程序执行效率。

但每次访问都需要计算,如果在函数执行过程中esp发生了变化,再次访问变量就需要重新计算偏移了。

参考文献:《C 反汇编与逆向分析技术揭秘》

0 人点赞