栈破坏下crash的分析方法

2023-11-08

在众多的coredump中,有一类crash调试起来是最麻烦的,那就是“栈被破坏”导致的函数调用回溯结构破坏引发的coredump。本文,主要讲讲这一类crash的成因、原理以及调试方法。

1. SMTC(show me the code)

首先,让我们来看一段代码

#include <stdio.h>
#include <string.h>
void fun(int n)
{
    printf("The %d step begin.\n", n);
    int a[10];
    for (int i = 0; i< n; i++) {
        a[i] = i;
    }
    if (n < 20) {
        fun(n +1);
    }
    printf("The %d step end\n", n);
}
int main(void)
{
	fun(8);
	return 0;
}

这段代码的关键在于fun函数会有递归调用,而在参数大于10的时候会导致写入的空间超过了栈上的“合法”内存。我们先来看下这段代码的输出

The 8 step begin.
The 9 step begin.
The 10 step begin.
The 11 step begin.
The 12 step begin.
The 13 step begin.
The 14 step begin.
The 15 step begin.
The 16 step begin.
The 17 step begin.
The 18 step begin.
The 19 step begin.
The 20 step begin.
The 20 step end
Segmentation fault (core dumped)

对于输出,我们做简要的解释:

  1. 所有的栈上地址都是合法(此处的合法,指的是操作系统允许写入)的,也就是说即使写入到a[19]这样的非预期地址,也不会crash
  2. 由于栈的增长方向是高地址向低地址,而“写坏”的是“高地址”,也就是说已经申请过的地址。
  3. 栈上与函数调用最相关的数据信息是rip&&esp,而直接导致crash的原因是因为rip被“写坏”,导致执行对应的指令出现问题。

2.原因详细解释

首先,我们来回顾一下函数调用过程中的stack数据分布与变化(link)。接着看上文的程序输出就明白:在fun函数调用的末尾,需要执行ret指令,也就是说会将地址为rbp+8对应的栈上数据放入rip寄存器中,然后执行rip对应的这一条这令,这里现在是存放的是一个0~19的数字,不是一个合法的指令地址,于是产生了crash。

stack func call list

也就是说,正常情况下,ebp数据结合栈上的数据实际上构成了一个单向链表,链表头是当前执行的函数,往链表尾部,是对应在各个层次的调用者函数。stack上对应的函数调用链表如上图。看到这里我们可以得出结论:

1.stack crash 的本质是rbp-rip的数据错乱导致。而具体crash的位置,取决于rbprip对应的数据。另一方面,栈上的数据错乱也不一定导致crash,有可能仅仅是把应该写入变量a的数据写到了变量b。
2.stack crash时,函数的执行已经脱离了出问题的函数。也就是说,A调用B,B函数中产生了栈上空间的错误写入,但是crash往往发生在A函数之中,因为只有B函数对应的汇编代码的最后一句retq执行完毕之后,才会发生crash,此时,程序的控制权在函数A之中。
3.stack crash时,函数调用栈已经被破坏。但是被破坏的是调用栈的头部。这也是唯一值得欣慰的信息了,函数调用栈尾部的信息依然完好无损。而我们可以据此,推测出函数调用的蛛丝马迹。

3.手动恢复函数调用栈

需要指出的是,被破坏的函数调用栈部分已经无法得到恢复了。此处我们能恢复的,仅仅是没有被破坏的部分。恢复函数栈的原理也很简单,那就是根据栈空间中的内存内容,找到那个“链表”即可。

继续使用上文我们对应的coredump文件,我们可以看到,由于函数调用最近的RBP对应的栈上内容已经被破坏,此时我们已经无法用bt指令得到正确的函数栈了。

Missing separate debuginfos, use: debuginfo-install glibc-2.17-105.el7.x86_64
(gdb) bt
#0  0x0000000f0000000e in ?? ()
#1  0x0000001100000010 in ?? ()
#2  0x0000001300000012 in ?? ()
#3  0x0000000100000000 in ?? ()
#4  0x0000000300000002 in ?? ()
#5  0x0000000500000004 in ?? ()

我们知道,函数调用栈在回溯过程中会执行两条关键的指令move %rbp %rsp; pop %rbp。而回溯行为对应的retq指令是在这两条指令之后执行的,此时rsp的值仍然是有效的。所以我们可以根据ESP的值打印出目前栈空间的数据。具体命令x/256xg 0x7ffd79ef9e40(rsp对应的值)和结果如下

(gdb) info reg rsp
rsp            0x7ffd79ef9e40	0x7ffd79ef9e40
(gdb) x/256xg 0x7ffd79ef9e40
0x7ffd79ef9e40:	0x0000001100000010	0x0000001300000012
0x7ffd79ef9e50:	0x0000000100000000	0x0000000300000002
0x7ffd79ef9e60:	0x0000000500000004	0x0000000700000006
0x7ffd79ef9e70:	0x0000000900000008	0x000000130000000a
......
0x7ffd79efa070:	0x00007fbe1d989d58	0x0000000c00000005
0x7ffd79efa080:	0x0000000100000000	0x0000000300000002
0x7ffd79efa090:	0x0000000500000004	0x0000000700000006
0x7ffd79efa0a0:	0x0000000900000008	0x0000000c0000000a
0x7ffd79efa0b0:	0x00007ffd79efa100	0x0000000000400583
0x7ffd79efa0c0:	0x00007fbe1d989d58	0x0000000b00000005
0x7ffd79efa0d0:	0x0000000100000000	0x0000000300000002
0x7ffd79efa0e0:	0x0000000500000004	0x0000000700000006
0x7ffd79efa0f0:	0x0000000900000008	0x0000000b0000000a
0x7ffd79efa100:	0x00007ffd79efa150	0x0000000000400583
0x7ffd79efa110:	0x00007fbe1dcfce80	0x0000000a00000000
0x7ffd79efa120:	0x0000000100000000	0x0000000300000002
0x7ffd79efa130:	0x0000000500000004	0x0000000700000006
0x7ffd79efa140:	0x0000000900000008	0x0000000a0040032a
0x7ffd79efa150:	0x00007ffd79efa1a0	0x0000000000400583

这里,函数调用关系比较长,我们以栈开始部分的数据来说明。使用x/256xg 0x7ffd79ef9e40+0x100获取栈跳过rsp开始被写坏的部分数据,得到如下rbp对应的list

broken_stack

好了,此时,我们已经找到了这个list,那么如果通过这个list找到函数调用关系呢?

通过rbpList恢复函数调用关系

通过《从汇编语言看函数调用》这篇文章,我们已经知道,栈上和rbp相邻的位置,就是对应的rip的值。而知道了rip的值,就能知道对应的代码位置。具体操作如下。

通过上图,我们根据这个list得到对应的rip-list对应的地址(rbp + 8对应的内容)依次全部为 0x0000000000400583 >> 0x0000000000400583 > ... > 0x00000000004005a7, 如下图:

ripList

有了rip地址,接下来只需要找出该地址对应的代码位置即可。这里,我们可以使用addr2line工具来分析代码段地址对应的源代码位置,结果如下。

[ykhuang@ykhuang-temp ~]$ addr2line -e test 0x0000000000400583
/home/ykhuang/test.cpp:13
[ykhuang@ykhuang-temp ~]$ addr2line -e test 0x00000000004005a7
/home/ykhuang/test.cpp:19

我们可以看到,这两个地址分别对应源代码的13和19行。 这里分别对应的printfreturn 0对应的位置。实际上出问题的位置发生在这两行代码的上一行,因为rip对应的意义是下一条指令的地址. 至此,我们已经得到了部分函数调用关系。实际debug的过程中,这也几乎是我们能从一个crash的堆栈上能够获取的全部信息了。有了这部分信息,可以让我们迅速定位问题。当然,结合实际的代码,我们可以从stack中靠近rsp被写坏的数据是什么,来反推和代码的对应关系。

4.总结

“栈破坏”导致的crash虽然难以排查,但是我们还是能根据栈上仅存的信息,尽可能缩小“问题”代码所在的位置。这其中的原来就是函数调用过程中函数栈的建立和销毁过程。当然,除此之外,你需要熟悉一些基本的gdb指令(查看内存、反汇编、查看对应寄存器的值等),也需要了解一些汇编指令的实际含义。其实,对于这种crash,还有另外的方式能够保存函数调用栈,我们以后再展开讨论。在实际的生产中,由于crash文件比较大,对crash现场的保存往往采用保存函数调用堆栈的方式。但是这种情况下,函数堆栈是无意义的,所以保存一些栈上数据,有利于我们更快定位问题,毕竟stack空间本来就不大。

博客源地址:栈破坏下crash的分析方法
更多相关内容:优孚-探索编程与技术的本源

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

栈破坏下crash的分析方法 的相关文章

  • 编译时运算符

    有人可以列出 C 中可用的所有编译时运算符吗 C 中有两个运算符 无论操作数如何 它们的结果始终可以在编译时确定 它们是sizeof 1 and 2 当然 其他运算符的许多特殊用途可以在编译时解决 例如标准中列出的那些整数常量表达式 1 与
  • 通过 CMIS (dotCMIS) 连接到 SP2010:异常未经授权

    我正在使用 dotCMIS 并且想要简单连接到我的 SP2010 服务器 我尝试用 C 来做到这一点 如下所示http chemistry apache org dotnet getting started with dotcmis htm
  • 动态加载程序集的应用程序配置

    我正在尝试将模块动态加载到我的应用程序中 但我想为每个模块指定单独的 app config 文件 假设我的主应用程序有以下 app config 设置
  • 查找c中结构元素的偏移量

    struct a struct b int i float j x struct c int k float l y z 谁能解释一下如何找到偏移量int k这样我们就可以找到地址int i Use offsetof 找到从开始处的偏移量z
  • Asp.NET WebApi 中类似文件名称的路由

    是否可以在 ASP NET Web API 路由配置中添加一条路由 以允许处理看起来有点像文件名的 URL 我尝试添加以下条目WebApiConfig Register 但这不起作用 使用 URIapi foo 0de7ebfa 3a55
  • BitTorrent 追踪器宣布问题

    我花了一点业余时间编写 BitTorrent 客户端 主要是出于好奇 但部分是出于提高我的 C 技能的愿望 我一直在使用理论维基 http wiki theory org BitTorrentSpecification作为我的向导 我已经建
  • C# 中通过 Process.Kill() 终止的进程的退出代码

    如果在我的 C 应用程序中 我正在创建一个可以正常终止或开始行为异常的子进程 在这种情况下 我通过调用 Process Kill 来终止它 但是 我想知道该进程是否已退出通常情况下 我知道我可以获得终止进程的错误代码 但是正常的退出代码是什
  • C#中如何移动PictureBox?

    我已经使用此代码来移动图片框pictureBox MouseMove event pictureBox Location new System Drawing Point e Location 但是当我尝试执行时 图片框闪烁并且无法识别确切
  • 创建链表而不将节点声明为指针

    我已经在谷歌和一些教科书上搜索了很长一段时间 我似乎无法理解为什么在构建链表时 节点需要是指针 例如 如果我有一个节点定义为 typedef struct Node int value struct Node next Node 为什么为了
  • 重载<<的返回值

    include
  • 如何设计以 char* 指针作为类成员变量的类?

    首先我想介绍一下我的情况 我写了一些类 将 char 指针作为私有类成员 而且这个项目有 GUI 所以当单击按钮时 某些函数可能会执行多次 这些类是设计的单班在项目中 但是其中的某些函数可以执行多次 然后我发现我的项目存在内存泄漏 所以我想
  • SolrNet连接说明

    为什么 SolrNet 连接的容器保持静态 这是一个非常大的错误 因为当我们在应用程序中向应用程序发送异步请求时 SolrNet 会表现异常 在 SolrNet 中如何避免这个问题 class P static void M string
  • 转发声明和包含

    在使用库时 无论是我自己的还是外部的 都有很多带有前向声明的类 根据情况 相同的类也包含在内 当我使用某个类时 我需要知道该类使用的某些对象是前向声明的还是 include d 原因是我想知道是否应该包含两个标题还是只包含一个标题 现在我知
  • 如何在 C 中调用采用匿名结构的函数?

    如何在 C 中调用采用匿名结构的函数 比如这个函数 void func struct int x p printf i n p x 当提供原型的函数声明在范围内时 调用该函数的参数必须具有与原型中声明的类型兼容的类型 其中 兼容 具有标准定
  • 如何序列化/反序列化自定义数据集

    我有一个 winforms 应用程序 它使用强类型的自定义数据集来保存数据进行处理 它由数据库中的数据填充 我有一个用户控件 它接受任何自定义数据集并在数据网格中显示内容 这用于测试和调试 为了使控件可重用 我将自定义数据集视为普通的 Sy
  • 如何查看网络连接状态是否发生变化?

    我正在编写一个应用程序 用于检查计算机是否连接到某个特定网络 并为我们的用户带来一些魔力 该应用程序将在后台运行并执行检查是否用户请求 托盘中的菜单 我还希望应用程序能够自动检查用户是否从有线更改为无线 或者断开连接并连接到新网络 并执行魔
  • 测试用例执行完成后,无论是否通过,如何将测试用例结果保存在变量中?

    我正在使用 NUNIT 在 Visual Studio 中使用 Selenium WebDriver 测试用例的代码是 我想在执行测试用例后立即在变量中记录测试用例通过或失败的情况 我怎样才能实现这一点 NUnit 假设您使用 NUnit
  • C# 模拟VolumeMute按下

    我得到以下代码来模拟音量静音按键 DllImport coredll dll SetLastError true static extern void keybd event byte bVk byte bScan int dwFlags
  • IEnumreable 动态和 lambda

    我想在 a 上使用 lambda 表达式IEnumerable
  • 如何防止用户控件表单在 C# 中处理键盘输入(箭头键)

    我的用户控件包含其他可以选择的控件 我想实现使用箭头键导航子控件的方法 问题是家长控制拦截箭头键并使用它来滚动其视图什么是我想避免的事情 我想自己解决控制内容的导航问题 我如何控制由箭头键引起的标准行为 提前致谢 MTH 这通常是通过重写

随机推荐

  • docker部署jenkins-slave分布式节点

    docker 运行jenkins slave示例 使用jnlp方式进行连接 即是agent主动连接master docker run jenkins jnlp slave url http jenkins server port workD
  • 计算机组成原理fc和fz,合肥工业大学计算机组成原理实验报告(DOC)

    合肥工业大学计算机组成原理实验报告 DOC 合肥工业大学计算机组成原理 合肥工业大学计算机组成原理试卷 计算机组成原理实验pdf 计算机组成原理微程序 计算机组成原理实验报告 计算机组成原理知识点 计算机组成原理试卷 计算机组成原理 pdf
  • apache出现You don't have permission to access / on this server. 提示

    看看是不是DocumentRoot的值改过了 如果是的话还要再看
  • 【单目标优化算法】孔雀优化算法(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码及详细文章 1 概述 受孔雀群智能行为的启发 POA的设计
  • 数据科学中的数据库简介

    推荐 使用 NSDT场景编辑器 快速搭建3D应用场景 用于高效视频 AI 和图形的通用加速器 数据科学中的数据库简介 数据科学涉及从大量数据中提取价值和见解 以推动业务决策 它还涉及使用历史数据构建预测模型 数据库有助于对如此大量的数据进行
  • 云原生之使用Docker部署wordpress网站

    云原生之使用Docker部署wordpress网站 一 wordpress介绍 二 检查本地docker环境 1 检查docker状态 2 检查docker版本 三 下载wordpress镜像 四 创建数据库 1 创建数据目录 2 创建my
  • 免费AWS EC2实例

    免费日期 2022年12 月 31日前 免费类型 Amazon Linux 2 AMI HVM SSD Volume Type 64 位 ARM 只支持ARM t4g micro 其他平台可用 例如 Ubuntu 18 04 或更新版本 R
  • FreeRTOS临界区

    FreeRTOS临界区是指那些必须完整运行 不能被打断的代码段 比如有的外设的初始化需要严格的时序 初始化过程中不能被打断 FreeRTOS 在进入临界区代码的时候需要关闭中断 当处理完临界区代码以后再打开中断 FreeRTOS 系统本身就
  • 引入springcloud报错。common依赖找不到_引用fabric-sdk和fabric报错 go mod 调试记录

    1 背景介绍 在fabric中 我们将proto定义文件放到fabric protos common路径下 在fabric sdk go中 我们引入的是fabric protos go包 当同时引入的时候 会将相同名称的proto对象注册
  • Swagger怎么做免鉴权

    前言 Swagger在API文档生成及测试方面非常方便 但是很多的API调用都需要用到token验证 然后经过Gateway网关 鉴权验证通过之后访问业务系统 为了方便后端开发自测接口 我们可以免去鉴权吗 答案是可以的 一般鉴权方式 我们先
  • 页面性能优化,如何减少回流

    在开发时 不可避免的会遇到性能优化的问题 怎么做性能才会更好 说到页面性能优化 我们就谈谈两个概念重绘和回流 1 什么是重绘 什么是回流 重绘 当渲染树中的一些元素需要更新属性 而这些属性只是影响元素的外观 风格 而不会影响布局的操作 比如
  • windows搭建WEB打印机

    文章目录 Web Print 添加一台虚拟打印机 名称为 CS Print 发布到AD域 客户端们都能够通过访问 https print www chinaskills com 查看打印机 证书由CSK2021 ROOTCA进行签署颁发 1
  • skywalking和jpa冲突

    1 报错 org springframework security authentication InternalAuthenticationServiceException No MethodInvocation found Check
  • 【C语言】通讯录的动态存储版本

    目录 一 前言 二 为什么要动态存储 1 动态存储的作用 2动态与静态存储的区别 三 动态存储的实现 1 通讯录容量 2 初始化通讯录 3 增加 减少通讯录成员 增加通讯录成员 判断及实现扩容函数的实现 减少通讯录成员 判断及实现减容函数的
  • iOS开发常见错误代码对照表---真机调试常见错误及解决方案

    iOS真机调试常见错误及解决方案 地址https developer apple com library ios technotes tn2250 index html apple ref doc uid DTS40009933 CH1 T
  • 11下滑半个屏幕_看完小米11发布会,2万粉购买小米,雷军给苹果的致命一击

    2020年12月28日 小米11发布会正式召开 这次的小米很聪明很聪慧 让我们看到了对用户满满的诚意 首先就是跑分 对于大部分用户而言 手机的跑分就代表着手机的性能 而小米搭配的高通骁龙888芯片 就让我们有了全新的认知 最终Antutu综
  • 依赖注入的几种方式

    获取bean对象 也称为对象装配 对象注入 依赖注入 对象装配的实现方法有3种 1 属性注入 2 构造方法注入 3 Setter注入 再讲本节内容之前 我们先来提两个传参的方式 首先呢 上节的文章里边 我们提到了五大类注解和 Bean注解
  • STM32F407ZGTE6利用模拟PWM驱动42步进电机(与pwm驱动led闪烁一样)

    前言 lmf老师来帮我解决42步进电机 预转不转 的问题 利用引脚模拟pwm波形驱动42步进电机 成功找到原因 还顺便给我留下了另一种驱动思路 直接引脚驱动 解决问题 42电机原地不动的原因是 线接触不良 采用杜邦线拼接 拟解决方案 重新换
  • C++程序设计期末考试抱佛脚

    大一上的 今早的计算机概论压中一道大题 我惊呆了 先放 点我看学友的复习总结 if嵌套配对 书p45 内嵌平衡语句 if if else else if else else总是与写在它前面的 最靠近的 尚未与其他else配对的if配对 其他
  • 栈破坏下crash的分析方法

    在众多的coredump中 有一类crash调试起来是最麻烦的 那就是 栈被破坏 导致的函数调用回溯结构破坏引发的coredump 本文 主要讲讲这一类crash的成因 原理以及调试方法 1 SMTC show me the code 首先