前几天碰到一个崩溃问题,利用coredump
查看崩溃堆栈信息时,却发现堆栈被破坏了
(gdb) bt
#0 0x0000000f55555186 in ?? ()
#1 0x0000000000000003 in ?? ()
#2 0x0000000e00000000 in ?? ()
#3 0x0000000000000000 in ?? ()
不过正好还可以利用栈帧和寄存器的信息继续去定位崩溃原因,这里简单做下分享用到的一些命令和知识
gdb的x命令
x命令是用于按照一定格式打印给定地址的内容,它的命令格式如下:
x [Address expression]
x /[Format] [Address expression]
x /[Length][Format] [Address expression]
-
[Address expression]
给定的需要打印内容的地址,这个地址可以是
-
指定的地址如`0x7fffffff270``
-
``C\C++
中的指针,如下的
ptr`指针const char *ptr="helloworld";
-
寄存器,如64位系统中的
rip\rsp\rbp
等
-
-
[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 ?? ()
接下来我们利用函数调用时下一条指令和栈底压栈的原理,来还原一下崩溃的过程
-
首先查看寄存器的信息,得到当前栈底寄存器
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
-
我们以8字节为内存单元,跳过被破坏的栈区域,往前查找128个内存单元,以地址格式输出,得到下图
-
根据图中的信息可以重建栈帧信息
栈底地址 返回后的下一条指令地址 0x7fffffffe140 0x401173 0x7fffffffe190 0x401173 0x7fffffffe1e0 0x401173 0x7fffffffe230 0x401173 0x7fffffffe280 0x401173 0x7fffffffe2d0 0x401173 0x7fffffffe320 0x401173 0x7fffffffe370 0x401173 0x7fffffffe3c0 0x401173 0x7fffffffe410 0x401173 -
用
addr2line
命令去查看指令地址对应的代码文件及行号root@DESKTOP-QA1H9TD:/mnt/d/Code/os# addr2line -e a.out 0x401173 /mnt/d/Code/os/stack.cpp:10
-
至此我们得到了部分函数的调用关系和大致的行号信息,结合这些信息和代码,我们就可以继续去定位崩溃的原因了
参考资料
GDB Command Reference - x command (visualgdb.com)
x86ManualBacktrace - Devpit