11

DEBUG-HACKS 内核转储与GDB调试

 2 years ago
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.
neoserver,ios ssh client

旭穹の陋室

DEBUG-HACKS 内核转储与GDB调试

发表于2022-10-11|更新于2022-12-21|语言
阅读量:1

获取用户进程的内核转储

获取内核转储(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,别名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

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

如果要逐条执行汇编指令,可以分别使用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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK