问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

gdb堆栈被破坏时的定位方法

创作时间:
作者:
@小白创作中心

gdb堆栈被破坏时的定位方法

引用
CSDN
1.
https://blog.csdn.net/weixin_37357702/article/details/121804164

当程序发生崩溃且堆栈信息被破坏时,如何定位问题原因?本文将通过一个具体的实例,详细介绍如何使用gdb调试工具的x命令和栈帧原理,来分析和定位堆栈被破坏情况下的崩溃原因。

gdb的x命令

x命令是用于按照一定格式打印给定地址的内容,它的命令格式如下:

x [Address expression]
x /[Format] [Address expression]
x /[Length][Format] [Address expression]
  • [Address expression]:给定的需要打印内容的地址,这个地址可以是

    1. 指定的地址如0x7fffffff270
    2. C/C++中的指针,如下的ptr指针
    const char *ptr="helloworld";
    
    1. 寄存器,如64位系统中的riprsprbp
  • [Format]:给定地址打印内容的输出格式,支持的格式包括

  • o - 八进制

  • x - 十六进制

  • d - 十进制

  • u - 无符号十进制

  • t - 二进制

  • f - 浮点数

  • a - 地址

  • c - 字符

  • s - 字符串

  • i - 指令

然后可以用增加下述修饰,表示以多少个字节作为一个内存单元,默认是4字节

  • b - 字节

  • h - 半字(2字节)

  • w - 字(4字节)

  • g - 巨型字(8字节)

  • [Length]:需要显示的内存单元个数,即从当前地址向后显示Length个内存单元的内容,一个内存单元的大小由上述的Format决定

栈帧

栈帧是记录函数调用过程的结构,详细可以阅读之前写的一篇博文什么是栈帧_Respiration-CSDN博客。简单来说,我们这里用到的一个原理是,当前函数调用其他函数时,会把返回后执行的当前函数下一条指令地址压栈,再把当前函数的栈底压栈,然后再开始执行调用函数的逻辑。

实例分析

编写一段无意义的代码,构建堆栈被破坏无法直接用gdb查看

#include <stdint.h>
void dummyFunc(int32_t n)
{
    int32_t szArr[10];
    szArr[n] = n;
    if (n < 20) {
        dummyFunc(n + 1);
    }
}
int main()
{
    dummyFunc(0);
}

运行后当然崩溃了,因为当n大于10时超出了szArr的数组大小,但是栈上分配给数组的空间就那么大,只能非预期地修改了a[19]这样的高地址内存内容,继而破坏了栈帧上记录的栈底地址和函数返回后继续执行的指令地址等,导致函数执行崩溃

然后用gdb查看是一堆的??

#0  0x0000000f55555186 in ?? ()
#1  0x0000000000000003 in ?? ()
#2  0x0000000e00000000 in ?? ()
#3  0x0000000000000000 in ?? ()

接下来我们利用函数调用时下一条指令和栈底压栈的原理,来还原一下崩溃的过程

  1. 首先查看寄存器的信息,得到当前栈底寄存器rbp的值为0x7fffffffe050,即指向当前栈底,而$rbp+8即为函数返回后下一条执行函数地址
(gdb) info reg
rax            0x0                 0
rbx            0x5555555551c0      93824992235968
rcx            0x5555555551c0      93824992235968
rdx            0x14                20
rsi            0x7fffffffe5b8      140737488348600
rdi            0x14                20
rbp            0x7fffffffe050      0x7fffffffe050
rsp            0x7fffffffe010      0x7fffffffe010
r8             0x0                 0
r9             0x7ffff7fe0d50      140737354009936
r10            0x7                 7
r11            0x0                 0
r12            0x555555555060      93824992235616
r13            0x7fffffffe5b0      140737488348592
r14            0x0                 0
r15            0x0                 0
rip            0xf55555186         0xf55555186
eflags         0x10246             [ PF ZF IF RF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
  1. 我们以8字节为内存单元,跳过被破坏的栈区域,往前查找128个内存单元,以地址格式输出,得到下图

  2. 根据图中的信息可以重建栈帧信息

栈底地址 返回后的下一条指令地址
0x7fffffffe140 0x401173
0x7fffffffe190 0x401173
0x7fffffffe1e0 0x401173
0x7fffffffe230 0x401173
0x7fffffffe280 0x401173
0x7fffffffe2d0 0x401173
0x7fffffffe320 0x401173
0x7fffffffe370 0x401173
0x7fffffffe3c0 0x401173
0x7fffffffe410 0x401173

  1. addr2line命令去查看指令地址对应的代码文件及行号
root@DESKTOP-QA1H9TD:/mnt/d/Code/os# addr2line -e a.out 0x401173
/mnt/d/Code/os/stack.cpp:10
  1. 至此我们得到了部分函数的调用关系和大致的行号信息,结合这些信息和代码,我们就可以继续去定位崩溃的原因了

参考资料

  • GDB Command Reference - x command (visualgdb.com)
  • x86ManualBacktrace - Devpit
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号