

DEBUG-HACKS 内核转储与GDB调试
source link: https://no5-aaron-wu.github.io/2022/10/11/debug-hacks-1-core-dumped/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

DEBUG-HACKS 内核转储与GDB调试
获取用户进程的内核转储
获取内核转储(core dump)的最大好处是,能保存问题发生时的状态。
启用内核转储
ulimit -c # 查看转储文件大小限制
ulimit -c unlimited # 不限制内核转储文件大小,开启内核转储,仅对当前shell有效
可以将ulimit -c unlimited
添加到~/.bashrc
中,使得每次打开shell都会生效。编写一个会产生Segmentation fault
的代码文件segfault.c
:
#include <stdio.h>
int main() {
int *a = NULL;
*a = 0x1;
return 0;
}
使用gcc编译并执行:
gcc -g segfault.c # -g 可执行程序包含调试信息
./a.out # Segmentation fault (core dumped)
当前目录下生成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
kernel.core_uses_pid = 0 # 设置为1,则会在文件名尾添加.PID
其中,%t-%e-%p-%c
依次为生成内核转储的时刻-进程名-PID-内核转储最大尺寸。尝试生成core dumped文件:
mkdir /root/code/core
sysctl -p # 使能配置
./a.out # 执行
ls /root/code/core # 1665540532-a.out-19306-18446744073709551615.core
GDB的基本使用方法
带着调试选项编译、构建调试对象
通过gcc的-g
选项生成调试信息:
gcc -Wall -O2 -g xxx.c
如果使用Makefile构建,一般要给CFLAGS
中指定-g
选项:
CFLAGS=-Wall -O2 -g
-Wall
是生成所有警告信息,-Werror
是将警告信息当作错误。
启动GDB
gdb 可执行文件名
(gdb) set args -a xxx -b yyy # 设置参数
gdb启动后,执行break
命令,简写为b
:
break 函数名
break 行号
break 文件名:行号
break 文件名:函数名
break +偏移量 # 当前暂停位置往后+偏移量行
break -偏移量
break *地址
break # 下一行代码设置断点
info break # 打印设置好的断点
用run
命令开始运行,简写为r
,会执行到断点处暂停运行。start
命令会执行到main函数开始处暂停运行。
用backtrace
命令可以在遇到断点暂停时显示栈帧,简写为bt
,别名where
和info stask
(简写为info s
)。
bt # 显示所有栈帧
bt N # 只显示开头N个栈帧
bt -N # 只显示最后N个栈帧
bt full # 显示栈帧还有局部变量,也可跟上 N/-N
用print
命令可以显示变量,简写为p
。
用p/格式 变量
可以控制显示的格式,可用的格式如下:
格式 | 说明 |
---|---|
x | 显示为十六进制数 |
d | 显示为十进制数 |
u | 显示为无符号十进制数 |
o | 显示为八进制数 |
t | 显示为二进制,t来自于two |
a | 地址 |
c | 显示为字符(ascii) |
f | 浮点数 |
s | 显示为字符串 |
i | 显示为机器语言(仅在显示内存的x 命令中可见) |
显示寄存器
用info registers
可以显示寄存器,简写为info reg。
用print
命令在寄存器名之前加$
,可显示寄存器的内容:
p $r1
用x
命令(来自于eXamining)可以显示内存的内容,格式为x/[数量][格式][单位] 地址
。可用的单位有:
单位 | 说明 |
---|---|
b | 字节 |
h | 半字(2字节) |
w | 字(4字节)(默认) |
g | 双字(8字节) |
x $pc
x/i $pc # 显示一条汇编指令
x/10i $pc # 显示十条汇编指令
x/10dw arr # 显示arr数组中前10个元素,以int(4字节十进制)方式显示
也可用dissassemble
命令进行反汇编,简写为disas
。
disas # 反汇编当前整个函数
disas $pc # 反汇编程序计数器所在函数的整个函数
disas $pc,$pc+10 # 反汇编从开始地址到结束地址之间的部分
next
命令执行源代码中的一行,简写为n
。
step
命令执行到函数内部,简写为s
。
如果要逐条执行汇编指令,可以分别使用nexti
和stepi
,简写为ni
和si
。
用continue
命令继续运行程序,会在遇到断点时再次暂停,如没有遇到断点,会一直执行到结束,简写为c
。后面可以加数字N
,表示再次遇到当前所在的断点会跳过N次。
使用watch
/awatch
/rwatch
命令监视变量在何处被改变/访问:
watch <表达式> # <表达式>发生变化(write)时暂停运行
awatch <表达式> # <表达式>被访问(read)或发生变化(write)时暂停运行
rwatch <表达式> # <表达式>被访问(read)时暂停运行
删除断点和监视点
使用delete
命令删除断点和监视点,格式为delete <编号>
,简写为d
。
改变变量的值
set variable <变量>=<表达式>
可以在运行时随意修改变量的值,无须修改源代码就能确定各种值的情况。
也可以随意定义变量,变量以$
开头,由英文字母和数字组成。
set $my_val=100
p $my_val # $1 = 100
生成内核转储文件
使用generate-core-file
可将调试中的进程生成内核转储文件:
gdb <可执行文件>
(gdb) start
... # 其他调试操作
(gdb) generate-core-file # Saved corefile core.4858
有了内核转储文件,以后就能查看生成转储文件时的运行历史(寄存器值、内存值等)。
此外,gcore
命令可以从命令行直接生成内核转储文件:
gcore <pid> # pid 为 待分析的进程号
这样可以无需停止正在运行的程序以获取内核转储文件。
attach 到进程
一个示例,hello.cpp
文件如下:
#include <stdio.h>
#include <iostream>
void show_me_the_money(int money)
{
bool flag = true;
printf("before while, money = %d \n", money);
while(flag);
printf("after while, money = %d \n", money);
}
int main(int argc, char *argv[]) {
int money = 5;
show_me_the_money(money);
return 0;
}
编译执行:
g++ -g hello.cpp -o hello
./hello # before while, money = 5
程序会卡在死循环里,另开一个终端,先通过ps aux|grep hello
查看进程ID,然后可以通过GDB attach到该进程:
ps aux|grep hello # 第二列为pid
gdb attach <pid>
(gdb) bt # 显示栈帧,观察卡死程序是通过怎么样的调用途径陷入等待状态
(gdb) p flag # $1 = true
(gdb) set variable flag=false # 手动修改flag,解除死循环
(gdb) c # 继续执行
# Continuing.
# [Inferior 1 (process 19216) exited normally]
这时发现原终端中卡死的程序也执行完毕。此外,在GDB中可以通过info proc
查看进程信息,通过detach
命令与进程分离。
break 断点 if 条件 # 条件为真则在断点处暂停
condition 断点编号 条件 # 为已有断点添加条件
condition 断点编号 # 为已有断点删除条件
以下命令可以执行指定次数:
ignore 断点编号 次数 # 编号指定的断点、监视点、捕获点忽略指定的次数
continue 次数 # 达到指定次数前,执行到断点时不暂停
step 次数 # 单步步入指定次数
stepi 次数 # 单步步入汇编指令指定次数
next 次数 # 单步执行指定次数
next 次数 # 单步执行汇编指令指定次数
finish
命令可以执行完当前函数后暂停,until
命令执行完当前代码块后暂停,常用于跳出循环。
删除断点和禁用断点
clear
命令也可以用来删除断点,与delete
的参数不同:
clear # 删除所有断点
clear 函数名 # 删除函数入口处的断点
clear 行号 # 删除行号处的断点
clear 文件名:行号
clear 文件名:函数名
delete [breakpoints] 断点编号 # breakpoints关键字 可省略
disable
命令可以临时禁用断点,breakpoints关键字可省略:
disable [breakpoints] # 禁用所有断点
disable [breakpoints] 断点编号 # 禁用指定的断点
disable display 显示编号 # 禁用display命令定义的自动显示
disable mem 内存区域 # 禁用mem命令定义的内存区域
相反地,可以使用enable
命令使能断点:
enable [breakpoints] # 启用所有断点
enable [breakpoints] 断点编号 # 启用指定的断点
enable [breakpoints] once 断点编号 # 启用指定断点一次,程序运行到该断点并暂停后,被禁用
enable [breakpoints] delete 断点编号 # 启用指定断点一次,程序运行到该断点并暂停后,被删除
enable display 显示编号 # 启用display命令定义的自动显示
enable mem 内存区域 # 启用mem命令定义的内存区域
commands
命令可以定义在断点暂停后自动执行的命令,格式如下:
commands 断点编号
命令
...
end
此外,如果命令的第一行为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
define li
x/10i $pc
end
document li
list machine instruction
end
(gdb) source <filename>
(gdb) start
(gdb) help li # list machine instruction
(gdb) li
peda插件
peda插件会在程序运行时实时显示寄存器值、汇编代码、栈等信息,更加方便:
# 安装gdb插件peda
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
GDB进阶操作
// sum.c
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#define MAX (1UL << 20)
typedef unsigned long long u64;
typedef unsigned int u32;
u32 max_addend = MAX;
u64 sum_till_MAX(u32 n) {
u64 sum;
n++;
sum = n;
if (n < max_addend) {
sum += sum_till_MAX(n);
}
return sum;
}
int main(int argc, char** argv) {
u64 sum = 0;
if ((argc == 2) && isdigit(*(argv[1]))) {
max_addend = strtoul(argv[1], NULL, 0);
}
if (max_addend > MAX || max_addend == 0) {
fprintf(stderr, "Invalid number is specified\n");
return 1;
}
sum = sum_till_MAX(0);
printf("sum(0..%u) = %llu\n", max_addend, sum);
return 0;
}
使用bt
命令查看栈帧情况:
gcc -o sum -g sum.c
gdb sum
(gdb) set args 10
(gdb) b 15
(gdb) r
(gdb) c 4
(gdb) bt
打印如下:
#0 sum_till_MAX (n=0x5) at sum.c:15
, argc=0x2, argv=0x7fffffffe408, init=, fini=,
#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
rtld_fini=, stack_end=0x7fffffffe3f8) at …/csu/libc-start.c:310
#7 0x000055555540069a in _start ()
用frame
命令可以查看当前所在的栈帧信息,用frame N
可以跳转到N
对应的栈帧,此时可以打印该栈帧下的变量:
(gdb) frame
# #0 sum_till_MAX (n=0x5) at sum.c:15
# 15 sum = n;
(gdb) p n
# $1 = 0x5
(gdb) frame 3
# #3 0x00005555554007a5 in sum_till_MAX (n=0x2) at sum.c:17
# 17 sum += sum_till_MAX(n);
(gdb) p n
# $2 = 0x2
此外,up
命令可以选择上一层的帧,down
命令可以选择下一层的帧。使用info frame N
可以显示更为详细的栈帧信息。
调试栈溢出
上述的sum.c
程序在不指定参数的情况下执行,会调用2^20次sum_till_MAX
函数,每次调用都会生成栈帧,消耗栈空间,从而发生了栈溢出:
gdb sum
(gdb) r
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
p $sp # $1 = (void *) 0x7fffff7fefe0
可以用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)
gdb sum -c <core-file>
…
[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
(gdb) info target
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
, argc=0x1, argv=0x7ffdec851268, init=, fini=,
#174686 0x00007f4114bbbc87 in __libc_start_main (main=0x563aeac007af
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
ulimit -Ss 81920
./sum # sum(0..1048576) = 549756338176
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK