DEBUG HACKS 学习笔记

作者:edelweiss 日期:2020年3月3日
参考书目:ISBN 978-7-121-14048-8 DEBUG HACKS 中文版


日本人放置蚊香用的“驱蚊猪”

1. 热身准备

1 调试是什么
2 Debug hacks 的地图
3 调试的心得

2. 调试前的必知必会

4 获取进程的内核转储

获取内核转储 (core dump) 的最大好处是,它能保存问题发生时的状态。只要有问题发生时程序的可执行文件和内核转储,就可以知道进程当时的状态。比如,在不清楚 bug 复现方法的情况下,或是 bug 极其罕见,又或者 bug 只在特定机器上发生的情况等,只要获取内核转储,那么即使手头没有复现环境,也能够进行调试。

启用内核转储
大多数 Linux 发行版默认情况下关闭了内核转储功能。用 ulimit 命令可以查看当前的内核转储功能是否有效。

-c 选项 表示内核转储文件的大小限制。上例中限制为 0 ,表示内核转储无效。按照以下方式执行 ulimit 命令,即可开启内核转储。

hanhan@ubuntu:~/DEBUG$ ulimit -c
0
hanhan@ubuntu:~/DEBUG$ ulimit -c unlimited #这个命令的意思是不限制内核转储的空间大小
hanhan@ubuntu:~/DEBUG$ ulimit -c
unlimited

在设置成无限制之后,发生问题时进程的内存就可以全部转储到内核转储文件中。在调试大量消耗内存的进程时,可能会希望设置内核转储文件的上限,这是直接在参数中指定大小即可。例如,下面的命令设置上限为 1 GB。

$ ulimit -c 1073741824

样例程序
segfault.c

#include <stdio.h>

int main(void)
{
    int *a = NULL;
    *a = 0x1;
    return 0;
}

使用 gcc -g 编译并加入调试信息。

dongyu@During:~/DEBUG$ gcc -g segfault.c 

在开启内核转储之后,执行上面的程序,确认能否生成内核转储。

dongyu@During:~/DEBUG$ ./a.out 
Segmentation fault (core dumped)
dongyu@During:~/DEBUG$ ls
a.out  core  segfault.c

当前目录下已经生成了 core 文件,要用 GDB 调试生成的内核转储文件,应当使用以下方式启动 GDB。

dongyu@During:~/DEBUG$ gdb -c core ./a.out
...
[New LWP 19862]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000056464918d60a in main () at segfault.c:6
6       *a = 0x1;
(gdb) 

segfault.c 的第六行收到了 SIGSEGV 信号。用 GDB list 命令可以查看附近的源代码。

(gdb) l 5
1   #include <stdio.h>
2   
3   int main(void)
4   {
5       int *a = NULL;
6       *a = 0x1;
7       return 0;
8   }
(gdb) 

由于指针变量的值为 NULL ,所以在解引用空指针的时候收到了信号。
这是个最简单的例子,但调试复杂程序时,从内核转储入手十分有效。程序越复杂,就越难判断接收信号的时候程序作了什么操作。此外,如果 bug 很难复现,那么单靠跟踪源代码会很难确定原因。在这些情况下,内核转储能将问题发生时的状态原原本本地保存下来,有助于确定 bug 原因。

在专用目录中生成内核转储
在使用大型文件系统时,会希望将内核转储放在固定的位置。默认情况下会在当前目录中生成,但可能难以弄清文件到低在哪里生成。此外,大量生成的内核转储文件可能会给系统的磁盘空间造成压力。这种情况下可以准备内核转储专用分区,并在该分区中设置内核转储文件,这样就方便很多。转储保存位置的完整路径可以通过 sysctl 变量 kernel.core_pattern 设置。假设在 /etc/sysctl.conf 中这样设置。
/etc/sysctl.conf

  1 # DEBUG HACKS 
  2 kernel.core_pattern = /var/core/%t-%e-%p-%c.core
  3 kernel.core_uses_pid = 0  
dongyu@During:~/DEBUG$ sudo sysctl -p

在该状态下 sudo 执行刚才的 a.out 程序,就会在 /var/core下生成内核转储文件。

dongyu@During:~/DEBUG$ sudo ./a.out 
Segmentation fault
dongyu@During:~/DEBUG$ ls /var/core/
1583486733-a.out-20519-18446744073709551615.core
dongyu@During:~/DEBUG$ sudo ./a.out 
Segmentation fault
dongyu@During:~/DEBUG$ ls
a.out  segfault.c
dongyu@During:~/DEBUG$ ls /var/core/
1583486733-a.out-20519-18446744073709551615.core
dongyu@During:~/DEBUG$ 

kernel.core_pattern 中可以设置的格式符

格式符 说明
%% %本身
%p 被转储的进程 ID (PID)
%u 被转储的进程的真实用户 ID (read UID)
%g 被转储进程的真实组 ID (real GID)
%s 引发转储的信号编号
%t 转储时刻 (从 1970年 1 月 1 日 0:00 开始的秒数)
%h 主机名 (同 uname (2) 返回的 nodename)
%e 可执行文件名
%c 转储文件的大小上限 (内核版本 2.6.24 以后可以使用)

上例中设置了 kernel.core_uses_pid=0, 是因为我们改变了文件中 PID 的位置。如果设置该值为 1,文件名末尾就会添加.PID。

使用用户编写的程序自动压缩内核转储文件
启动整个系统的内核转储功能
利用内核转储掩码排除共享内存

5 GDB 的基本使用方法一

GDB 的基本使用流程
(1) 带着调试选项编译、构建调试对象
(2) 启动调试器 (GDB)
(2-1) 设置断点
(2-2)显示栈帧
(2-3)显示值
(2-4)继续执行

准备
uname.c

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 
  4 
  5 int main(int argc, char* argv[])
  6 {
  7     int a = 10;
  8     int b = 20;
  9     int c = a + b;
 10 
 11     printf("c = %d\n", c);
 12 
 13     for (int i = 1; i < argc; i++ )
 14     {
 15         printf("%s\n", argv[i]);
 16     }                                                                                     
 17 
 18     return 0;
 19 }
~        

通过 gcc 的 -g 选项生成调试信息

$ gcc -wall -O2 -g 源文件

如果用 configure 脚本生成 makefile, 可以这样用。
$ ./configure CFLAGS="-Wall -02 -g"
本例实际操作

dongyu@During:~/DEBUG$ gcc -o uname -Wall -g -O2 uname.c 
dongyu@During:~/DEBUG$ ls
uname  uname.c

启动

dongyu@During:~/DEBUG$ gdb uname 
...
(gdb) 

设置断点
可以在函数名行号上设置断点。程序运行后,到达断点就会自动暂停运行。此时可以查看该时刻的变量值、显示栈帧、重新设置断点或重新运行等。断点命令 (break) 可以简写为 b 。

格式:
break 函数名
break 行号
break 文件名:行号
break 文件名:函数名
break + 偏移量
break *地址

(gdb) break main 
Breakpoint 1 at 0x5a0: file uname.c, line 6.
(gdb) break 9
Breakpoint 2 at 0x5c1: file uname.c, line 9.

设置好的断点可以通过 info break 确认

(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000000005a0 in main at uname.c:6
2       breakpoint     keep y   0x00000000000005c1 in main at uname.c:9
(gdb) 

运行
用 run 命令开始运行。不加参数只执行 run, 就会执行到设置了断点的位置暂停下来。可以简写为 run 。
在 run 命令后面加上可执行程序的参数

(gdb) run hello world   #参数 1 hello 参数 2 world

经常用到的一个操作是在 main() 上设置断点,然后执行到 main()函数。start 命令能达到同样的效果。

显示栈帧
backtrace 命令可以在遇到断点而暂停时显示栈帧。该命令简写为 bt 。此外,backtrace 的别名还有 where 和 info stack (简写为 info s)。

格式:
backtrace:显示所有栈帧。
backtrace N:只显示开头 N 个栈帧。
backtrace full N :只显示最后 N 个 栈帧。

显示变量
print 命令可以显示变量。print 可以简写为 p。

(gdb) print argc
$1 = 3
(gdb) print argv
$2 = (char **) 0x7fffffffe578
(gdb) print argv
$3 = (char **) 0x7fffffffe578
(gdb) print argv[0]
$4 = 0x7fffffffe7bf "/home/dongyu/DEBUG/uname"
(gdb) print argv[1]
$5 = 0x7fffffffe7d8 "hello"
(gdb) 

显示寄存器
info registers 可以显示寄存器,简写为 info reg。
在寄存器名之前添加 $,即可显示各个寄存器的内容。

格式:
p/格式 变量名

显示寄存器可使用的格式

格式 说明
x 显示为十六进制数
d 显示为十进制数
u 显示为无符号十进制数
o 显示为八进制数
t 显示为二进制数, t 的由来是 two
a 地址
c 显示为字符 (ASCII)
f 浮点小数
s 显示为字符串
i 显示为机器语言 (仅在显示内存的 X 命令中可用)

单步执行
单步执行的意思是根据源代码一行一行地执行。

执行源代码中一行的命令为 next (简写为 n)。执行时如果遇到函数调用,可能想执行到函数内部,此时可以使用 step (简写为 p ) 命令。

next 命令和 step 命令都是执行源代码中的一行。如果要逐条执行汇编指令,可以分别使用 nexti 和 stepi 命令。

nexti 命令不会进入函数内部执行,而 stepi 命令会。

继续运行
调试时,可以使用 continue (简写为c) 命令继续运行程序。程序会在遇到断点后再次暂停运行。如果没有遇到断点,就会一直运行到结束。

格式:
continue
continue 次数

指定次数可以忽略断点。例如,continue 5 则 5 次遇到断点不停止,第6次遇到断点是才暂停执行。

被调试的程序通常为以下几种情况之一:

  1. 可以正常结束。
  2. 由于某种原因异常结束 (发生内核转储、非法访问等)
  3. 无法结束 (死循环等)
  4. 被挂起 (停止响应、死锁等)
    除了正常结束之外,其他情况都需要从头开始继续执行,以寻找问题的原因 (调试)。

监视点
大型软件或大量使用指针的程序中,很难弄清楚变量在什么地方被改变。要想找到变量在何处被改变,可以使用 watch 命令 (监视点, watchpoint)。

格式:
watch <表达式>
<表达式> 发生变化时暂停运行。
此处 <表达式> 的意思是常量或变量等。

格式:
awatch <表达式>
<表达式> 被访问、改变时暂停运行。

格式:
reatch <表达式>
<表达式> 被访问时暂停运行。

删除断点和监视点
用 delete (简写为d)命令删除断点和监视断点。

格式:
delete <编号>
删除 <编号> 指示的断点或监视点。

其他断点
硬件断点 (hbreak), 适用于 ROM 空间等无法修改的内存区域中的程序。在有些架构中无法使用。
临时断点 (tbreak)和临时硬件断点 (thbreak), 与断点 (硬件断点)相同,都会在运行到该处时暂停,不同之处就是临时断点 (临时硬件断点)会在此时被删除,所以在只需要停止一次时用起来很方便。

遗憾的是并没有临时监视点。

改变变量的值

格式:
set variable <变量>=<表达式>

该功能可以在运行时随意修改变量的值,因此无需修改源代码就能确认各种值的情况。

生成内核转储文件
使用 generate-core-file 可将调试中的进程生成内核转储文件。

有了内核转储文件和调试对象,以后就能查看生成转储文件时的运行历史 (寄存器值、内存值等)。

6 GDB 的基本使用方法二

attach 到进程
要调试守护进程 (daemon process )等已经启动的进程,或者调试陷入死循环而无法返回控制台的进程时,可以使用 attach 命令。

格式:
attach pid
执行这一命令就可以 attach 到进程 ID 为 pid的进程上。

条件断点
有一种断点只在特定条件下中断。

格式:
break 断点 if 条件
这条命令将测试给定的条件,如果为真则暂停运行。

删除断点触发条件

格式:
condition 断点编号

添加断点触发条件

格式:
condition 断点编号 条件

反复执行

格式:
igore 断点编号 次数
在编号指定的断点、监视点(watchpoint)或捕获点(catchpoint)忽略指定的次数。

continue 命令与 ignore 命令一样,也可以指定次数,达到指定次数前,执行到断点时不暂停,二者的意义是相同的。

删除断点和禁用断点
用 clear 命令删除已定义的断点。
只想临时禁用断点的话,可以使用disable 命令。将禁用的断点重新启用,则可使用 enable 命令。

断点命令
断点命令 (commands)可以定义在断点中断后自动执行的命令。

7 GDB 的基本使用方法三

值的历史
通过 print 命令显示过的值会记录在内部的值历史中。这些值可以在其他表达式中使用。

变量 说明
$ 值历史的最后一个值
$n 值历史的第 n 个值
$$ 值历史的倒数第 2 个值
$$n 值历史的倒数第 n 个值
$_ x 命令显示过的最后的地址
$__ x 命令显示过的最后的地址的值
$_exitcode 调试中的程序的返回代码
$bpnum 最后设置的断点编号

变量
可以随意定义变量。变量以$开头,有英文字母和数字组成。

命令历史
可以将命令历史保存到文件中。保存命令历史后,就能在其他调试会话中重复利用这些命令(通过箭头查找以前的命令),十分方便。默认命令历史文件位于./.gdb_history。

初始化文件
Linux 环境下的初始化文件为.gdbinit。如果存在.gdbinit 文件,GDB 就会在启动之前将其作为命令文件运行。

命令定义
利用 define 命令可以自定义命令,还可以使用 document 命令给自定义命令添加说明。用 "help 命令名" 可以查看定义的命令。

8 intel 架构的基本知识

作为调试的基本知识,这里简单介绍一下 CPU 架构。
字节序
所谓 Endian,就是多字节数据在内存中的排列方式。
例如 0x12345678 这个数据,地位数据排在内存低地址,高位数据排在内存高地址,这就叫做小端。

0003 0002 0001 0000
0x12 0x34 0x56 0x78

32 位环境中的寄存器
通用寄存器有 8 种,分别是 EAX、EBX、ECX、ESI、EDI、EBP、ESP,用于逻辑运算、数学运算、地址计算、内存指针等。

32位环境中运行基本程序的寄存器

ESP 寄存器用于保存栈指针
某些命令使用特定的寄存器。例如,字符串将 EXC、ESI 和 EDI 寄存器作为操作数使用。

通用寄存器的主要用途

寄存器 用途
EAX 操作数的运算、结果
EBX 指向 DS 端中数据的指针 (主要端寄存器的用途见表 2-8)
ECX 字符串操作或循环的计数器
EDX 输入输出指针
ESI 指向 DS 寄存器所指示的段中某个数据的指针,或者是字符串操作中字符串的复制源 (source)
EDI 指向 ES 寄存器所指示的端中某个数据的指针,或者是字符串操作中字符串的复制目的地 (destination)
ESP 栈指针 (SS 段)
EBP 指向栈上数据的指针 (SS 段)
CS 代码段
DS 数据段
SS 堆栈段
ES 数据段
FS 数据段
GS 数据段

程序代码放在代码段中,数据放在数据段中,程序所用的栈放在堆栈段中。

但是,通用寄存器的用途并不限于上面所述,也可以用于一般用途,所以上表只能作为参考。寄存器的结构见下图。


通用系统

EFLAGS 寄存器中包含状态标志 (status flag)、控制标志 (confrol flag)、系统标志 (system flag) 等。


EFLAGS 寄存器

EIP (Instruction Pointer)寄存器是 32 位指令指针
其他寄存器有控制寄存器 (CR0~CR4)、GDTR、IDTR、TR、LDTR、调试寄存器 ( DR0/DR1/DR2/DR3/DR6/DR7 )、内存类型范围寄存器 MTPP、MSR(Model Specific Register)寄存器、机器检查寄存器 (Machine Check Register)、性能监视计数器 (Performance Monitoring Counter)等。


FPU 寄存器
MMX 寄存器和 XMM 寄存器

64 位环境中的寄存器

64 位环境中的寄存器

地址
CPU 可以通过内存总线访问到的地址称为物理地址。

平坦模型 (flat model)中,内存可看做单一、平坦的连续地址空间,称为线性地址空间。Linux 采用这种内存模型。


平坦模型

分段式内存模型 (segment model) 中,将内存看做被称为“段”(segment)的独立地址空间的集合。通过段选择器和偏移量组成的逻辑地址来访问段内地址。首先用段选择器识别出要访问的段,然后通过偏移量找到该段的地址空间中的内存 (图 2-10)。32位模式下最多能指定 16383 个段,各段的最大大小为 2的32次方 字节。


分段模型

64 位模式采用了平坦模型,因此可以使用 64 位线性地址。不能使用分段式内存模型。

数据类型
基本数据类型包括字节 (8 比特)、字(16 比特)、双字(32 比特)、四字(64比特)、和双四字(128比特)。

9 调试时必须的栈知识

栈 (strack) 是程序存放数据的内存区域之一,其特征是 LIFO (Last In First Out)式数据结构,即后放进去的数据先被取出。向栈中存储数据的操作称为 PUSH, 从栈中取出数据的操作称为 POP 。在保存动态分配的自动变量时需要使用栈。此外在函数调用时,栈还用于传递函数参数,以及用于保存返回地址和返回值。

示例程序:这个程序将命令行参数传递过来的数字作为终值,计算 0 到终值之前所有正数的总和。

#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..%lu) = %llu\n", max_addend, sum);
    return 0;
}

10 函数调用时的参数传递方法 X86_64
11 函数调用时的参数传递方法 i386
12 函数调用时的参数传递方法 C++
13 怎样学习汇编语言
14 从汇编代码查找相应的源代码

3. 内核调试的准备

15 Oops 信息的解读方法
16 使用 minicom 进行串口连接
17 通过网络获取内核消息
18 使用 SysRq 键调试
19 使用 diskdump 获取内核崩溃转储
20 使用 kdump 获取内核崩溃转储
21 crash 命令的使用方法
22 死机时利用 IPMI watchdog timer 获取崩溃转储
23 用 NMI watchdog 在死机时获取崩溃转储
24 内核独有的汇编指令之一
25 内核独有的汇编指令之二

4. 应用程序调试实践

26 发生SIGSEGV, 应用程序异常停止
27 backtrace 无法正确显示
28 数组非法访问导致内存破坏
29 利用监视点检测非法内存访问
30 mallco() 和 free() 发生故障
31 应用程序停止响应 (死锁篇)
32 应用程序停止响应 (死循环篇)

5. 实践内核调试

33 kernel panic (空指针引用篇)
34 kernel panic (链表破坏篇)
35 kernel panic
36 内核停止响应(死循环篇)
37 内核停止响应(自旋锁篇之一)
38 内核停止响应(信号量篇之二)
39 内核停止响应(信号量篇)
40 实时进程停止响应
41 运行缓慢的故障
42 CPU 负载过高的故障

6. 高手们的调试技术

43 使用 strace 寻找故障原因的线索
44 objdump 的方便选项
45 Valfrind 的使用方法(基本篇)
46 Valfrind 的使用方法 (实践篇)
48 使用 jprobes 查看内核内部的信息
49 使用 kprobes 获取内核内部任意位置的信息
50 使用 kprobes 在内核内部任意位置通过变量名获取信息
51 使用 KAHO 获取被编译器优化掉的变量的值
52 使用 systemtap 调试运行中的内核之一
53 使用 systemtap 调试运行中的内核之二
54 /proc/meminfo 中的宝藏
55 用/proc/<PID>/mem 快速读取进程的内存内容
56 OOM Killer 的行为和原理
57 错误注入
58 利用错误注入发现Linux 内核的潜在bug
59 Linux 内核的 init 节
60 解决性能问题
61 利用VMware Vprobe 获取信息
62 利用 Xen 获取内存转储
63 理解用 GOT/PLT 调用函数的原理
64 调试 initramfs 镜像
65 使用 RT Watchdog 检测失去响应的实时进程
66 查看手头的 x86 机器是否支持 64 位模式
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容