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. IOS动态调试汇总-傻瓜版教程

    参考博客: https://juejin.cn/post/6872764160640450574#heading-4 (断点后续指令) https://www.jianshu.com/p/67f08a ...

  2. 《剑指offer》面试题2:实现Singleton 模式

    面试题2:实现Singleton 模式 题目:设计一个类,我们只能生成该类的一个实例.   只能生成一个实例的类是实现了Singleton (单例)模式的类型.由于设计模式在面向对象程序设计中起着举足 ...

  3. 1_开环系统和闭环系统_反馈控制_Open/Closed Loop System_Feedback

  4. 重磅:前端 MVVM 与 FRP 的升阶实践 —— ReRest 可视化编程

    ReRest (Reactive Resource State Transfer) 是前端开发领域新兴的方法论体系,它继承了 MVVM 与 FRP 编程理念,在技术上有不少创新.本文从专利稿修改而来, ...

  5. PAT B1066图像过滤

    输入样例: 3 5 100 150 0 3 189 254 101 119 150 233 151 99 100 88 123 149 0 255   输出样例: 003 189 254 000 00 ...

  6. Python实现简单用户注册信息管理系统

    运行效果: 注意:运行前请在同一目录下创建一个userdata.bin用于保存用户数据 源代码: 1 # coding:utf-8 2 ''' 3 用户注册信息管理系统 4 功能包括: 5 1.查看全 ...

  7. JS判断数组中的对象的每一个值不能为空

    方法一:使用every()函数,此函数不怎么常用,想要了解更多请自查 //表格 evaluateData为表格的数据 <el-table id="out-table3" :d ...

  8. sequelize模型

    model表示数据库中的表,有时它也会被称为"模型"或"工厂".Model不能通过构造函数创建,只能通过sequlize.define方法来定义或通过sequl ...

  9. 【零碎小bug系列】windows下的回车和换行符,cmd(telnet)上输出不左对齐

    cmd(telnet)上输出不左对齐,而是有莫名其妙的空格 目录 cmd(telnet)上输出不左对齐,而是有莫名其妙的空格 背景 解决 细究 背景 在cmd上使用telnet连接本地端口的服务器时, ...

  10. 不同标准下的C语言常量范围的默认类型的检测 (测试样例为C90与C99)

    不同标准下的C语言常量范围的默认类型的检测 一.C90与C99标准下的不同常量范围的默认类型     C90标准下对不同常量范围默认类型的检测实现及运行结果: C99标准下对不同范围默认类型的检测实现 ...