DEBUG-HACKS 内核转储与GDB调试
获取用户进程的内核转储
获取内核转储(core dump)的最大好处是,能保存问题发生时的状态。
启用内核转储
ulimit -c # 查看转储文件大小限制 |
可以将ulimit -c unlimited
添加到~/.bashrc
中,使得每次打开shell都会生效。编写一个会产生Segmentation fault
的代码文件segfault.c
:
|
使用gcc编译并执行:
gcc -g segfault.c # -g 可执行程序包含调试信息 |
当前目录下生成core
文件,用GDB调试生成的core文件:
gdb -c core ./a.out |
有如下打印:
…
Reading symbols from ./a.out…done.
[New LWP 15050]
Core was generated by `./a.out’.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000056149ce0060a in main () at segfault.c:5
5 *a = 0x1;
(gdb)
(gdb) l 5 # gdb list 指令,查看第5行附近的代码 |
在专用目录中生成内核转储
打开/etc/sysctl.conf
并添加如下配置:
kernel.core_pattern = /root/code/core/%t-%e-%p-%c.core |
其中,%t-%e-%p-%c
依次为生成内核转储的时刻-进程名-PID-内核转储最大尺寸。尝试生成core dumped文件:
mkdir /root/code/core |
GDB的基本使用方法
带着调试选项编译、构建调试对象
通过gcc的-g
选项生成调试信息:
gcc -Wall -O2 -g xxx.c |
如果使用Makefile构建,一般要给CFLAGS
中指定-g
选项:
CFLAGS=-Wall -O2 -g |
-Wall
是生成所有警告信息,-Werror
是将警告信息当作错误。
启动GDB
gdb 可执行文件名 |
设置断点
gdb启动后,执行break
命令,简写为b
:
break 函数名 |
其中,break *函数名
,断点会设置在汇编语言层次的函数开头,不加*则会设置到地址偏后一点的源代码级别的开头。
运行
用run
命令开始运行,简写为r
,会执行到断点处暂停运行。start
命令会执行到main函数开始处暂停运行。
显示栈帧
用backtrace
命令可以在遇到断点暂停时显示栈帧,简写为bt
,别名where
和info stask
(简写为info s
)。
bt # 显示所有栈帧 |
显示变量
用print
命令可以显示变量,简写为p
。
用p/格式 变量
可以控制显示的格式,可用的格式如下:
格式 | 说明 |
---|---|
x | 显示为十六进制数 |
d | 显示为十进制数 |
u | 显示为无符号十进制数 |
o | 显示为八进制数 |
t | 显示为二进制,t来自于two |
a | 地址 |
c | 显示为字符(ascii) |
f | 浮点数 |
s | 显示为字符串 |
i | 显示为机器语言(仅在显示内存的x 命令中可见) |
显示寄存器
用info registers
可以显示寄存器,简写为info reg。
用print
命令在寄存器名之前加$
,可显示寄存器的内容:
p $r1 |
显示x86_64下的浮点寄存器(xmm寄存器):
p $xmm0.v4_float[0] # xmm0寄存器按4xfloat的第一个元素 |
用x
命令(来自于eXamining)可以显示内存的内容,格式为x/[数量][格式][单位] 地址
。可用的单位有:
单位 | 说明 |
---|---|
b | 字节 |
h | 半字(2字节) |
w | 字(4字节)(默认) |
g | 双字(8字节) |
x $pc |
也可用dissassemble
命令进行反汇编,简写为disas
。
disas # 反汇编当前整个函数 |
显示栈的内容
x/10g $rsp # g: gaint word 双字,显示10个双字的内容 |
单步执行
next
命令执行源代码中的一行,简写为n
。
step
命令执行到函数内部,简写为s
。
如果要逐条执行汇编指令,可以分别使用nexti
和stepi
,简写为ni
和si
。
继续执行
用continue
命令继续运行程序,会在遇到断点时再次暂停,如没有遇到断点,会一直执行到结束,简写为c
。后面可以加数字N
,表示再次遇到当前所在的断点会跳过N次。
监视点
使用watch
/awatch
/rwatch
命令监视变量在何处被改变/访问:
watch <表达式> # <表达式>发生变化(write)时暂停运行 |
删除断点和监视点
使用delete
命令删除断点和监视点,格式为delete <编号>
,简写为d
。
改变变量的值
set variable <变量>=<表达式>
可以在运行时随意修改变量的值,无须修改源代码就能确定各种值的情况。
也可以随意定义变量,变量以$
开头,由英文字母和数字组成。
set $my_val=100 |
生成内核转储文件
使用generate-core-file
可将调试中的进程生成内核转储文件:
gdb <可执行文件> |
有了内核转储文件,以后就能查看生成转储文件时的运行历史(寄存器值、内存值等)。
此外,gcore
命令可以从命令行直接生成内核转储文件:
gcore <pid> # pid 为 待分析的进程号 |
这样可以无需停止正在运行的程序以获取内核转储文件。
attach 到进程
一个示例,hello.cpp
文件如下:
|
编译执行:
g++ -g hello.cpp -o hello |
程序会卡在死循环里,另开一个终端,先通过ps aux|grep hello
查看进程ID,然后可以通过GDB attach到该进程:
ps aux|grep hello # 第二列为pid |
这时发现原终端中卡死的程序也执行完毕。此外,在GDB中可以通过info proc
查看进程信息,通过detach
命令与进程分离。
条件断点
break 断点 if 条件 # 条件为真则在断点处暂停 |
反复执行
以下命令可以执行指定次数:
ignore 断点编号 次数 # 编号指定的断点、监视点、捕获点忽略指定的次数 |
finish
命令可以执行完当前函数后暂停,until
命令执行完当前代码块后暂停,常用于跳出循环。
删除断点和禁用断点
clear
命令也可以用来删除断点,与delete
的参数不同:
clear # 删除所有断点 |
disable
命令可以临时禁用断点,breakpoints关键字可省略:
disable [breakpoints] # 禁用所有断点 |
相反地,可以使用enable
命令使能断点:
enable [breakpoints] # 启用所有断点 |
断点命令
commands
命令可以定义在断点暂停后自动执行的命令,格式如下:
commands 断点编号 |
此外,如果命令的第一行为silent
命令,就不会显示在断点处暂停的信息,单独进行信息输出时这点很有用。
常用命令汇总
命令 | 简写 | 说明 |
---|---|---|
backtrace | bt、where | 显示栈帧 |
break | b | 设置断点 |
continue | c | 继续执行 |
delete | d | 删除断点 |
finish | 运行到函数结束 | |
info breakpoints | i b | 显示断点信息 |
next | n | 单步执行 |
p | 显示表达式 | |
run | r | 运行程序 |
step | s | 单步步入 |
x | 显示内存内容 | |
until | u | 执行到代码块结束 |
其他非常用指令 | ||
directory | dir | 插入目录 |
disable | dis | 禁用断点等 |
down | do | 在当前调用的栈帧中选择要显示的栈帧 |
edit | e | 编辑文件或函数 |
frame | f | 选择要显示的栈帧 |
forward-search | fo | 向前搜索 |
generate-core-file | gcore | 生成内核转储 |
help | h | 显示帮助一览 |
info | i | 显示信息 |
list | l | 显示函数或行 |
nexti | ni | 汇编指令单步执行 |
print-object | po | 显示目标信息 |
sharedlibrary | share | 加载共享库的符号 |
stepi | si | 执行下一行 |
值的历史
通过print
命令显示过的值会记录在内部的值历史中,这些值可以在其他表达式中使用。可以用show value
命令显示历史中的最后10个值,其他访问方式如下:
变量 | 说明 |
---|---|
$ | 值历史的最后一个值 |
$n | 值历史的第n个值 |
$$ | 值历史的倒数第2个值 |
$$n | 值历史的倒数第n个值 |
$_ | x 命令显示过的最后的地址 |
$__ | x 命令显示过的最后的地址的值 |
$_exitcode | 调试中的程序的返回代码 |
$bpnum | 最后设置的断点编号 |
命令定义
用define
命令可以自定义命令,用document
命令可以自定义的命令添加说明,用help
命令可以查看定义的命令。可以将自定义的命令写到文件中,在gdb调试时通过source
命令读取。
# commands file |
(gdb) source <filename> |
peda插件
peda插件会在程序运行时实时显示寄存器值、汇编代码、栈等信息,更加方便:
# 安装gdb插件peda |
GDB进阶操作
操作栈帧
// sum.c |
使用bt
命令查看栈帧情况:
gcc -o sum -g sum.c |
打印如下:
#0 sum_till_MAX (n=0x5) at sum.c:15
#1 0x00005555554007a5 in sum_till_MAX (n=0x4) at sum.c:17
#2 0x00005555554007a5 in sum_till_MAX (n=0x3) at sum.c:17
#3 0x00005555554007a5 in sum_till_MAX (n=0x2) at sum.c:17
#4 0x00005555554007a5 in sum_till_MAX (n=0x1) at sum.c:17
#5 0x0000555555400866 in main (argc=0x2, argv=0x7fffffffe408) at sum.c:33
#6 0x00007ffff7a03c87 in __libc_start_main (main=0x5555554007af, argc=0x2, argv=0x7fffffffe408, init= , fini= ,
rtld_fini=, stack_end=0x7fffffffe3f8) at …/csu/libc-start.c:310
#7 0x000055555540069a in _start ()
用frame
命令可以查看当前所在的栈帧信息,用frame N
可以跳转到N
对应的栈帧,此时可以打印该栈帧下的变量:
(gdb) frame |
此外,up
命令可以选择上一层的帧,down
命令可以选择下一层的帧。使用info frame N
可以显示更为详细的栈帧信息。
调试栈溢出
上述的sum.c
程序在不指定参数的情况下执行,会调用2^20次sum_till_MAX
函数,每次调用都会生成栈帧,消耗栈空间,从而发生了栈溢出:
gdb sum |
Stopped reason: SIGSEGV
0x0000555555400782 in sum_till_MAX (n=<error reading variable: Cannot access memory at address 0x7fffff7fefec>) at sum.c:12
12 u64 sum_till_MAX(u32 n)
这时可以查看一下程序计数器PC和栈指针SP的情况(如果使用peda,可以直接查看寄存器RIP和RSP):
x/i $pc # => 0x555555400782 <sum_till_MAX+8>: mov DWORD PTR [rbp-0x14],edi |
可以用i proc mapping
命令查看进程的内存映射情况,其显示的是被调试进程对应的/proc/<PID>/maps
的信息:
process 6024
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x555555400000 0x555555401000 0x1000 0x0 /root/code/debug_hacks/stack_status/sum
0x555555600000 0x555555601000 0x1000 0x0 /root/code/debug_hacks/stack_status/sum
0x555555601000 0x555555602000 0x1000 0x1000 /root/code/debug_hacks/stack_status/sum
0x7ffff79e2000 0x7ffff7bc9000 0x1e7000 0x0 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7bc9000 0x7ffff7dc9000 0x200000 0x1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dc9000 0x7ffff7dcd000 0x4000 0x1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dcd000 0x7ffff7dcf000 0x2000 0x1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dcf000 0x7ffff7dd3000 0x4000 0x0
0x7ffff7dd3000 0x7ffff7dfc000 0x29000 0x0 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ff0000 0x7ffff7ff2000 0x2000 0x0
0x7ffff7ff7000 0x7ffff7ffb000 0x4000 0x0 [vvar]
0x7ffff7ffb000 0x7ffff7ffc000 0x1000 0x0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x29000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ffd000 0x7ffff7ffe000 0x1000 0x2a000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0
0x7fffff7ff000 0x7ffffffff000 0x800000 0x0 [stack]
其中最后一行显示了该进程栈空间的起始(栈顶)、结束(栈底)地址和大小,而上面看到栈指针的值0x7fffff7fefe0
已经超出了栈的范围,发生了栈溢出。
由于i proc mapping
命令会打开/proc/<PID>/maps
,因此在分析core dump时无法使用,这时我们可以用info files
或info target
命令来查看:
./sum # Segmentation fault (core dumped) |
…
[New LWP 23248]
Core was generated by `./sum’.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000563aeac00782 in sum_till_MAX (n=<error reading variable: Cannot access memory at address 0x7ffdec051ffc>) at sum.c:12
12 u64 sum_till_MAX(u32 n) {
p $sp # $1 = (void *) 0x7ffdec051ff0 |
0x0000563aeac00000 - 0x0000563aeac01000 is load1
0x0000563aeae00000 - 0x0000563aeae01000 is load2
0x0000563aeae01000 - 0x0000563aeae02000 is load3
0x00007f4114b9a000 - 0x00007f4114b9b000 is load4a
0x00007f4114b9b000 - 0x00007f4114b9b000 is load4b
0x00007f4114d81000 - 0x00007f4114d81000 is load5
0x00007f4114f81000 - 0x00007f4114f85000 is load6
0x00007f4114f85000 - 0x00007f4114f87000 is load7
0x00007f4114f87000 - 0x00007f4114f8b000 is load8
0x00007f4114f8b000 - 0x00007f4114f8c000 is load9a
0x00007f4114f8c000 - 0x00007f4114f8c000 is load9b
0x00007f41151ad000 - 0x00007f41151af000 is load10
0x00007f41151b4000 - 0x00007f41151b5000 is load11
0x00007f41151b5000 - 0x00007f41151b6000 is load12
0x00007f41151b6000 - 0x00007f41151b7000 is load13
0x00007ffdec052000 - 0x00007ffdec852000 is load14
0x00007ffdec886000 - 0x00007ffdec88a000 is load15
0x00007ffdec88a000 - 0x00007ffdec88b000 is load16
这里并没有具体显示栈空间,但可以推断出load14
便是,可以通过bt
和info frame
命令进一步确认:
(gdb) bt 3 |
#0 0x0000563aeac00782 in sum_till_MAX (n=<error reading variable: Cannot access memory at address 0x7ffdec051ffc>) at sum.c:12
#1 0x0000563aeac007a5 in sum_till_MAX (n=0x2aa5c) at sum.c:17
#2 0x0000563aeac007a5 in sum_till_MAX (n=0x2aa5b) at sum.c:17
(gdb) i f 0 |
Stack frame at 0x7ffdec052020:
rip = 0x563aeac00782 in sum_till_MAX (sum.c:12); saved rip = 0x563aeac007a5
called by frame at 0x7ffdec052050
source language c.
Arglist at 0x7ffdec052010, args: n=<error reading variable: Cannot access memory at address 0x7ffdec051ffc>
Locals at 0x7ffdec052010, Previous frame’s sp is 0x7ffdec052020
Saved registers:
rbp at 0x7ffdec052010, rip at 0x7ffdec052018
(gdb) bt -3 |
#174685 0x0000563aeac00866 in main (argc=0x1, argv=0x7ffdec851268) at sum.c:33
#174686 0x00007f4114bbbc87 in __libc_start_main (main=0x563aeac007af, argc=0x1, argv=0x7ffdec851268, init= , fini= ,
rtld_fini=, stack_end=0x7ffdec851258) at …/csu/libc-start.c:310
#174687 0x0000563aeac0069a in _start ()
(gdb) i f 174685 |
Stack frame at 0x7ffdec851190:
rip = 0x563aeac00866 in main (sum.c:33); saved rip = 0x7f4114bbbc87
called by frame at 0x7ffdec851250, caller of frame at 0x7ffdec851160
source language c.
Arglist at 0x7ffdec851180, args: argc=0x1, argv=0x7ffdec851268
Locals at 0x7ffdec851180, Previous frame’s sp is 0x7ffdec851190
Saved registers:rbp at 0x7ffdec851180, rip at 0x7ffdec851188
可以看到最上层的栈帧地址和main
函数的栈帧地址都是在上述范围0x00007ffdec052000 - 0x00007ffdec852000
内,而栈指针0x7ffdec051ff0
超出了这个范围,便是发生了栈溢出。
栈空间的大小可以用ulimit -s
查看,可以用ulimit -Ss
修改栈尺寸,从而避免栈溢出:
ulimit -s # 8192 单位为KB,8M |