C语言函数调用栈

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

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

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

当函数调用发生时,首先需要保存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 <stdio.h> 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 <bits/libc-header-start.h>,需要安装multilib库:sudo apt install gcc-multilib

   0x565561dd <main>       endbr32
0x565561e1 <main+4> push ebp ; 将ebp入栈,保存caller的基址,esp -= 4
0x565561e2 <main+5> mov ebp, esp ; 将ebp更新为当前的esp
0x565561e4 <main+7> sub esp, 0x10 ; esp -= 0x10
0x565561e7 <main+10> call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax> ; 没看懂 0x565561ec <main+15> add eax, 0x2df0 ; 没看懂
0x565561f1 <main+20> push 8 ; 参数入栈,esp -= 4
0x565561f3 <main+22> push 7
0x565561f5 <main+24> push 6
0x565561f7 <main+26> push 5
0x565561f9 <main+28> push 4
0x565561fb <main+30> push 3
0x565561fd <main+32> push 2
0x565561ff <main+34> push 1
0x56556201 <main+36> call func <func> ; 调用func,返回地址入栈 0x56556206 <main+41> add esp, 0x20 ; 恢复栈顶
0x56556209 <main+44> mov dword ptr [ebp - 4], eax ; eax存放func的返回值
0x5655620c <main+47> mov eax, 0
0x56556211 <main+52> leave
0x56556212 <main+53> ret 0x565561ad <func> endbr32
0x565561b1 <func+4> push ebp ; 将ebp入栈,保存caller的基址,esp -= 4
0x565561b2 <func+5> mov ebp, esp ; ebp更新为当前的esp
0x565561b4 <func+7> sub esp, 0x10 ; esp -= 0x10
0x565561b7 <func+10> call __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax> ; 没看懂 0x565561bc <func+15> add eax, 0x2e20 <func+15> ; 没看懂
0x565561c1 <func+20> mov eax, dword ptr [ebp + 8] ; 取出arg1(值为1),放入eax中
0x565561c4 <func+23> add eax, 1 ; arg1 + 1
0x565561c7 <func+26> mov dword ptr [ebp - 8], eax ; 计算结果(局部变量loc1)放入栈中
0x565561ca <func+29> mov eax, dword ptr [ebp + 0x24] ; 取出arg8(值为8),放入eax中
0x565561cd <func+32> add eax, 8 ; arg8 + 8
0x565561d0 <func+35> mov dword ptr [ebp - 4], eax ; 计算结果(局部变量loc8)放入栈中
0x565561d3 <func+38> mov edx, dword ptr [ebp - 8]
0x565561d6 <func+41> mov eax, dword ptr [ebp - 4]
0x565561d9 <func+44> add eax, edx ; eax = eax (loc8) + edx (loc1),函数返回值存放在eax中
0x565561db <func+46> leave ; mov esp, ebp pop ebp
0x565561dc <func+47> 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://www.cnblogs.com/xuyaowen/p/libc-header-start.html

C语言函数调用栈的更多相关文章

  1. C语言函数调用栈(一)

    程序的执行过程可看作连续的函数调用.当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行.函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call sta ...

  2. C语言函数调用栈(二)

    5 函数调用约定 创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数.主调函数必须精确存储这些参数,以便被调函数能够访问到它们.函数通过选择特定的调用约定,来表明其希望以特定方式接收参数.此外, ...

  3. C语言函数调用栈(三)

    6 调用栈实例分析 本节通过代码实例分析函数调用过程中栈帧的布局.形成和消亡. 6.1 栈帧的布局 示例代码如下: //StackReg.c #include <stdio.h> //获取 ...

  4. go语言调度器源代码情景分析之四:函数调用栈

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第3小节. 什么是栈 栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需 ...

  5. C语言函数调用时候内存中栈的动态变化详细分析(彩图)

    版权声明:本文为博主原创文章,未经博主允许不得转载.欢迎联系我qq2488890051 https://blog.csdn.net/kangkanglhb88008/article/details/8 ...

  6. 从栈上理解 Go语言函数调用

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/518 本文使用的go的源码 1.15.7 前言 函数调用类型 这篇文 ...

  7. 测试c语言函数调用性能因素之测试三

    函数调用:即调用函数调用被调用函数,调用函数压栈,被调用函数执行,调用函数出栈,调用函数继续执行的一个看似简单的过程,系统底层却做了大量操作. 操作: 1,               调用函数帧指针 ...

  8. C语言数据结构----栈与递归

    本节主要说程序中的栈函数栈的关系以及栈和递归算法的关系. 一.函数调用时的栈 1.程序调用时的栈是也就是平时所说的函数栈是数据结构的一种应用,函数调用栈一般是从搞地质向低地址增长的,栈顶为内存的低地址 ...

  9. C语言函数调用过程,汇编角度查看

    C语言函数调用过程,汇编角度查看 把函数的参数按照调用约定压栈或者存储到寄存器中 调用要使用的函数,先把调用者的地址入栈,方便回来 跳转到函数 把函数使用到的一些寄存器压栈,避免修改寄存器的值 执行函 ...

随机推荐

  1. Oracle 数据库备份实战

    最近公司的客户希望使用oracle数据库,所以我们只好将数据从mysql数据库迁移到oracle数据库,并对oracle数据库制定了一个备份策略,之前虽然对oracle很熟悉,但做备份策略还是第一次, ...

  2. css浮动的"巨坑"与完美解决办法

    浮动 1 浮动概念 如果想实现网页中排版布局,比如一行内显示对应的标签元素,可以使用浮动属性.浮动可以实现元素并排. 块转行内日块也可以实现一行显示,不过存在空白折叠现象 float 浮动 属性值 描 ...

  3. TCP 重传、滑动窗⼝、流量控制、拥塞控制

    重传机制 TCP 会在以下两种情况发⽣超时重传: 数据包丢失 确认应答丢失 重传超时 重传超时是TCP协议保证数据可靠性的另一个重要机制,其原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果 ...

  4. 设置python 虚拟环境 virtualenv django 虚拟环境

    https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/development_environment Ubuntu vir ...

  5. 4.4 ROS节点名称重名

    4.4 ROS节点名称重名 场景:ROS 中创建的节点是有名称的,C++初始化节点时通过API:ros::init(argc,argv,"xxxx");来定义节点名称,在Pytho ...

  6. 如何监控微信小程序HTTP请求错误

    摘要: Fundebug的微信小程序错误监控插件更新至0.5.0,支持监控HTTP请求错误. 接入插件 接入Fundebug的错误监控插件非常简单,只需要下载fundebug.0.5.0.min.js ...

  7. js压缩图片到2m以下

    用的canvas.这个问题测试妹子反馈了好几次bug,解决了好多次,虽然用了比较僵硬的办法,但总算最终解决了. 因为php的同事说,页面上的图片要直接调用七牛的接口上传到七牛,所以后端那边不能处理,必 ...

  8. 现在做 Web 全景合适吗?

    Web 全景在以前带宽有限的条件下常常用来作为街景和 360° 全景图片的查看.它可以给用户一种 self-immersive 的体验,通过简单的操作,自由的查看周围的物体.随着一些运营商推出大王卡等 ...

  9. WebGL小姐姐教我学画画之起手式

    初次接触WebGL,如有错误之处欢迎留言,共同学习进步. v WebGL的自画像 我,WebGL,全名Web Graphics Library,是为了让死宅程序猿们(摊手)能在浏览器上为所欲为的画女朋 ...

  10. css使div居中

    每次想要使div居中都会设置position:absolute;,发现设置其他控件位置时会出现问题,所以采用以下办法: margin:0 auto;