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. swagger的作用和配置使用

    纯API项目中 引入swagger可以生成可视化的API接口页面     引入包 nuget包: Swashbuckle.AspNetCore(最新稳定版) 配置 1.配置Startup类Config ...

  2. 业务网关之AK中心建设

    啥是AK AK(Access Key)是一种身份证明,它解决了"资源的使用者是谁"这个问题,比如在生活中,身份证可以证明你是你,而在云计算或程序中,AK能证明你是这个应用的拥有者. ...

  3. 前端每日实战:134# 视频演示如何用 CSS 和 GSAP 创作一个树枝发芽的 loader

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/LJmpXZ 可交互视频 此视频是可 ...

  4. .Net Core:Docker无法拉取mcr.microsoft.com相关镜像解决办法

    今天在教同事Docker简单部署Asp.Net Core项目,pull镜像时突然出现下图中的错误: 因为微软在 2018 年五月之后,只会将相关镜像打包发布到 MCR 上.但是 MCR 对国内用户不太 ...

  5. 实验 3 Spark 和 Hadoop 的安装

      1.           安装 Hadoop 和 Spark 进入 Linux 系统,参照本教程官网"实验指南"栏目的"Hadoop 的安装和使用",完成 ...

  6. properties和XML配置文件内容的获取

    @ 目录 总结内容 1. Java中为什么要使用配置文件 2. Java中常用的配置文件类型有哪些以及它们的特点 Properties配置文件 XML配置文件 总结 总结内容 1. Java中为什么要 ...

  7. 静态变量和成员变量的区别、final修饰特点、创建对象的内存图、静态内存图

    静态变量和成员变量的区别* 静态变量也叫类变量  成员变量也叫对象变量* A:所属不同 * 静态变量属于类,所以也称为为类变量 * 成员变量属于对象,所以也称为实例变量(对象变量)* B:内存中位置不 ...

  8. VSCode使用Settings Sync同步配置和插件

    转载参考地址:https://www.cnblogs.com/zzhaolei/p/12028241.html 1.需求 自己平常工作,一般在公司用公司的电脑,在家里就是自己的,但是vscode如果配 ...

  9. spring总览

    Spring 概述 1. 什么是spring? Spring 是个java企业级应用的开源开发框架.Spring主要用来开发Java应用,但是有些扩展是针对构建J2EE平台的web应用.Spring  ...

  10. js验证邮箱格式

    function test() { var temp = document.getElementById("text1"); //对电子邮件的验证 var myreg = /^([ ...