这个是虚坏叔叔的主站。牢记域名: [xuhss.com](拼音首字母)。首页可以搜索或: 虚幻账号

C语言函数调用栈

🚀 优质资源分享 🚀

学习路线指引(点击解锁) 知识定位 人群定位
🧡 Python实战微信订餐小程序 🧡 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
💛Python量化交易实战💛 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

C语言函数调用栈

栈溢出(stack overflow)是最常见的二进制漏洞,在介绍栈溢出之前,我们首先需要了解函数调用栈。

函数调用栈是一块连续的用来保存函数运行状态的内存区域,调用函数(caller)和被调用函数(callee)根据调用关系堆叠起来。栈在内存区域中从高地址向低地址生长。 每个函数在栈上都有自己的栈帧,用来存放局部变量、函数参数等信息。当caller调用callee时,callee对应的栈帧就会被开辟,当调用结束返回caller时,callee对应的栈帧就会被销毁。

80c7e215679a25cc66b1a176ead322dc - C语言函数调用栈

下图展示了栈帧的结构。在32位程序中,寄存器ebp指向栈帧的底部,用来存储当前栈帧的基址,在函数运行过程中不变,可以用来索引函数参数和局部变量的位置。寄存器esp指向栈帧的顶部,当栈生长时,esp的值减少(向低地址生长)。寄存器eip用于存储下一条指令的地址。在64位程序中,三个寄存器分别为rbp、rsp和rip。

e88ac217ab796a7a9b9b8cbdf1220ba0 - C语言函数调用栈

当函数调用发生时,首先需要保存caller的状态,以便函数调用结束后进行恢复,然后创建callee的状态。具体来说:

  1. 如果是32位程序,将传给callee的参数按照逆序依次压入caller的栈帧中;如果是64位程序,将传给callee的参数按照逆序依次传入寄存器r9、r8、rcx、rdx、rsi、rdi,如果参数的个数超过了6个,将其余参数压入caller的栈帧中。如果callee不需要参数,则这一步骤省略。
  2. 将caller调用callee后的下一条指令的地址压入栈中,作为callee的返回地址,这样,当函数返回后可以正常执行接下来的指令。
  3. 将当前ebp寄存器的值压入栈中,这是caller栈帧的基址,将ebp更新为当前的esp。
  4. 将callee的局部变量压入栈中。
  5. 函数调用结束后,就是上面过程的逆过程,callee栈帧中数据会出栈,恢复到caller栈帧状态。

上面的第1步由caller完成,第2步在caller执行call指令时完成,第3、4步由callee完成。

下面看一个具体的例子,callerStack.c代码如下:

// callerStack.c
// C语言函数调用栈 

# include 

int func(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8)
{
    int loc1 = arg1 + 1;
    int loc2 = arg8 + 8;
    return loc1 + loc2;
}

int main(void)
{
    int ret = func(1, 2, 3, 4, 5, 6, 7, 8);
    return 0;
}

用命令gcc -m32 callerStack.c -o callerStack32生成32位程序,用gdb反汇编,得到的结果如下:

(这里额外说一下,如果是在64位机器上执行上述命令可能会报错: fatal error: bits/libc-header-start.h: No such file or directory #include ,需要安装multilib库:sudo apt install gcc-multilib

   0x565561dd  endbr32 
 0x565561e1  push ebp ; 将ebp入栈,保存caller的基址,esp -= 4
 0x565561e2  mov ebp, esp ; 将ebp更新为当前的esp
 0x565561e4  sub esp, 0x10 ; esp -= 0x10
 0x565561e7  call \_\_x86.get\_pc\_thunk.ax <\_\_x86.get\_pc\_thunk.ax> ; 没看懂

 0x565561ec  add eax, 0x2df0 ; 没看懂
 0x565561f1  push 8 ; 参数入栈,esp -= 4
 0x565561f3  push 7
 0x565561f5  push 6
 0x565561f7  push 5
 0x565561f9  push 4
 0x565561fb  push 3
 0x565561fd  push 2
 0x565561ff  push 1
 0x56556201  call func  ; 调用func,返回地址入栈

 0x56556206  add esp, 0x20 ; 恢复栈顶
 0x56556209  mov dword ptr [ebp - 4], eax ; eax存放func的返回值
 0x5655620c  mov eax, 0
 0x56556211  leave 
 0x56556212  ret 

 0x565561ad  endbr32 
 0x565561b1  push ebp ; 将ebp入栈,保存caller的基址,esp -= 4
 0x565561b2  mov ebp, esp ; ebp更新为当前的esp
 0x565561b4  sub esp, 0x10 ; esp -= 0x10
 0x565561b7  call \_\_x86.get\_pc\_thunk.ax <\_\_x86.get\_pc\_thunk.ax> ; 没看懂

 0x565561bc  add eax, 0x2e20  ; 没看懂
 0x565561c1  mov eax, dword ptr [ebp + 8] ; 取出arg1(值为1),放入eax中
 0x565561c4  add eax, 1 ; arg1 + 1
 0x565561c7  mov dword ptr [ebp - 8], eax ; 计算结果(局部变量loc1)放入栈中
 0x565561ca  mov eax, dword ptr [ebp + 0x24] ; 取出arg8(值为8),放入eax中
 0x565561cd  add eax, 8 ; arg8 + 8
 0x565561d0  mov dword ptr [ebp - 4], eax ; 计算结果(局部变量loc8)放入栈中
 0x565561d3  mov edx, dword ptr [ebp - 8]
 0x565561d6  mov eax, dword ptr [ebp - 4]
 0x565561d9  add eax, edx ; eax = eax (loc8) + edx (loc1),函数返回值存放在eax中
 0x565561db  leave ; mov esp, ebp pop ebp
 0x565561dc  ret ; pop eip

以上就是C语言函数的调用过程以及栈的情况,但是我还有几点疑问没有弄清楚,记录一下:

  1. 为什么在函数刚开始的地方sub esp, 0x10,从后面的代码来看,开辟的空间用于存放局部变量,那为什么不是在局部变量定义的时候将局部变量的值入栈,再移动esp呢?而是一次性先esp -= 0x10,这样不会带来空间的浪费吗?
  2. call __x86.get_pc_thunk.ax是什么意思?
  3. add eax, 0x2e20有什么作用?

参考资料

星盟安全团队课程:https://www.bilibili.com/video/BV1Uv411j7fr
CTF竞赛权威指南(Pwn篇)(杨超 编著,吴石 eee战队 审校,电子工业出版社)
https://blog.csdn.net/xuyaowen/p/libc-header-start.html

转载请注明:虚坏叔叔 » C语言函数调用栈

喜欢 (7)

您必须 登录 才能发表评论!