0. 简介
coredump是一个使用c++编程工作者最常用的方法,但是如果在 GCC -O3
优化级别下,很多局部变量是会被优化掉的,此时只能通过人工分析反汇编代码来获取所需信息,而这么做的前提是保存下来的寄存器中的值是准确的。绝大部分情况下 coredump 是由于 segment fault
或 assert
触发的,segment fault
情况下 Kernel 保存下来的 registers
信息是准确的,GDB 中直接用 info registers
就可以看到。然而若是由 assert 触发,由于 assert 会进行多层函数调用后最终执行 raise()
,错误现场的寄存器信息是不准确的,这时候就需要一些其他手段来解决此问题。为此我们这篇文章将着重介绍如何使用coredump当中的高级用法。这里主要参考了僷枫_华大佬的博客,并结合自己的理解做了一些整合和梳理
1 GDB使用整理
1.1 主动生成运行中进程的core dump
这种需求主要发生在进程卡死,想知道卡在何处时。
gcore -o <core dump文件名> <pid>
可以主动生成core dump文件,这个操作不会杀死进程,如果有需要可手动杀死。
然后参照[使用core dump](#使用core dump)的步骤去查看调用堆栈就可以了
1.2 GDB调试指令
- q[uit]
退出gdb的q指令。
- help
在gdb中,用help指令查看各种指令的联机帮助。
- file
如果在启动gdb的时候,没有指定program,在gdb内,用file指令指定。
- set args
用于在run程序之前,指定此程序的命令行参数,也可以用来去掉命令行的参数,如下:
(gdb) set args [arguments]
- set print pretty on
漂亮的打印信息,总是需要的。
- b[reak]
进入gdb后,一般我们都要先设置断点,然后再启动程序。
b指令用于设置断点,后面除了跟行号,还可以:
- 如果是多文件的场景设置断点:
b <file_name>:<line_number>
,此时file_name无需路径。 - 还可以直接用函数名设置断点:
b function_name
,比如:b main
。 - 或者:
b <file_name>:<function_name>
。 - 还可以在设置断点的时候,增加一个condition:
b location if condition
。 - 将断电设置在一个地址上,
b *0x400540
,有个*
,调试汇编代码时很好用。
gdb还有很多其它花样的break:
- tbreak
临时断点,作用一次后自动消失,设置方式与break相同。
- rbreak regex
对符合regex正则表达式条件的函数名设置断点,效果与break一样,不会自动消失。
- l[ist]
在gdb中查看源代码和对应的行号。
l指令输入后,默认从当前位置打印10行代码出来显示,按回车(自动执行上一条gdb命令)会自动继续显示10行代码。改变list指令默认behavior可以这样:
(gdb) l <line_number> # 从指定行号开始打印10号代码 (gdb) l <start_line_number>,<end_line_number> (gdb) l <function_name>
- show listsize
查看l指令显示代码行数。
- set listsize
设置l指令显示代码的行数,默认为10。
- r[un] [args]
r指令(重新)启动程序,直到程序结束,或Crash,或者遇到breakpoint。
程序在断点停下来后,就要开始一点点推进程序代码的执行,在此过程中仔细检查各变量和相关内存区域的值。用r指令也可以在任何时候**重新运行程序!**r指令也可以带上给调试进程的命令行参数,但在重复run时,要去掉args,只能使用set args
。
- start
启动调试程序,停在main处,使用start指令,gdb自动在main处设置了一个临时断点。同run指令一样,start也可以重复执行,每次都会停在main开始的位置。
- kill
stop debugging.
- n[ext]
执行下一条指令,如果下一条指令是函数调用,不进入函数。
直接回车,重复上一个命令!如果上一个命令是n,就很有用。
- s[tep]
执行下一条指令,如果下一条指令是函数调用,进入函数。
- c[ontinue]
继续,continue,直到下一个breakpoint,或者程序结束,或者程序Crash。所以,r指令一般只用一次,然后就不再用了,后面要让程序继续跑起来,用c指令。
- finish
将当前的function执行完,返回function调用的地方。
- where/bt
显示backtrace,查看当前程序调用栈信息,以及程序当前停在哪一行的。
- advance
advance指令实现让代码自动运行到指定函数的入口,并进入函数。
可以简单地用advance bar
,来代替tbreak bar; continue
。如果没有到达指定位置,执行会在当前函数末尾停下来(Execution will also stop upon exit from the current stack frame)。
- skip
Ignore a function while stepping.
- i[nfo]
用于显示各种信息。
- i[nfo] b
查看所有breakpoint信息,此处会显示出断点的编号。
- i[nfo] shared
查看程序使用的动态链接库信息。
- i[nfo] locals
查看所有local variables。
- i[nfo] frame
查看当前的stack frame。
help info可以看到更多关于info的选项。
- delete
删除N号断点。
- delete [breakpoints]
清除所有断点。
- disable/enable
关闭/开启N号断点。
- p[rint]
print指令,后面跟一个表达式,显示表达式的值。
同时显示多个变量的值:print {var1, var2, var3}
,like this…
可以通过/fmt
格式来增加格式信息,比如:p /x *b
,以hex的方式打印b地址的内容。格式与x指令通用。p指令还可以用来查macro的“值”,但是需要gcc编译的时候,带上-g3 -gdwarf-2
参数。
- x
examine memory,这里的memory,是指程序的虚拟地址空间。
显示当前rip指向地址的4条汇编指令:
(gdb) x /4i $pc
- display
设置在程序停止运行时,自动显示的表达式。在单步调试的时候,这个指令很有用,每次next,自动显示你关心的变量值。可以设置多个display,用多行命令,或者display {exp1, exp2, exp3}
。
- undisplay
取消display。
- macro
调试代码中的macro。复杂代码中到底哪个macro在起作用?!
(gdb) help macro
macro exp
最有用…
- disass[emble]
将当前RIP位置周围的代码进行反汇编显示,或反汇编指定的函数,或某个地址范围进行反汇编显示。
disass # show一下周围的汇编指令 disass $pc,+16 # 显示从rip地址开始,向后16bytes的汇编 disass funcname # disassemble function disass 0x400544 disass 0x400544, 0x40054d
- show disassembly-flavor
(gdb) show disassembly-flavor The disassembly flavor is "att".
- set disassembly-flavor
(gdb) set disassembly-flavor intel (gdb) disass
是时候学点汇编了!
- i[nfo] r[egister]
显示所有整型寄存器的值。也可以指定显示某个寄存器:
(gdb) i r rax rax 0x2c63652 46544466
- i[nfo] all-registers
显示所有寄存器。
- i[nfo] line [N]
将源代码中的某行,与地址范围对应起来。例如:
(gdb) info line 5 Line 5 of "test.c" starts at address 0x401146 <main> and ends at 0x40114e <main+8>.
1.3 调试线程
- i threads
显示所有线程,每个线程有个编号。
- thread
切换到编号为N的线程。
- thread apply […]
让一个或多个线程执行gdb的command命令。
2. 反汇编直接分析
这种方法是最常见也是最方便的方法。下图是人为制造coredump,31 行对空指针赋值,产生 coredump
gdb a.out core.11246
在地址 0x00000000004008ce 处发生 coredump
然后我们可以执行程序进行反汇编
objdump --source -d a.out > a.asm
搜索地址 “4008ce”
当赋值 0x23 时发生 coredump。这就可以和真实的代码对上了
3. 终端调试分析—Segmentation fault
Segmentation fault(段错误)是一种由于程序访问了未分配的内存地址或者越界访问内存而导致的错误。当程序试图访问不属于它的内存段(比如访问空指针或者数组越界)时,操作系统会发送一个信号给程序,通知它发生了段错误。这通常是由于程序bug导致的,需要开发人员进行代码调试和修复。这类情况都是比较准的,所以我们可以使用disassemble
反汇编的形式来看相关挂掉的地方。
查看堆栈使用bt或者where命令
如上,在带上调试信息的情况下,我们实际上是可以看到core的地方和代码行的匹配位置。
但往往正常发布环境是不会带上调试信息的,因为调试信息通常会占用比较大的存储空间,一般都会在编译的时候把-g选项去掉。
没有调试信息的情况下找core的代码行