《Linux栈破坏了,如何还原》
【栈破坏导读】栈破坏有了解过吗?何为栈破坏,栈破坏了,程序会立刻引发崩溃,我们通过gdb去调试coredump,栈被破坏的栈帧是没法被恢复的,这也给我们调试程序带来很大的困难,那如何还原栈破坏的第一现场?本文将为你详细解答。
何为栈破坏,我写个小程序给各位看下:
int function(int a) {unsigned long long int *p = (unsigned long long int *)&a;p[0]=p[1]=p[2] = 0xffffffffffffffff;return 0;}int main() {int a = 10;return function(a);}
在function中,我们定义了个unsigned long long int类型的指针指向int类型变量a的地址,因为int类型a占用的栈内存大小为4字节,unsigned long long int类型的指针步长就是8字节,连续给4字节的栈内存赋值3个大小为8字节的整形变量,肯定破坏了cpu为function分配的栈空间,甚至可能把上一层的栈帧给写坏了。那么程序崩溃时,就算我们有coredump,gdb也是没法把function的栈帧给还原回来。 那针对这种情况,我们从栈帧切换的原理去反推函数调用的栈帧。
Linux系统读取磁盘中二进制程序的elf文件,通过内存映射的方式,为当前二进制程序分配一段独立的虚拟地址空间,有了独立的虚拟地址空间,当前二进制程序便以独立进程的方式运行起来了。那进程的虚拟地址空间布局是怎样的?

重点关注的共享内存和当前进程加载的动态库在虚拟地址空间的位置,比栈顶指针rsp的地址要小,比堆的最大值要大。因为栈空间是由高地址到低地址生长的,堆空间是由低地址到高地址扩展的。
好,在介绍推栈之前,我们必须要熟悉函数调用中涉及到的栈帧切换的流程,且看如下的小程序:
int function1(int i){function2(++i);return 0;}int main(){return function1(0);}
main函数调用function1,那栈帧是如何从main切换到funtion1的,栈顶指针rsp、栈底指针ebp是如何切换的,函数调用返回时,rip是如何帮助当前栈帧返回到上一层栈帧的?我们用gdb调试程序,并在function1设置断点。在调用function1之前,我们看下寄存器rsp、rbp、rip这些是怎样的?

可以很清晰看到当前rbp、rsp在同一个位置,因为main函数的栈空间并没有定义任何栈变量,所以栈顶和栈底的地址都在同一个位置,rip指向的指令地址是0x400606,这条指令依然在main+4位置。那么执行disassemble查看当前程序的汇编指令,看看0x400606表示是哪条语句。

可以看到0x400606表示将0赋值给edi寄存器。这个暂且不管,继续执行si指令,跳转到0x000000000040060b地址,也即将调用函数function1。

调用完函数function1,0x0000000000400610指向的便是function1执行完之后要执行的指令地址,也是eip需要存储的地址,继续执行si指令,跳转到function1中,查看寄存器rsp、rbp的值,可以很清晰地看到rsp减少了8字节,也就是栈空间往下扩张了。rip指向了指令地址值0x4005e2(function1)。

那返回上一层栈帧(main)的指令地址0x0000000000400610去哪里了?我们执行下x/64xg $rsp(把当前栈帧往调用者方向溯源64*8个字节的内容给打出来)。

可以看到上一层栈帧(main)的指令地址0x0000000000400610已经被压到栈中保存起来了。

继续执行si指令,得到如下的结果:

将main函数的rbp存入rsp指向的内容(mov %rsp,%rbp),也就是将rsp执行的指令地址设置为新的栈底(rbp:0x7fffffffe2e0),随后sub $0x10,%rsp为函数参数、局部变量的所占用的空间分配内存,此时就完成了从main函数栈帧切换到function1函数栈帧。那此时function1的栈帧范围就是0x7fffffffe2e0—0x7fffffffe2d0(16字节)。
分析完函数栈帧切换流程,那再介绍下动态库中符号地址、二进制程序中代码段中符号地址在进程虚拟地址空间中的布局。
动态库
上文提到的进程虚拟地址空间布局图,动态库中函数的地址,一般位于以0x7f开头地址处,比rsp,rbp的指令地址要小,比程序入口main函数地址、堆地址要大,比如:libc库的一些函数如printf,fopen,fread,fwrite等都位于这个范围。

代码段
代码段位于elf文件的.text段,进程启动,会通过内存映射的方式将磁盘空间二进制文件elf中.text映射到进程虚拟空间的指定范围内。

虽然程序每次运行时,具体段对应的虚拟内存值会有变化,假如程序崩溃,当前的虚拟内存布局是固定的,这些信息都会写在coredump文件中。我们可以使用readelf命令来读取各个段的虚拟地址范围。

gdb中可以使用info shared命令来找到对应的动态库的代码段在虚拟内存中的地址。

推栈方法
1、用gdb调试程序并附加上当前的进程对应的coredump。
2、使用gdb获取寄存器信息,找到rsp、rbp、rip对应的地址值。
3、根据rsp、rbp的地址范围在x/64xg $rsp指令输出的内容中去寻找大小相近的地址。
4、rip是进程崩溃那一刻执行的指令地址,结合动态库在当前进程的虚拟地址范围,在x/64xg $rsp指令输出的内容中去寻找大小相近的地址。
5、基于第4步收集到的地址集合,结合动态库在进程虚拟地址空间中的起始地址,计算出各地址相对于起始地址的偏移量。
6、基于第3步收集到的地址集合,结合当前二进制程序中代码段.text在进程虚拟地址空间中起始地址(通过info files指令可以查看),计算出各地址相对于起始地址的偏移量。
7、准备程序的符号文件,使用addr2line计算出函数所在的源码文件及对应的行号。
结合实际案例进行演练
先准备下面的代码main.c、libcrash.c
typedef int (*FUNC)(int);extern int crash(int);int function2(int i){FUNC f;f = crash;return f(i);}int function1(int i){function2(++i);return 0;}int main(){return function1(0);}
#include <stdio.h>#include <stdlib.h>int function6(int a){unsigned long long int *p = (unsigned long long int *)&a;p[0]=p[1]=p[2] = 0xffffffffffffffff;return 0;}int function5(int a){int b = a;return function6(b);}int function4(int a){int b = a;return function5(b);}int function3(int a){int b = a;return function4(b);}int crash(int i){int a;function3(a);return ++i;}
libcrash.c最后会编译成libcrash.so库,也就是这个库中function6破坏了栈空间。还有些生成符号gensym、删除符号rmsym、MakeFile脚本、设置coreDump和加载符号路径的脚本。
生成符号脚本:
function gensym() {if [ ! -d .debug ]; thenmkdir .debugfiobjcopy --only-keep-debug libcrash.so .debug/libcrash.so.`md5sum libcrash.so| awk '{print $1}'`objcopy --add-gnu-debuglink=.debug/libcrash.so.`md5sum libcrash.so| awk '{print $1}'` libcrash.soobjcopy --only-keep-debug main .debug/main.`md5sum main| awk '{print $1}'`objcopy --add-gnu-debuglink=.debug/main.`md5sum main| awk '{print $1}'` main}gensym
function rmsym() {if [ -d .debug ]; thenrm -rf .debugfi } rmsym
cmake脚本:
all:gcc -fno-stack-protector -g --shared libcrash.c -o libcrash.sogcc -fno-stack-protector -g main.c -L`pwd` -lcrash -o mainbash -c "./gensym.sh"strip main libcrash.so.PYTHON: cleanclean:-rm -f main libcrash.sobash -c "./rmsym.sh"
设置符号和coreDump脚本:
sysctl -n kernel.core_pattern > ~/kernel.core_pattern.baksudo sysctl -w kernel.core_pattern=/tmp/core/core-%e-%s-%p-%u-%g-%tmkdir /tmp/coreulimit -c unlimitedexport LD_LIBRARY_PATH=`pwd`
执行make,生成main二进制程序,再运行main程序,生成coredump,使用gdb调试coredump,查看堆栈信息。

因为我们的程序调用链很长,当前堆栈并不完整,只展示部分。再比如有些极端的场景下,栈帧完整看不到了,没有任何相关符号信息。那么想推导出完整的堆栈信息,此时就需要用到上文介绍的推栈方法了。

回溯当前栈帧往调用者方向64*8个字节的内容打印出来:

看到当前栈帧的基址是0x7fffffffe1d0,rip指向的指令地址是0x7ffff7bd96ee(function6),那么我们在上图中寻找和0x7fffffffe1d0相近的栈基rbp地址,寻找和rip指令地址相近的动态库函数地址。
动态库libcrash.so的虚拟地址范围如下。

结合上图动态库虚拟地址范围,寻找和rip指令地址相近的,来自动态库libcrash.so的符号地址集合。

再结合main二进制程序中.text代码段在进程虚拟地址空间的地址范围,进一步推导main二进制程序中的符号地址集合。


好,经过上次的查找和收集,我们可以得到如下的函数地址偏移信息:

最后一步通过addr2line指令,结合上图中的地址偏移量,把整个堆栈的符号信息全部还原出来(具体调用哪些库,调用了哪个函数,来自哪一行,全部还原回来了)。

结合两个c文件,看看还原得对不对。


相关文章:
《Linux栈破坏了,如何还原》
【栈破坏导读】栈破坏有了解过吗?何为栈破坏,栈破坏了,程序会立刻引发崩溃,我们通过gdb去调试coredump,栈被破坏的栈帧是没法被恢复的,这也给我们调试程序带来很大的困难,那如何还原栈破坏的第一…...
环形链表问题的探究与代码实现
在数据结构与算法的学习中,环形链表是一个经典的问题。它不仅考察对链表这种数据结构的理解,还涉及到指针操作和逻辑推理。本文将结合代码和图文,深入分析如何判断链表中是否有环以及如何找到环的入口点。 目录 一、判断链表中是否有环 …...
【CSS3】筑基篇
目录 复合选择器后代选择器子选择器并集选择器交集选择器伪类选择器 CSS 三大特性继承性层叠性优先级 背景属性背景色背景图背景图平铺方式背景图位置背景图缩放背景图固定背景复合属性 显示模式显示模式块级元素行内元素行内块元素 转换显示模式 结构伪类选择器结构伪类选择器…...
React:类组件(上)
kerwin老师我来了 类组件的创建 class组件,js里的类命名首字符大写,类里面包括构造函数,方法 组件类要继承React.Component才有效 必须包含render方法 import React from react class App extends React.Component{render() {return <…...
开启mysql远程登录
目录 前言开启步骤 前言 为了安全考虑,mysql默认不允许远程登录,需要我们自己开启。当然在远程登录之前mysql的端口也要开放。下面是mysql开启远程登录的步骤。 开启步骤 本地登录mysql mysql -u root -p然后输入登录密码 给登录账号授权 GRANT AL…...
Eclipse 查看 JAVA SE 23 官方API 源代码
第一步:下载 JAVA SE 23 官方API 源代码 JavaSE23API源代码资源-CSDN文库 (或者到open jdk网站JDK Builds from Oracle:)下载https://download.java.net/java/GA/jdk23.0.2/6da2a6609d6e406f85c491fcb119101b/7/GPL/openjdk-23.0.2_windows-…...
Spring Cloud之注册中心之Nacos的使用
目录 Naacos 服务注册/服务发现 引⼊Spring Cloud Alibaba依赖 引入Nacos依赖 引入Load Balance依赖 配置Nacos地址 服务端调用 启动服务 Naacos Nacos是Spring Cloud Alibaba的组件, Spring Cloud Alibaba遵循Spring Cloud中定义的服务注册, 服务发现规范. 因此使⽤Na…...
字符串相乘——力扣
给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。 注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。 示例 1: 输入: num1 "2", num2 "3" …...
机试准备第13天
第一题是模拟出入栈游戏。 #include <stdio.h> #include <stack> #include <iostream> using namespace std; int main() {string str;while(getline(cin, str)){stack<char> stk;int j 0;//扫描出栈序列strfor(char i a;i<z;i){stk.push(i);//每…...
基于OpenCV的车牌识别系统(源码+论文+部署教程)
运行环境 基于OpenCV的车牌识别系统运行环境如下: • Python: ≥ 3.5 • OpenCV: ≥ 4.0 • IDE工具:Visual Studio Code(可自行选择) • 技术栈:Python OpenCV Tkinte 主要功能 基于OpenCV的车牌识别系统主要…...
MySQL:CRUD(增删查改)
目录 一、准备工作 二、Create 新增 1、语法 2、单行数据全列插入 3、单行数据指定列插入 4、多行数据指定列插入 5、多行数据全列插入 三、Retrieve 检索 1、语法 2、全列查询 3、指定列查询 4、查询字段为表达式 (1)常量表达式 &…...
德鲁伊连接池
德鲁伊连接池(Druid Connection Pool)是一个开源的Java数据库连接池项目,用于提高数据库连接的性能和可靠性。德鲁伊连接池通过复用数据库连接、定时验证连接的可用性、自动回收空闲连接等机制,有效减少了数据库连接的创建和销毁开…...
【git】【网络】【项目配置运行】HTTP 协议的微型简易 Web 服务器---tinyEasyMuduoWebServer
【git】【网络】【项目配置运行】HTTP 协议的微型简易 Web 服务器—tinyEasyMuduoWebServer csdn项目: 原文链接:https://blog.csdn.net/weixin_45178775/article/details/122257814 github链接:https://github.com/wyewyewye/tinyEasyMuduo…...
每周一个网络安全相关工具——MetaSpLoit
一、Metasploit简介 Metasploit(MSF)是一款开源渗透测试框架,集成了漏洞利用、Payload生成、后渗透模块等功能,支持多种操作系统和硬件平台。其模块化设计(如exploits、auxiliary、payloads等)使其成为全球…...
Python入门———条件、循环
目录 语句 顺序语句 条件语句 缩进和代码块 判断年份是否是闰年 空语句 pass 循环 while 循环 求5的阶乘: 求1!2!3!4!5! for循环 打印1-10 打印2,4,6,8&#x…...
InDraw6.2.3 | 甾体、核苷、黄酮类化合物实现简称命名
导语 当化学家对着屏幕输入"2-amino-1,9-dihydro-6H-purin-6-one"时,隔壁生物学家可能正在搜索"鸟嘌呤";这种命名差异如同"火星文"与"地球语"的碰撞。现在,鹰谷InDraw 6.2.3版带着53种多环化合物的…...
Linux中的TCP编程接口基本使用
TCP编程接口基本使用 本篇介绍 在UDP编程接口基本使用已经介绍过UDP编程相关的接口,本篇开始介绍TCP编程相关的接口。有了UDP编程的基础,理解TCP相关的接口会更加容易,下面将按照两个方向使用TCP编程接口: 基本使用TCP编程接口…...
系统部署【信创名录】及其查询地址
一、信创类型 (一)服务器: 1.华为云 2.腾讯云 3.阿里云 (二)中央处理器(CPU): 1.海思,鲲鹏920服务器 (三)中间件 1.人大金仓 ࿰…...
JavaWeb后端基础(7)AOP
AOP是Spring框架的核心之一,那什么是AOP?AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。AOP是一种思想,而在Spring框…...
Python 中多种方式获取屏幕的 DPI值
在 Python 中,可以通过多种方式获取屏幕的 DPI(每英寸点数)。以下是几种常见的方法: 方法 1:使用 tkinter 模块 tkinter 是 Python 的标准 GUI 库,可以通过它获取屏幕的 DPI。 import tkinter as tkdef …...
高效数据分析实战指南:Python零基础入门
高效数据分析实战指南 —— 以Python为基石,构建您的数据分析核心竞争力 大家好,我是kakaZhui,从事数据、人工智能算法多年,精通Python数据分析、挖掘以及各种深度学习算法。一直以来,我都发现身边有很多在传统行业从…...
Unity DOTS从入门到精通之EntityCommandBufferSystem
文章目录 前言安装 DOTS 包ECBECB可以执行的指令示例: 前言 DOTS(面向数据的技术堆栈)是一套由 Unity 提供支持的技术,用于提供高性能游戏开发解决方案,特别适合需要处理大量数据的游戏,例如大型开放世界游…...
开放充电点协议(OCPP)技术解析:架构演进与通信机制 - 慧知开源充电桩平台
开放充电点协议(OCPP)技术解析:架构演进与通信机制 引言 开放充电点协议(Open Charge Point Protocol, OCPP)作为电动汽车充电基础设施的核心通信标准,其技术架构与实现逻辑直接影响充电桩与中央管理系统&…...
MySQL 索引的数据结构(详细说明)
6. MySQL 索引的数据结构(详细说明) 文章目录 6. MySQL 索引的数据结构(详细说明)1. 为什么使用索引2. 索引及其优缺点2.1 索引概述 3. InnoDB中索引的推演3.1 索引之前的查找3.2 设计索引3.3 常见索引概念1. 聚簇索引2. 二级索引(辅助索引、非聚簇索引)…...
初学者快速入门Python爬虫 (无废话版)
全篇大概 5000 字(含代码),建议阅读时间 40min 一、Python爬虫简介 1.1 什么是网络爬虫? 定义: 网络爬虫(Web Crawler)是自动浏览互联网并采集数据的程序,就像电子蜘蛛在网页间"爬行"。 分类&…...
【git】ssh配置提交 gitcode-ssh提交
【git】ssh配置提交 gitcode-ssh提交 之前一直用的是gitee和阿里云的仓库,前两天想在gitcode上面备份一下我的打洞代码和一些资料 就直接使用http克隆了下来 。 在提交的时候他一直会让我输入账号和密码,但是我之前根本没有设置过这个,根本没…...
【二】JavaScript能力提升---this对象
目录 this的理解 this的原理 事件绑定中的this 行内绑定 动态绑定 window定时器中的this 相信小伙伴们看完这篇文章,对于this的对象可以有一个很大的提升! this的理解 对于this指针,可以先记住以下两点: this永远指向一个…...
C++————类和对象(一)
1.类定义格式 在C中,类(class)是封装数据和操作这些数据的函数的构造。类的定义包含成员变量和成员函数。 类的基本定义格式如下: class ClassName {// 访问修饰符public:// 公有成员DataType memberVariable; // 成员变量voi…...
SpringBoot参数校验:@Valid 与 @Validated 详解
SpringBoot参数校验:Valid 与 Validated 详解 一、案例(参数校验的必要性) 传统方式(无注解)的缺点: // 需要手动校验每个字段,代码冗余且易出错 public String register(User user) {// 手动…...
<论文>MiniCPM:利用可扩展训练策略揭示小型语言模型的潜力
一、摘要 本文跟大家一起阅读的是清华大学的论文《MiniCPM: Unveiling the Potential of Small Language Models with Scalable Training Strategies》 摘要: 对具有高达万亿参数的大型语言模型(LLMs)的兴趣日益增长,但同时也引发…...
