获取用户进程的内核转储

获取内核转储(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 # 打印设置好的断点

其中,break *函数名,断点会设置在汇编语言层次的函数开头,不加*则会设置到地址偏后一点的源代码级别的开头。

运行

run命令开始运行,简写为r,会执行到断点处暂停运行。start命令会执行到main函数开始处暂停运行。

显示栈帧

backtrace命令可以在遇到断点暂停时显示栈帧,简写为bt,别名whereinfo 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

显示x86_64下的浮点寄存器(xmm寄存器):

p $xmm0.v4_float[0]				# xmm0寄存器按4xfloat的第一个元素
p $xmm1.v2_double[0] # xmm1寄存器按2xdouble的第一个元素

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 # 反汇编从开始地址到结束地址之间的部分

显示栈的内容

x/10g $rsp			# g: gaint word 双字,显示10个双字的内容
# 将栈顶偏移8个字节的地方的值(unsigned long=8个字节)取出,看作float指针,并打印其所指值
printf "%.2f\n", *(float*)(*(unsigned long*)($rsp+0x8))
# 将栈顶偏移16个字节的地方的值(unsigned long=8个字节)取出,看作char指针,并打印其所指值(即字符串)
p (char*)(*(unsigned long*)($rsp+0x10))

单步执行

next命令执行源代码中的一行,简写为n

step命令执行到函数内部,简写为s

如果要逐条执行汇编指令,可以分别使用nextistepi,简写为nisi

继续执行

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 单步执行
print 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
#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
# #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 filesinfo 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便是,可以通过btinfo 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
ulimit -Ss 81920
./sum # sum(0..1048576) = 549756338176