你真的了解栈溢出么?

2023-11-13

记得之前看过一篇文章说,最好查的bug是语法错误,因为编译器会告诉你,最不好查的bug是栈溢出,因为啥,因为不仅编译器不会告诉你,连你自己有可能都找不到原因出在哪。

经过了一段时间的摸索,算是基本搞清楚了栈溢出的原理,写下来以防日后出现问题无从下手。

前言

开发过单片机的同学应该不陌生这个名词,一般我们也说堆栈,其实这里有两个意思:一般我们说堆栈其实指的就是帧本身,而说堆指的就是堆。这是两个不同的分区。便于理解给出一张典型的C语言在linux系统下的占区图:

这里写图片描述

可以看出,对于Linux系统下的,存储空间的分配有着较为层次清晰的分层。单片机大概也遵循这个分区架构。

二进制代码以及常量(CONST修饰)以及全局变量在最底层,存储空间最靠前的部分
然后是堆区,堆区向上增长,我们常用到的molloc()、free()等函数操作的就是这个区,这也是芯片系统中唯一可以让程序员通过代码操作的一片存储空间
再然后是动态链接库
在往上(高地址)便是栈区。 最高地址一般为操作系统内核,用户无法访问

了解了这个之后我们开始详解何为栈、栈为什么会溢出以及在代码级如何预防栈溢出,最后说一下栈溢出攻击的事情。

那么什么是栈呢

在计算机中,栈可以理解为一个特殊的容器,用户可以将数据依次放入栈中,然后再将数据按照相反的顺序从栈中取出。也就是说,先放入的数据最后才能取出,而最后放入的数据必须先取出。这称为先进后出(First In Last Out)原则。

放入数据常称为入栈或压栈(Push),取出数据常称为出栈或弹出(Pop)。

这里写图片描述

可以发现,栈底始终不动,出栈入栈只是在移动栈顶,当栈中没有数据时,栈顶和栈底重合。

这里需要注意标识栈顶和栈底的两个寄存器: ebp寄存器指向栈底,esp寄存器指向栈顶。从本质上来讲,栈是一段连续的内存,需要同时记录栈底和栈顶,才能对当前的栈进行定位。

栈溢出是怎么回事

了解了栈实际上也是一块内存后,栈溢出就好理解了。

当我们定义的数据所需要占用的内存超过了栈的大小时,就会发生栈溢出。编译器会报栈溢出错误。

如一块芯片的内存RAM大小为4k,当我们定义了一个大数组,如下:

int buf[1024*5] = {0};

很明显定义的数组超过了内存大小,这就导致了栈溢出。

预防栈溢出需要我们在编程时了解内存使用,尽可能不要定义特别大的数组,尽可能不要定义特别复杂的函数,如多个形参等。

函数调用栈

定义的数组会占用栈空间,同样,定义的函数也会占用栈空间,一个简单的例子便是函数的入栈和出栈。

举个例子:

void func(int a, int b)
{
    int p =12, q = 345;
}
int main()
{
    func(90, 26);
    return 0;
}

函数的进栈出栈过程如下图所示:
这里写图片描述

函数进栈

  1. main() 是主函数,也需要进栈,如步骤①所示。

  2. 在步骤②中,执行语句func(90, 26);,先将实参 90、26 压入栈中,再将返回地址压入栈中,这些工作都由 main() 函数(调用方)完成。这个时候 ebp 的值并没有变,仅仅是改变 esp 的指向。

  3. 到了步骤③,就开始执行 func() 的函数体了。首先将原来 ebp 寄存器的值压入栈中(也即图中的 old ebp),并将 esp 的值赋给 ebp,这样 ebp 就从 main() 函数的栈底指向了 func() 函数的栈底,完成了函数栈的切换。由于此时 esp 和ebp 的值相等,所以它们也就指向了同一个位置。

  4. 为局部变量、返回值等预留足够的内存,如步骤④所示。由于栈内存在函数调用之前就已经分配好了,所以这里并不是真的分配内存,而是将 esp 的值减去一个整数,例如 esp - 0XC0,就是预留 0XC0 字节的内存。

  5. 将 ebp、esi、edi 寄存器的值依次压入栈中。

  6. 将局部变量的值放入预留好的内存中。

至此,func() 函数的活动记录就构造完成了。可以发现,在函数的实际调用过程中,形参是不存在的,不会占用内存空间,内存中只有实参,而且是在执行函数体代码之前、由调用方压入栈中的。

未初始化的局部变量的值为什么是垃圾值

为局部变量分配内存时,仅仅是将 esp 的值减去一个整数,预留出足够的空白内存,不同的编译器在不同的模式下会对这片空白内存进行不同的处理,可能会初始化为一个固定的值,也可能不进行初始化。

函数出栈

步骤⑦到⑨是函数 func() 出栈过程:
7) 函数 func() 执行完成后开始出栈,首先将 edi、esi、ebx 寄存器的值出栈。

  1. 将局部变量、返回值等数据出栈时,直接将 ebp 的值赋给 esp,这样 ebp 和 esp 就指向了同一个位置。

  2. 接下来将 old ebp 出栈,并赋值给现在的 ebp,此时 ebp 就指向了 func() 调用之前的位置,即 main() 活动记录的 old ebp 位置,如步骤⑨所示。

这一步很关键,保证了还原到函数调用之前的情况,这也是每次调用函数时都必须将 old ebp 压入栈中的原因。

最后根据返回地址找到下一条指令的位置,并将返回地址和实参都出栈,此时 esp 就指向了 main() 活动记录的栈顶, 这意味着 func() 完全出栈了,栈被还原到了 func() 被调用之前的情况。

函数执行完局部变量的值真的不存在了?

经过上面的分析可以发现,函数出栈只是在增加 esp 寄存器的值,使它指向上一个数据,并没有销毁之前的数据。

栈上的数据只有在后续函数继续入栈时才能被覆盖掉,这就意味着,只要时机合适,在函数外部依然能够取得局部变量的值。请看下面的代码:

#include <stdio.h>
int *p;
void func(int m, int n)
{
    int a = 18, b = 100;
    p = &a;
}
int main()
{
    int n;
    func(10, 20);
    n = *p;
    printf("n = %d\n", n);
    return 0;
}

运行结果:

n = 18

在 func() 中,将局部变量 a 的地址赋给 p,在 main() 函数中调用 func(),函数刚刚调用结束,还没有其他函数入栈,局部变量 a 所在的内存没有被覆盖掉,所以通过语句n = *p;能够取得它的值。

参考网址:C语言中文网

v v v v v v

本文为作者原创,请勿转载,转载请联系作者。
点击下方卡片,关注我的公众号,有最新的文章和项目动态。

v v v v v v

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

你真的了解栈溢出么? 的相关文章

  • 微信小程序开发笔记一

    微信小程序开发笔记 一 微信小程序的结构 1 初识小程序 2 快捷键 3 查阅文档 二 常用组件 1 input组件 2 button组件 三 小程序中的函数 1 函数的两种定义方法 2 带参函数 3 js中的默认函数 4 其它常用函数 四
  • vue在原有的类名上,动态渲染添加新类名

    vue在原有的类名上 动态渲染添加新类名
  • Redis相关-03

    Redis相关 03 Redis配置文件详解 持久化 RDB操作 持久化 AOF操作 Redis订阅发布 Redis集群环境搭建 主从复制 宕机手动配置主机 哨兵模式 缓存穿透以及雪崩 一 Redis配置文件详解 1 单位说明 Note o

随机推荐

  • S7-1500系列博途中使用SCL语言编程方法简介

    SCL Structured Contorl Language 结构化控制语言 在TIA博途软件中 默认支持SCL语言 在建立程序块时可以直接选择SCL语言 SCL语言类似计算机高级语言 如果你有C Java C Python这种高级语言的
  • gradle 历史版本下载链接

    https gradle org releases
  • 阿里云解决外网不能访问

    开发十年 就只剩下这套Java开发体系了 gt gt gt 1 未配置该端口安全策略 配置如下后 所有ip都可以访问 全部端口都可以使用了 如果只需要特定ip或端口开放也可以进行设置 2 防火墙的原因 我写了关于centos开启防火墙和开放
  • C# 1. 介绍

    1 介绍 C 读作 See Sharp 是一种简洁 现代 面向对象且类型安全的编程语言 C 起源于 C 语言家族 因此 对于 C C 和 Java 程序员 可以很快熟悉这种新的语言 C 已经分别由 ECMA International 和
  • 启动VMware虚拟机时出现黑屏解决办法

    以管理员身份运行 命令提示符 gt 输入命令 netsh winsock reset gt 运行后重启电脑 gt Enjoy it 上述命令作用 重置winsock网络规范
  • linux etc下的profile和/etc/bashrc

    etc profile的设置方法对所有登录的用户都有效 bashrc只对当前用户有效 上面两个都是配置文件 开机后 系统会先读取 etc profile 再读 bashrc 不同的用户 bashrc文件可以有不同的设置 而 etc prof
  • GitHub使用--上传一个文件

    上传文件到GitHub需要用到两个软件 分别是GitHub TortoiseGit 创建步骤如下 1 选择文件夹 2 右键选择 代码仓库 3 如果上传的文件根目录是这个 就不勾选 反之勾选 4 确认 5 文件右击选择commit 6 填写M
  • 扩展阿里p3c实现自定义代码规范检查

    前段时间fastjson报出了漏洞 只要打开setAutoType特性就会存在风险 自己测试环境的一个项目被揪出来了 虽然改动很小 但就是觉得憋屈 fastjson还是挺好的 想着禁用的话太可惜 用的话又要注意安全 就想着找款工具提示下在用
  • Node.js基础——模块

    文章目录 在Vscode上使用node js运行js代码 法一 终端运行 法二 右键Run Code Vsode设置node代码提示 CommonJS规范 模块化规范 JS标准的缺陷 没有模块化系统带来的影响 CommonJS的模块化规范
  • Flutter运行在Android上卡Running Gradle task ‘assembleDebug...

    Flutter运行在Android上卡Running Gradle task assembleDebug 是因为无法访问官方源 下面进行换源 1 修改配置文件 buildscript repositories google mavenCen
  • 代码静态分析

    1 简介 静态测试包括代码检查 静态结构分析 代码质量度量等 它可以由人工进行 充分发挥人的逻辑思维优势 也可以借助软件工具自动进行 代码检查代码检查包括代码走查 桌面检查 代码审查等 主要检查代码和设计的一致性 代码对标准的遵循 可读性
  • 微信小程序中使用video组件

    文章目录 前情提要 搭建视频服务器 小程序项目 app json pages index index wxml pages index index wxss pages index index js 相关链接 前情提要 小程序里要放置视频
  • 算法:邮局选址问题

    一条直线上有N个居民点 需要建设K个邮局 邮局只能建在居民点上 则所有居民点到最近邮局到最短距离是 动态规划 时间O N N 核心思想 外层循环 邮局数量K 直到包括最大邮局 中层循环 区间 0 R 直到包括整个区间 内层循环 从 0 R
  • 2021-03-20

    Tensorflow 从本地导入数据集 离线使用数据集 1 keras datasets cifar10 load data 进入函数load data j将函数中的path 路径进行修改 dirname cifar 10 batches
  • VM vSphere 导出OVF文件,导入VM Workstation 无法正常引导进入系统

    VM vSphere 导出OVF文件 导入VM Workstation 无法正常引导进入系统的原因有很多种 本次进行修复的原因为 蓝屏错误 终止代码 KMODE EXCEPTION NOT HANDLED 1 首先排查是VM Worksta
  • vscode中使用emmet

    vscode内置emmet功能 可以用在html jsx css sass less等文件上 但是默认没有开启 在setting中添加 emmet triggerExpansionOnTab true 之后 在 html文件里输入div m
  • 本体构建——使用Protege利用Cellfie批量导入excel数据时常见错误及解决方法

    protege利用Cellfie导入数据 https blog csdn net mysky54 article details 108033813 在Tools Create axioms from Excel workbook 中打开文
  • 数字SOC设计之低功耗设计入门(三)——系统与架构级低功耗设计

    前面讲解了使用EDA工具 主要是power compiler 进行功耗分析的流程 这里我们将介绍在数字IC中进行低功耗设计的方法 同时也结合EDA工具 主要是Design Compiler 如何实现 我们的讲解的低功耗设计主要是自顶向下的设
  • JS&Java 交互 报错 JavaException was raised during method Invocation

    如上图所示当JS调用本地方法时报错 原因让人很容易忽略 在与JS交互的方法中不能直接操作View 需要放在主线程操作才能生效 如下代码所示 JavascriptInterface public void setTitle final Str
  • 你真的了解栈溢出么?

    记得之前看过一篇文章说 最好查的bug是语法错误 因为编译器会告诉你 最不好查的bug是栈溢出 因为啥 因为不仅编译器不会告诉你 连你自己有可能都找不到原因出在哪 经过了一段时间的摸索 算是基本搞清楚了栈溢出的原理 写下来以防日后出现问题无