GDB 的使用
GDB 是C/C++程序的调试器。
编译时需要添加debug信息,添加 -g、-g3、-ggdb、 -ggdb3 参数。(带3的会携带宏的信息)
基本使用
启动
gdb <可执行文件>,会进入gdb 交互式环境,可以加-tui参数进入tui环境。此时要调试的程序并没有执行,需要用run命令执行。
gdb attach <pid>,会直接attach到某个进程。也可以先用gdb启动交互式环境再attach <pid>。
可以通过tui enable/disable进入或退出tui环境。
断点
info b:查看当前设置了哪些断点。b <断点位置> <断点条件>b <函数名称>:在函数入口设置断点。b <行号>:在当前文件的第n行设置断点。b <文件名:行号>b <断点位置> if x == 1:条件断点。
delete <断点序号>...:删除断点,序号通过info查看。enable/disable <断点序号>...:启用或禁用断点。
进入断点后:
调试流程控制:
next/n:下一行,遇到函数调用不会进入step/s:下一步,遇到函数调用会进入finish:完成当前函数,返回到调用当前函数的那行until/u:继续执行到某一行停下。(如果目的点是函数外的话,会先到函数调用处停下)continue/c:跳过此断点。
信息获取:
print/p:p x打印变量值,p x=1修改变量值。变量可以是寄存器,使用$寄存器名称访问。backtrace/bt:打印调用栈list/l:查看当前行附近代码(l .列出当前行附近10行;l列出上次列出最后一行的后10行)info/i: 有许多子命令i args:打印函数参数i thread:查看线程,*代表当前线程
打印内存信息
命令格式: x/<num><type><size> <address>,含义是从起始地址address开始读size大小的内存,以type的类型打印值。重复num次,num>0则每次地址递增,num<0则每次地址递减。
num: 可以不指定,默认是1type:a: addresst: binaryx: 十六进制格式,z: 十六进制,zero padded on the left.d: 十进制格式,ud: unsigned 十进制f: floatc: 字符s: 字符串i: instruction 汇编指令
size:b: byteh: halfwordw: wordgiant: giant, 8 bytes
汇编
打印汇编指令:disas [/r|/m|/s] (<函数位置>|<开始地址>[, <结束地址>]):
- 位置格式是:
[文件名称::]<函数名>,默认是当前文件。 /r代表会展示指令的16进制形式。/m:代表会打印对应的源码,并且指令顺序是按源码行的顺序来的。(regardless of any optimization that is present。This modifier hasn’t proved useful in practice and is deprecated in favor of /s.)/s:代表会打印对应的源码,但是顺序是按指令本身的顺序来的。
参数
可以通过 set <参数名称> <value> 设置参数。使用 show <参数名称>查看参数值。
多线程调试 scheduler-locking
进入断点时这个值才能修改。
off:默认值, 在断点处继续往下运行时所有线程均可执行。(注意:当进入断点时所有线程都是不可执行的)on:在断点处继续往下运行时仅当前被调试线程可执行。step:用的较少,这里不展开。
测试用代码(gcc a.c -g -lpthread -o a.out):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
static int a = 0;
static int b = 0;
void *runnable1(void *args) {
while (1) {
a++;
sleep(1);
}
}
void *runnable2(void *args) {
while (1) {
b++;
sleep(1);
}
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, runnable1, NULL);
pthread_create(&t2, NULL, runnable2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}测试过程:
- 给
a++那行添加断点,运行。 - 进入断点,查看当前的参数确认是
off。打印一下a和b的值,假设是a = 0, b = 1。然后继续运行,又进入断点。此时打印值可能会是a = 1, b = 3,说明继续执行时两个线程都在执行。 - 然后再设置参数为
on,继续运行。此时打印值会是a = 2, b = 3,说明这次只有a线程执行了。
多进程调试
detach-on-fork: 默认on,当设置为off时,父子进程都会attach(即之前提到的进入断点)follow-fork-mode: 当detach-on-fork为on时,执行进程fork时,由这个参数控制attach到parent还是child。默认是parent。i inferiors:查看进程inferior <序号>切换调试的进程
测试用例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <unistd.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <stdlib.h>
pid_t gettid() {
return syscall(__NR_gettid);
}
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
exit(1);
} else if (pid == 0) {
printf("in child, pid: %d, tid:%d\n", getpid(), gettid());
exit(0);
} else {
printf("in parent, pid: %d, tid:%d\n", getpid(), gettid());
}
return 0;
}- 首先在fork的后一行添加断点。运行一次,此时会先打印出
in child。说明目前调试的是父进程,c继续运行。 - 修改
follow-fork-mode为child。运行一次,此时会先打印出in parent。说明目前调试的是子进程,c继续运行。 - 再修改
detach-on-fork为off,运行一次。此时会先进入parent,但是child已经创建了并且也是attach的。使用i inferiors查看当前attach的进程,此时会有两个进程。通过inferior <子进程序号>可以切换到子进程。会发现子进程并不是停留在fork的下一行,而是在fork函数内部。
原理
首先需要了解一下ptrace系统调用。
ptrace系统调用
函数签名:long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data)
- request:指定执行的动作
- PTRACE_TRACEME: 让父进程追踪当前进程
- PTRACE_PEEKTEXT、PTRACE_PEEKDATA:读取进程内存
- PTRACE_POKETEXT、PTRACE_POKEDATA:修改进程内存
- PTRACE_CONT:让进程继续执行
- PTRACE_GETSIGINFO: 获取进程的signal信息
- PTRACE_SINGLESTEP: 单步执行一条汇编指令
- PTRACE_ATTACH: 当前进程动态地attach到目标进程
- pid:被追踪的进程的进程号
- addr:这个参数一般是地址值,指示要读取或写入的进程内存地址。
- data:根据 request 的类型,这个参数可能用于提供数据,例如写入目标进程的内存,或者用于接收某些类型的输出。
gdb 通过预先指定elf文件进行调试:
- gdb 运行被调试的程序时是通过fork生成子进程,然后子进程调用exec替换运行程序为被调试的程序。
- 随后子进程会调用ptrace PTRACE_TRACEME 让gdb能够trace被调试程序。
断点的原理:
- 当设置一个断点时会将断点所在的汇编指令替换为
INT 3指令,当被调试的进程执行到这个命令时,会收到中断信号SIGTRAP被暂停。而父进程先前执行的waitpid会返回,通过PTRACE_GETSIGINFO能够拿到是否是由SIGTRAP。如果是,说明进入了断点,此时响应用户的操作如获取被调试进程的相关信息。并且设置断点的同时gdb会将原指令保存起来,当继续运行时则将这个指令写回。读取指令需要用到PTRACE_PEEKDATA、而写入用到PTRACE_POKEDATA。
DWARF
它是gcc编译时记录在elf文件中的用于调试的信息,调试器能利用这些数据找到指令与源码行的关系。
dwarfdump elf文件 可以查看elf文件中的dwarf信息。
另外dwarf信息可以存放在独立的文件中,调试的时候引入,这样就不用增大elf文件的大小。