[C++逆向] 6 函数的工作原理
栈帧的形成和关闭
- 当栈顶指针小于栈底指针时就形成了栈帧 (esp < ebp)
- 需要注意的是,栈的增长方向是地址减小的方向
- 进入到新函数的时候,就会相应生成栈帧,当结束的时候,就会清除使用的栈空间(栈平衡)
- Debug版本中,在函数退出的时候,会检查esp是否等于ebp以此检查栈平衡。若不平衡则调用__chkesp() 弹出提示框。
- 实际进入函数过程
- 先保存原来的ebp,edi、esi寄存器到栈中
- 然后调整ebp的位置到esp。也即是调整新函数栈帧
- 接着通过sub esp,xxh 打开0x40大小的栈空间,是留给局部变量使用的
- 结束的时候 又会通过 add esp,xxh 释放栈空间
- 将原本的ebp地址从栈中弹出,恢复调用函数的栈帧
通过使用O2选项,就不会有检查栈平衡的代码,还可能没有保存环境、使用ebp保存当前栈底等一系列操作,代码会变得简洁高效。
各种调用方式的考察
因为函数调用会有不定参数的问题,如果参数是不定参数的时候,被调用函数不知道具体的参数数目,就需要调用他的函数平衡栈。但是正常情况下,被调用函数可以自己平衡栈。
三种调用约定
- _cdecl: C/C++默认调用方式,调用函数平衡栈,可使用不定参数
- _stdcall: 被调用函数平衡栈,所以不能使用不定参数
- _fastcall: 寄存器方式传参,被调用函数平衡栈,不能使用不定参数
所以,可以通过传参方式和平衡栈的方式来判断调用方式
_stdcall
-
退出函数时会通过 ret x; 的方式平衡栈顶,等价于 esp += x
-
有时不一定通过ret平衡,也可能通过pop等指令平衡,具体需要看代码怎么使用栈的
_cdecl
-
在被调用函数中不需要操作,调用函数的call指令下面add esp,x;来平衡栈
-
复写传播,_cdecl方式的函数在同一作用域多次使用的时候,,最后可以一起平衡栈
_fastcall
- 使用寄存器传参,但是寄存器智能使用edx和ecx多余的参数依旧需要栈传参
所以,实际传参效率 _fastcall > _cdecl > _stdcall
使用ebp或者esp寻址
在不是O2选项时,会使用ebp寻址局部变量
否则在O2选项中,为了节省寄存器,使用esp寻址
寻址的本质不过是对ebp或者esp做加减法操作,使得地址产生偏移,获取对应的值
因为IDA考虑到方便区分参数和局部变量,所以对于局部变量的寻址使用负数标号
因为调用函数的过程,是先将参数压栈,然后使用call指令,所以参数的偏移应该是正数。
函数调用过程
- 根据调用方式不同,参数压栈或者寄存器赋值
- 执行call指令,此时将call的下一条指令压栈
- 然后执行push ebp保存调用函数的栈底指针
- mov ebp,esp 则将调整出被调用函数的栈帧,此时ebp == esp,此时参数相对于ebp来说是高地址
- 一般来说,会通过sub esp,xxh 来分配局部变量的空间
- 在Debug模式下,会将他们通过rep stos 指令填充cccccccc也即是int 3中断防止这里的东西被执行
- 退出的时候,根据上面sub esp,xxh的值,给他add esp,xxh回去,就是释放局部变量
- 根据调用方式,_fastcall 和 _stdcall需要函数中平衡栈
- pop出调用函数的ebp还原调用函数的栈帧
- ret指令返回
- 若是_cdecl 调用,则通过add esp,xxh的方式平衡栈
某次调用函数时的栈结构
函数参数
使用push指令将数据压入栈中,而push实际上是把操作数复制到栈顶,所以此时压入栈的数据和原数据是不同的,相互独立。所以函数中修改参数,不会影响原来的数据。
不定长参数
C/C++将不定长参数的函数定义为:
- 至少有一个参数
- 所有不定长的参数类型传入是都是dword类型
- 需要在某一个参数中说明参数个数或者最后一个参数赋值为结尾标记
只要获得第一个参数的地址,那么只要对这个地址多加法就能获得其他参数。
获取参数类型是为了解释地址中的数据。
printf就是通过第一个参数获取参数总数的,字符串中几个%就是几个参数(%%)除外
函数的返回值
一般来说都是给eax赋值来传递返回值的
而如果是结构体,成员只有两个就使用eax和edx传递返回值。