C语言函数调用栈
C语言函数调用栈
栈溢出(stack overflow)是最常见的二进制漏洞,在介绍栈溢出之前,我们首先需要了解函数调用栈。
函数调用栈是一块连续的用来保存函数运行状态的内存区域,调用函数(caller)和被调用函数(callee)根据调用关系堆叠起来。栈在内存区域中从高地址向低地址生长。 每个函数在栈上都有自己的栈帧,用来存放局部变量、函数参数等信息。当caller调用callee时,callee对应的栈帧就会被开辟,当调用结束返回caller时,callee对应的栈帧就会被销毁。

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

当函数调用发生时,首先需要保存caller的状态,以便函数调用结束后进行恢复,然后创建callee的状态。具体来说:
如果是32位程序,将传给callee的参数按照逆序依次压入caller的栈帧中;如果是64位程序,将传给callee的参数按照逆序依次传入寄存器r9、r8、rcx、rdx、rsi、rdi,如果参数的个数超过了6个,将其余参数压入caller的栈帧中。如果callee不需要参数,则这一步骤省略。
将caller调用callee后的下一条指令的地址压入栈中,作为callee的返回地址,这样,当函数返回后可以正常执行接下来的指令。
将当前ebp寄存器的值压入栈中,这是caller栈帧的基址,将ebp更新为当前的esp。
将callee的局部变量压入栈中。
函数调用结束后,就是上面过程的逆过程,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语言函数的调用过程以及栈的情况,但是我还有几点疑问没有弄清楚,记录一下:
为什么在函数刚开始的地方
sub esp, 0x10,从后面的代码来看,开辟的空间用于存放局部变量,那为什么不是在局部变量定义的时候将局部变量的值入栈,再移动esp呢?而是一次性先esp -= 0x10,这样不会带来空间的浪费吗?call __x86.get_pc_thunk.ax是什么意思?add eax, 0x2e20有什么作用?
参考资料
星盟安全团队课程:https://www.bilibili.com/video/BV1Uv411j7fr
CTF竞赛权威指南(Pwn篇)(杨超 编著,吴石 eee战队 审校,电子工业出版社)
https://www.cnblogs.com/xuyaowen/p/libc-header-start.html
C语言函数调用栈的更多相关文章
- C语言函数调用栈(一)
程序的执行过程可看作连续的函数调用.当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行.函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call sta ...
- C语言函数调用栈(二)
5 函数调用约定 创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数.主调函数必须精确存储这些参数,以便被调函数能够访问到它们.函数通过选择特定的调用约定,来表明其希望以特定方式接收参数.此外, ...
- C语言函数调用栈(三)
6 调用栈实例分析 本节通过代码实例分析函数调用过程中栈帧的布局.形成和消亡. 6.1 栈帧的布局 示例代码如下: //StackReg.c #include <stdio.h> //获取 ...
- go语言调度器源代码情景分析之四:函数调用栈
本文是<go调度器源代码情景分析>系列 第一章 预备知识的第3小节. 什么是栈 栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需 ...
- C语言函数调用时候内存中栈的动态变化详细分析(彩图)
版权声明:本文为博主原创文章,未经博主允许不得转载.欢迎联系我qq2488890051 https://blog.csdn.net/kangkanglhb88008/article/details/8 ...
- 从栈上理解 Go语言函数调用
转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/518 本文使用的go的源码 1.15.7 前言 函数调用类型 这篇文 ...
- 测试c语言函数调用性能因素之测试三
函数调用:即调用函数调用被调用函数,调用函数压栈,被调用函数执行,调用函数出栈,调用函数继续执行的一个看似简单的过程,系统底层却做了大量操作. 操作: 1, 调用函数帧指针 ...
- C语言数据结构----栈与递归
本节主要说程序中的栈函数栈的关系以及栈和递归算法的关系. 一.函数调用时的栈 1.程序调用时的栈是也就是平时所说的函数栈是数据结构的一种应用,函数调用栈一般是从搞地质向低地址增长的,栈顶为内存的低地址 ...
- C语言函数调用过程,汇编角度查看
C语言函数调用过程,汇编角度查看 把函数的参数按照调用约定压栈或者存储到寄存器中 调用要使用的函数,先把调用者的地址入栈,方便回来 跳转到函数 把函数使用到的一些寄存器压栈,避免修改寄存器的值 执行函 ...
随机推荐
- 创建POJO
创建一个名为student.java的POJO. 第一步,在包里面创建一个student类,右键点击包,点击新建,再点击创建一个类. 第二步,输入类名,点击完成. 第三步,输入数据类型和主键名,再写一 ...
- css创建叉和勾
a{ display: inline-block; width: 10px;height:5px; background: red;line-height: 0;font-size:0;vertica ...
- HTML5离线存储整理
前端html部分 //canvas.html <!DOCTYPE html> <html manifest="/test.appcache"> <he ...
- 前端每日实战:89# 视频演示如何用 CSS 和 D3 创作旋臂粒子动画
效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/xJrOqd 可交互视频 此视频是可 ...
- tf.test.is_gpu_available() 返回结果为False解决办法
安装完gpu版本的tensorflow,导入正常,但是tf.test.is_gpu_available()一直返回False,解决办法: 1.打开NVIDIA控制面板,查看CUDA的驱动版本,如果版本 ...
- 微信支付之微信H5支付(坑,ajax不支持重定向跳转)
这里讲的是 微信h5支付, 是微信以外的手机浏览器调用微信h5支付 h5支付: H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起 ...
- FastAPI(六十七)实战开发《在线课程学习系统》接口开发--用户登陆接口开发
接上一篇文章FastAPI(六十六)实战开发<在线课程学习系统>接口开发--用户注册接口开发.这次我们分享实际开发--用户登陆接口开发. 我们先来梳理下逻辑 1.查询用户是否存在2.校验密 ...
- python---十进制转换成n进制
""" 十进制转换成n进制 例子: 100转换成8进制-----144 256除8 商32 余0 32除8 商4 余0 4除8 商0 余4 每次结果的余数进栈, 最后出栈 ...
- Hash冲突以及解决
哈希函数:它把一个大范围的数字哈希(转化)成一个小范围的数字,这个小范围的数对应着数组的下标.使用哈希函数向数组插入数据后,这个数组就是哈希表. 冲突 当冲突产生时,一个方法是通过系统的方法找到数组的 ...
- Apache Zookeeper 使用-安装
Apache Zookeeper 使用-安装 官方网站 https://zookeeper.apache.org/ 官方文档 下载地址 Zookeeper 是什么? ZooKeeper is a ce ...