GDB简介
GDB(GNU Debugger)是Linux下一款C/C++程序调试工具,通过在命令行中执行相应的命令实现程序的调试,使用GDB时只需要在shell中输入gdb
命令或gdb filename
(filename为可执行程序文件名)即可进入GDB调试环境。
GDB主要有以下功能:
- 设置断点
- 单步调试
- 查看变量的值
- 动态改变程序的执行环境
- 分析崩溃程序产生的core文件
GDB常用命令
命令 | 简写 | 含义 |
---|---|---|
file |
- | 装入待调试的可执行文件 |
run | r | 执行程序(至结束) |
start | - | 开始调试(至main开始处暂停) |
step | s | 执行一条程序,若为函数则进入内部执行 |
next | n | 执行一条程序,不进入函数内部 |
continue | c | 连续运行 |
finish | - | 运行到当前函数返回 |
kill | k | 终止正在调试的程序 |
list | l | 列出源代码的一部分(10行) |
print |
p |
打印变量的值 |
info locals | i locals | 查看当前栈帧的局部变量 |
backtrace | bt | 查看函数调用栈帧编号 |
frame |
f |
选择栈帧(再看局部变量) |
display |
- | 每次自动显示跟踪的变量的值 |
undisplay |
- | 取消跟踪 |
break |
b | 设置(调试)断点 |
delete breakpoints |
d breakpoints |
删除断点,不加行号则删除所有 |
disable breakpoints |
- | 屏蔽断点 |
enable breakpoints |
- | 启用断点 |
info breakpoints | i breakpoints | 显示所有断点 |
break 9 if sum != 0 | - | 根据条件设置断点(sum不等于0时,第9行设断点) |
set var sum=0 | - | 修改变量的值(使sum变量的值为0) |
watch |
- | 监视一个变量的值 |
examine <...> | - | 查看内存中的地址 |
jump |
j | 跳转执行 |
signal <...> | - | 产生信号量 |
return | - | 强制函数返回 |
call |
- | 强制调用函数 |
make <...> | - | 不退出gdb下重新产生可执行文件 |
shell <...> | - | 不退出gdb下执行shell命令 |
quit | q | 退出gdb环境 |
判断文件是否携带调试信息
1.根据提示信息
要调试C/C++的程序,首先在编译时,要使用gdb调试程序,在使用gcc编译源代码时必须加上“-g”参数。保留调试信息,否则不能使用GDB进行调试。
有一种情况,有一个编译好的二进制文件,你不确定是不是带有-g参数,带有GDB调试,这个时候你可以使用如下的命令验证:
如果没有调试信息,则会出现:
Reading symbols from /home/minger/share/tencent/gdb/main…(no debugging symbols found)…done.
/home/minger/share/tencent/gdb/main是程序的路径。
如果带有调试功能,下面会提示:
Reading symbols from /home/minger/share/tencent/gdb/main…done.
说明可以进行GDB调试。
2.readelf
还有使用命令readlef查看可执行文件是否带有调试功能
调用GDB调试器
1.gdb、file
2.gdb - pid
3.gdb attach pid
gdb attach 41863
set height 0
handle SIGUSR2 SIG43 SIG39 SIGCONT noprint nostop
info line *(preprocess_ipv4_addr+0x36)
b preprocess_ip_addr
info locals
call http_perf_add_metirc_by_msg+0x63·
p g_worker_wall_ctx.seq_num=18·
call http_perf_add_metirc(0, 0, 99)·
call get_node_metirc_value(0,0)·
get_node_metirc_value+0xdd·
$2 = (uint64_t *) 0x7efbd07eb400
p PERF_STAT_SHM_KEY=0
4.gdb obj
run/start命令:启动程序
启动命令(run、start)
概述
根据不同场景的需要,GDB 调试器提供了多种方式来启动目标程序,其中最常用的就是 run 指令,其次为 start 指令。也就是说,run 和 start 指令都可以用来在 GDB 调试器中启动程序,它们之间的区别是:
- 默认情况下,run 指令会一直执行程序,直到执行结束。如果程序中手动设置有断点,则 run 指令会执行程序至第一个断点处;
- start 指令会执行程序至 main() 主函数的起始位置,即在 main() 函数的第一行语句处停止执行(该行代码尚未执行)。
可以这样理解,使用 start 指令启动程序,完全等价于先在 main() 主函数起始位置设置一个断点,然后再使用 run 指令启动程序。另外,程序执行过程中使用 run 或者 start 指令,表示的是重新启动程序。
在进行 run 或者 start 指令启动目标程序之前,还可能需要做一些必要的准备工作,大致包括以下几个方面:
- 如果启动 GDB 调试器时未指定要调试的目标程序,或者由于各种原因 GDB 调试器并为找到所指定的目标程序,这种情况下就需要再次手动指定;
- 有些 C 或者 C++ 程序的执行,需要接收一些参数(程序中用 argc 和 argv[] 接收);
- 目标程序在执行过程中,可能需要临时设置 PATH 环境变量;
- 默认情况下,GDB 调试器将启动时所在的目录作为工作目录,但很多情况下,该目录并不符合要求,需要在启动程序手动为 GDB 调试器指定工作目录。
- 默认情况下,GDB 调试器启动程序后,会接收键盘临时输入的数据,并将执行结果会打印在屏幕上。但 GDB 调试器允许对执行程序的输入和输出进行重定向,使其从文件或其它终端接收输入,或者将执行结果输出到文件或其它终端。
详解
假设使用 GDB 调试器调试如下程序:
//存储路径为 /tmp/demo/main.c
//其生成的可执行文件为 main.exe,位于同一路径下
#include<stdio.h>
int main(int argc,char* argv[])
{FILE * fp;if((fp = fopen(argv[1],"r")) == NULL){printf("file open fail");}else{printf("file open true");}return 0;
}
要知道,命令行窗口打开时默认位于 ~ (表示当前用户的主目录)路径下,假设我们就位于此目录中使用 gdb 命令启动 GDB 调试器,则在执行 main.exe 之前,有以下几项操作要做:
1、首先,对于已启动的 GDB 调试器,我们可以先通过 l (小写的 L)指令验证其是否已找到指定的目标程序文件:
[root@bogon ~]# gdb -q <-- 使用 -q 选项,可以省略不必要的输出信息
(gdb) l
No symbol table is loaded. Use the "file" command.
可以看到,对于找不到目标程序文件的 GDB 调试器,l 指令的执行结果显示“无法加载符号表”。这种情况下,我们就必须手动为其指定要调试的目标程序,例如:
(gdb) file /tmp/demo/main.exe
Reading symbols from /tmp/demo/main.exe...
(gdb) l
1 #include<stdio.h>
2 int main(int argc,char* argv[])
3 {
4 FILE * fp;
5 if((fp = fopen(argv[1],"r")) == NULL){
6 printf("file open fail");
7 }
8 else{
9 printf("file open true");
10 }
(gdb)
11 return 0;
12 }
(gdb)
可以看到,通过借助 file 命令,则无需重启 GDB 调试器也能指定要调试的目标程序文件。
除了 file 指令外,GDB 调试器还提供有其它的指定目标调试文件的指令,感兴趣的读者可千万 GDB 官网做详细了解,后续章节在用到时也会做详细的讲解。
2.通过分析 main.c 中程序的逻辑不难发现,要想其正确执行,必须在执行程序的同时给它传递一个目标文件的文件名。
总的来说,为 GDB 调试器指定的目标程序传递参数,常用的方法有 3 种:
1)启动 GDB 调试器时,可以在指定目标调试程序的同时,使用 --args 选项指定需要传递给该程序的数据。仍以 main.exe 程序为例:
[root@bogon demo]# gdb --args main.exe a.txt
整个指令的意思是:启动 GDB 调试器调试 main.exe 程序,并为其传递 "a.txt" 这个字符串(其会被 argv[] 字符串数组接收)。
2)GDB 调试器启动后,可以借助 set args 命令指定目标调试程序启动所需要的数据。仍以 main.exe 为例:
(gdb) set args a.txt
3)除此之外,还可以使用 run 或者 start 启动目标程序时,指定其所需要的数据。例如:
(gdb) run a.txt
(gdb) start a.txt
3.要知道,对于调试 /tmp/demo/ 路径下的 main.exe 文件,将其作为 GDB 调试器的工作目录,一定程度上可以提高我们的调试效率。反之,如果 GDB 调试器的工作目录和目标调试文件不在同一目录,则很多时候需要额外指明要操作文件的存储路径(例如第 1) 种情况中用 file 指令指明调试文件时就必须指明其存储位置)。
默认情况下,GDB 调试器的工作目录为启动时所使用的目录。例如在 ~ 路径下启动的 GDB 调试器,其工作目录就为 ~(当前用户的 home 目录)。当然,GDB 调试器提供有修改工作目录的指令,即 cd 指令。例如,将 GDB 调试器的工作目录修改为 /tmp/demo,则执行指令为:
gdb) cd /tmp/demo
由此,GDB 调试器的工作目录就变成了 /tmp/demo。
4.某些场景中,目标调试程序的执行还需要临时修改 PATH 环境变量,此时就可以借助 path 指令,例如:
(gdb) path /temp/demo
Executable and object file path: /temp/demo:/usr/local/sbin:/usr/local/bin...
注意,此修改方式只是临时的,退出 GDB 调试后会失效。
5. 默认情况下,GDB 调试的程序会接收 set args 等方式指定的参数,同时会将输出结果打印到屏幕上。而通过对输入、输出重定向,可以令调试程序接收指定文件或者终端提供的数据,也可以将执行结果输出到文件或者某个终端上。
例如,将 main.exe 文件的执行结果输出到 a.txt 文件中,执行如下命令:
(gdb) run > a.txt
由此,在 GDB 调试的工作目录下就会生成一个 a.txt 文件,其中存储的即为 main.exe 的执行结果。
示例
总的来说,只有将调试程序所需的运行环境搭建好后,才能使用 run 或者 start 命令开始调试。如下是一个完整的实例,演示了 GDB 调试 mian.exe 之前所做的准备工作:
[root@bogon demo]# pwd <--显示当前工作路径
/tmp/demo
[root@bogon demo]# ls <-- 显示当前路径下的文件
a.txt main.c main.exe
[root@bogon demo]# cd ~ <-- 进入 home 目录
[root@bogon ~]# gdb -q <-- 开启 GDB 调试器
(gdb) cd /tmp/demo <-- 修改 GDB 调试器的工作目录
Working directory /tmp/demo.
(gdb) file main.exe <-- 指定要调试的目标文件
Reading symbols from main.exe...
(gdb) set args a.txt <-- 指定传递的数据
(gdb) run <-- 运行程序
Starting program: /tmp/demo/main.exe a.txt
file open true[Inferior 1 (process 43065) exited normally]
不同目标程序启动方式
1.调试方式启动运行无参程序
以下是linux下GDB调试的一个实例,先给出一个示例用的小程序,C语言代码:
main.c
#include <stdio.h>void Print(int i){printf("hello,程序猿编码 %d\n", i);
}int main(int argc, char const *argv[]){int i = 0;for (i = 1; i < 3; i++){Print(i);}return 0;
}
编译:
gcc -g main.c -o main
下面“gdb”命令启动GDB,将首先显示GDB说明,不管它:
上面最后一行“(gdb)”为GDB内部命令引导符,等待用户输入GDB命令。
下面使用“file”命令载入被调试程序 main(这里的 main 即前面gcc 编译输出的可执行文件):
如果最后一行提示Reading symbols from /home/minger/share/tencent/gdb/main…done. 表示已经加载成功。
下面使用“r”命令执行(Run)被调试文件,因为尚未设置任何断点,将直接执行到程序结束:
2.调试启动带参程序
假设有以下程序,启动时需要带参数:
#include <stdio.h>int main(int argc, char const *argv[]){if (1 >= argc){printf("usage:hello name\n");return 0;}printf("hello,程序猿编码 %s\n", argv[1]);return 0;
}
编译:
gcc -g test.c -o test
这种情况如何启动调试呢?只需要r的时候带上参数即可。
3.调试core文件
Core Dump:Core的意思是内存,Dump的意思是扔出来,堆出来(段错误)。开发和使用Unix程序时,有时程序莫名其妙的down了,却没有任何的提示(有时候会提示core dumped),这时候可以查看一下有没有形如core.进程号的文件生成,这个文件便是操作系统把程序down掉时的内存内容扔出来生成的, 它可以做为调试程序的参考,能够很大程序帮助我们定位问题。那怎么生成Core文件呢?
生成Core方法
产生coredump的条件,首先需要确认当前会话的ulimit –c,若为0,则不会产生对应的coredump,需要进行修改和设置。
即便程序core dump了也不会有core文件留下。我们需要让core文件能够产生,设置core大小为无限:
ulimit -c unlimied
因为core dump默认会生成在程序的工作目录,但是有些程序存在切换目录的情况,导致core dump生成的路径没有规律,
所以最好是自己建立一个文件夹,存放生成的core文件。
我建立一个 /data/coredump 文件夹,在根目录data里的coredump文件夹。
调用如下命令:
echo /data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern
将更改core文件生成路径,自动放在这个/data/coredump文件夹里。
%e表示程序名, %p表示进程id
测试代码:
#include <stdio.h>int main(int argc, char const *argv[])
{int i = 0;scanf("%d",i);printf("hello,程序猿编码 %d\n",i );return 0;
}
编译运行:
运行后结果显示段错误,该程序在主函数内部scanf的时候回崩溃,i前面应该加上&。
这个时候,进入/data/coredump文件夹可以查看生成的core
然后用gdb调试该core,命令为 gdb core.test.3591 ,显示如下
program terminated with signal 11 告诉我们信号中断了我们的程序,发生了段错误。
这个时候可以敲命令 backtrace(bt) 查看函数的调用的栈帧和层级关系。
这个一堆问号很多人遇到过,网上有些人说是没加载符号表,有人说是标准glibc版本不一致,不纠结这个问题。
gdb 可执行程序exe
进入gdb环境后
core-file core的名字
敲命令bt可以查看准确信息。
gdb 可执行程序
进入gdb环境后,core-file core的名字
敲bt命令,这是gdb查看back trace的命令,查看函数的调用的栈帧和层级关系。
可以看到最近的栈中存储的是调用了IO操作,可以看到main函数的26行出错。
到此为止,就是core文件配置生成和调试方法。
step/next命令:单步调试与 print/display命令:查看/设置变量
//test.c
#include <stdio.h>void judge_sd(int num){if ((num & 1) == 0){printf("%d is even\n",num);return;}else{printf("%d is odd\n",num);return;}
}int main(int argc, char const *argv[]){judge_sd(0);judge_sd(1);judge_sd(4);return 0;
}
编译:
gcc -g test.c -o test
程序的功能比较简单,这里不多做解释。断点附近的代码你了解后,这时候你就可以使用单步执行一条一条语句的去执行。可以随时查看执行后的结果。单步执行有两个命令,分别是step和next。我们可能打了多处断点,或者断点打在循环内,这个时候,可以使用continue命令。这三个命令的区别在于:
1、next命令(可简写为n)用于在程序断住后,继续执行下一条语句。
2、step命令(可简写为s),它可以单步跟踪到函数内部。
3、continue命令(可简写为c)或者fg,它会继续执行程序,直到再次遇到断点处。
1.单步进入-step
step 一条语句一条语句的执行。它有一个别名,s。它可以单步跟踪到函数内部。
先用list(可简写为l)将源码列出来,例如:
先启动调试,然后把源码列出来。
从上面的过程可以看到,在5行设置断点,运行程序,可见,step命令进入到了被调用函数中judge_sd。使用step命令也会在这个方法中一行一行的单步执行。但是如果没有该函数源码,需要跳过该函数执行,可使用finish命令,继续后面的执行。
2.单步执行-next
next命令示例:
next命令(可简写为n)用于在程序断住后,继续执行下一条语句。上面的信息在5行处打断点,然后运行到6行,然后输入 运行n 2,则会单步执行两行。可见,使用next命令只会在本方法中单步执行。
3.继续执行到下一个断点-continue
我们可能打了多处断点,或者断点打在循环内,这个时候,想跳过这个断点,甚至跳过多次断点继续执行该怎么做呢?可以使用continue命令。它的作用就是从暂停处继续执行。命令的简写形式为c。继续执行过程中遇到断点或者观察点变化依然会暂停。示例代码如下:
4.跳过执行–skip
根据上面的信息可以看到,使用skip之后,将不会进入judge_sd函数。好处就是skip可以在step时跳过一些不想关注的函数或者某个文件。
如果想删除skip,使用skip delete [num] 。
5.查看变量
现在你已经会设置断点,查看断点附近的代码,并可以单步执行和继续执行。接下来你可能会想知道程序运行的一些情况,如查看变量的值。print命令正好满足了你的需求。以帮助我们进一步定位问题。
5.1 print
格式:
(gdb) print num
(gdb) p num
print(可简写为p)打印变量内容。示例代码如下:
//test.c
#include <stdio.h>
#include <stdlib.h> //malloc,free,randint main(int argc, char const *argv[])
{int input;int i ;printf("Please enter the length of the string:");scanf("%d",&input);char *buf = (char *) malloc(input + 1);//字符最后包含'\0'if (buf == NULL){printf("malloc failed!\n");return -1;}//随机生成字符串for ( i = 0; i < input; i++){buf[i] = rand()%26 +'a';}buf[i] = '\0';printf("A randomly generated string: %s\n",buf);free(buf);return 0;
}
编译:
gcc -g test.c -o test
先用list(可简写为l)将源码列出来,例如:
print命令的简写形式为p,使用它打印出变量的值。
打印出的变量i的值为80。
当然,多个函数或者多个文件会有同一个变量名,这个时候可以在前面加上文件名或者函数名来区分:
p 'testfile.c'::i
p 'sum'::i
在看看指针。
注意到了没有,如果使用上面的方式打印指针指向的内容,那么打印出来的只是指针地址而已。那怎么打印出指针指向的内容呢?
需要解引用,如下:
仅仅使用*只能打印第一个值,如果要打印多个值,后面跟上@并加上要打印的长度。
或者@后面跟上变量值:如下:
另外值得一提的是,$可表示上一个变量,在调试链表时时经常会用到的,它有next成员代表下一个节点,则可使用下面方式不断打印链表内容,举例:
p *linkNode #这里显示linkNode节点内容
p *$.next #这里显示linkNode节点下一个节点的内容
5.2 display
和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。
也就是说,使用 1 次 print 命令只能查看 1 次某个变量或表达式的值,而同样使用 1 次 display 命令,每次程序暂停执行时都会自动打印出目标变量或表达式的值。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸。
5.2.1 display
display 命令没有缩写形式,常用的语法格式如下 2 种:
(gdb) display expr
(gdb) display/fmt expr
其中,expr 表示要查看的目标变量或表达式;参数 fmt 用于指定输出变量或表达式的格式,表 1 罗列了常用的一些 fmt 参数。
表 1 /fmt 常用的值
/fmt | 功 能 |
---|---|
/x | 以十六进制的形式打印出整数。 |
/d | 以有符号、十进制的形式打印出整数。 |
/u | 以无符号、十进制的形式打印出整数。 |
/o | 以八进制的形式打印出整数。 |
/t | 以二进制的形式打印出整数。 |
/f | 以浮点数的形式打印变量或表达式的值。 |
/c | 以字符形式打印变量或表达式的值。 |
注意,display 命令和 /fmt 之间不要留有空格。以 /x 为例,应写为 (gdb)display/x expr。
以调试 main.exe 为例:
[root@bogon demo]# gdb main.exe -q
Reading symbols from ~/demo/main.exe...done.
(gdb) l
1 #include <stdio.h>
2 int main(){
3 int num,result=0,i=0;
4 scanf("%d", &num);
5 while(i<=num){
6 result += i;
7 i++;
8 }
9 printf("result=%d\n", result);
10 return 0;
(gdb)
11 }
(gdb) b 4
Breakpoint 1 at 0x40053c: file main.c, line 4.
(gdb) b 9
Breakpoint 2 at 0x400569: file main.c, line 9.
(gdb) r
Starting program: /root/demo/main.exeBreakpoint 1, main () at main.c:4
4 scanf("%d", &num);
(gdb) display num
1: num = 32767
(gdb) display/t result
2: /t result = 0
(gdb) n
3
5 while(i<=num){
2: /t result = 0
1: num = 3
(gdb) c
Continuing.Breakpoint 2, main () at main.c:9
9 printf("result=%d\n", result);
2: /t result = 110
1: num = 3
(gdb) c
Continuing.
result=6Program exited normally.
(gdb)
可以看到,使用 display 命令查看 num 和 result 变量值时,不仅在执行该命令的同时会看到目标变量的值,后续每次程序停止执行时,GDB 调试器都会将目标变量的值打印出来。
5.2.2 info display
事实上,对于使用 display 命令查看的目标变量或表达式,都会被记录在一张列表(称为自动显示列表)中。通过执行info dispaly
命令,可以打印出这张表:
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: y /t result
1: y num
其中,各列的含义为:
- Num 列为各变量或表达式的编号,GDB 调试器为每个变量或表达式都分配有唯一的编号;
- Enb 列表示当前各个变量(表达式)是处于激活状态还是禁用状态,如果处于激活状态(用 y 表示),则每次程序停止执行,该变量的值都会被打印出来;反之,如果处于禁用状态(用 n 表示),则该变量(表达式)的值不会被打印。
- Expression 列:表示查看的变量或表达式。
5.2.3 undisplay
对于不需要再打印值的变量或表达式,可以将其删除或者禁用。
- 通过执行如下命令,即可删除自动显示列表中的变量或表达式:
(gdb) undisplay num...
(gdb) delete display num...
参数 num... 表示目标变量或表达式的编号,编号的个数可以是多个。
举个例子:
(gdb) undisplay 1
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: y /t result
(gdb)
可以看到,借助 undisplay 命令成功删除了编号为 1 的 num 变量。
5.2.4 disable display
- 通过执行如下命令,可以禁用自动显示列表中处于激活状态下的变量或表达式:
(gdb) disable display num...
num... 表示要禁用的变量或表达式的编号,编号的个数可以是多个,表示一次性禁用多个变量或表达式
举个例子:
(gdb) disable display 2
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: n /t result
(gdb)
可以看到,编号为 2 的 result 变量的 Enb 由 y 变成了 n。处于禁用状态的变量或表达式,程序停止执行时将不再自动打印出它们的值。
5.2.5 enable display
当然根据需要,也可以激活当前处于禁用状态的变量或表达式,执行如下命令即可:
(gdb) enable display num...
参数 num... 表示要激活的变量或表达式的编号,编号的个数可以是多个,表示一次性激活多个变量或表达式。
举个例子:
(gdb) enable display 2
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: y /t result
(gdb)
总的来说,每次程序停止执行时,GDB 调试器会将自动显示列表中处于激活状态下的变量或表达式的值打印出来,display 命令可以实现在查看目标变量或表达式的值的同时,将其添加到自动显示列表中,而 print 命令则只会打印出目标变量或表达式的值。
6.设置变量
使用print命令查看了变量的值,如果感觉这个值不符合预期,想修改下这个值,再看下执行效果。这种情况下,我们该怎么办呢?通常情况下,我们会修改代码,再重新执行代码。使用gdb的set命令,一切将变得更简单。
set命令可以直接修改变量的值。
set var sum=XXX
break命令:断点设置与list命令:查看源码
前言
上篇GDB启动调试我们讲到了GDB启动调试的多种方式,在 Linux 环境软件开发中,GDB 是主要的调试工具,用来调试 C 和 C++ 程序。这一篇主要讲GDB的断点设置与查看源码。
为什么要设置断点呢?
当我们想查看变量内容,堆栈情况等等,可以指定断点。程序执行到断点处会暂停执行。break 命令用来设置断点,缩写形式为b。设置断点后,以便我们更详细的跟踪断点附近程序的执行情况。
GDB break命令
break 命令(可以用 b 代替)常用的语法格式有以下 2 种。
1、(gdb) break location // b location
2、(gdb) break ... if cond // b .. if cond
- 第一种格式中,location 用于指定打断点的具体位置,其表示方式有多种,如表1所示。
表1 location 参数的表示方式
location 的值 | 含 义 |
---|---|
linenum | linenum 是一个整数,表示要打断点处代码的行号。要知道,程序中各行代码都有对应的行号,可通过执行 l(小写的 L)命令看到。 |
filename:linenum | filename 表示源程序文件名;linenum 为整数,表示具体行数。整体的意思是在指令文件 filename 中的第 linenum 行打断点。 |
+ offset - offset | offset 为整数(假设值为 2),+offset 表示以当前程序暂停位置(例如第 4 行)为准,向后数 offset 行处(第 6 行)打断点;-offset 表示以当前程序暂停位置为准,向前数 offset 行处(第 2 行)打断点。 |
function | function 表示程序中包含的函数的函数名,即 break 命令会在该函数内部的开头位置打断点,程序会执行到该函数第一行代码处暂停。 |
filename:function | filename 表示远程文件名;function 表示程序中函数的函数名。整体的意思是在指定文件 filename 中 function 函数的开头位置打断点。 |
- 第二种格式中,... 可以是表 1 中所有参数的值,用于指定打断点的具体位置;cond 为某个表达式。整体的含义为:每次程序执行到 ... 位置时都计算 cond 的值,如果为 True,则程序在该位置暂停;反之,程序继续执行。
1.通过行号设置断点
格式:
break [行号]
break 行号,断点设置在该行开始处,注意:该行代码未被执行
如果你的程序是用c或者c++写的,那么你可以使用“文件名:行号”的形式设置断点。示例如下:
//test.c
#include <stdio.h>void judge_sd(int num){if ((num & 1) == 0){printf("%d is even\n",num);return;}else{printf("%d is odd\n",num);return;}
}int main(int argc, char const *argv[]){judge_sd(0);judge_sd(1);judge_sd(4);return 0;
}
编译:
gcc -g test.c -o test
gdb test
break 文件名 : 行号,适用于有多个源文件的情况。
示例中的(gdb) b test.c:18是设置了断点。断点的位置是test.c文件的18行。使用r命令执行脚本时,当运行到18行时就会暂停。注意:该行代码未被执行
2.通过函数设置断点
格式:
break [函数名]
break 函数名,断点设置在该函数的开始处,断点所在行未被执行:
同样可以将断点设置在函数处:
b judge_sd
3.设置条件断点
格式:
break ... if cond
如果按上面的方法设置断点后,每次执行到断点位置都会暂停。有时候非常讨厌。我们只想在指定条件下才暂停。这时候根据条件设置断点就有了用武之地。设置条件断点的形式,就是在设置断点的基本形式后面增加 if条件。示例如下:
break test.c:6 if num>0
当在num>0时,程序将会在第6行断住。
GDB tbreak命令
tbreak 命令可以看到是 break 命令的另一个版本,tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在于,使用 tbreak 命令打的断点仅会作用 1 次,即程序暂停之后,该断点就会自动消失。
tbreak 命令的使用格式和 break 完全相同,有以下 2 种:
1、(gdb) tbreak location
2、(gdb) tbreak ... if cond
其中,location、... 和 cond 的含义都和 break 命令中的参数含义相同,即表 1 也同样适用于 tbreak 命令。
以 main.exe 为例,如下演示了 tbreak 命令的用法:
#include<stdio.h>
int main(int argc,char* argv[])
{int num = 1;while(num<100){num *= 2;}printf("num=%d",num);return 0;
}
(gdb) tbreak 7 if num>10
Temporary breakpoint 1 at 0x1165: file main.c, line 7.
(gdb) r
Starting program: /home/ubuntu64/demo/main.exeTemporary breakpoint 1, main (argc=1, argv=0x7fffffffe088) at main.c:7
7 num *= 2;
(gdb) p
$1 = 16
(gdb) c <-- 继续执行程序,则原使用 tbreak 在第 7 行打的断点将不再起作用
Continuing.
num=128[Inferior 1 (process 6534) exited normally]
(gdb)
可以看到,自num=16
开始,后续循环过程中 num 的值始终大于 10,则num>10
表达式的值永远为 True,理应在第 7 行暂停多次。但由于打断点采用的是 tbreak 命令,因此断点的作用只起 1 次。
GDB rbreak 命令
和 break 和 tbreak 命令不同,rbreak 命令的作用对象是 C、C++ 程序中的函数,它会在指定函数的开头位置打断点。
rbreak 命令的使用语法格式为:
(gdb) rbreak regex
其中 regex 为一个正则表达式,程序中函数的函数名只要满足 regex 条件,rbreak 命令就会其内部的开头位置打断点。值得一提的是,rbreak 命令打的断点和 break 命令打断点的效果是一样的,会一直存在,不会自动消失。
这里我们对 main.c 源文件的程序做如下修改:
(gdb) l <-- 显示源码
1 #include<stdio.h>
2 void rb_one(){
3 printf("rb_one\n");
4 }
5 void rb_second(){
6 printf("rb_second");
7 }
8 int main(int argc,char* argv[])
9 {
10 rb_one();
(gdb)
11 rb_second();
12 return 0;
13 }
(gdb) rbreak rb_* <--匹配所有以 rb_ 开头的函数
Breakpoint 1 at 0x1169: file main.c, line 2.
void rb_one();
Breakpoint 2 at 0x1180: file main.c, line 5.
void rb_second();
(gdb) r
Starting program: /home/ubuntu64/demo/main.exeBreakpoint 1, rb_one () at main.c:2
2 void rb_one(){
(gdb) c
Continuing.
rb_one
Breakpoint 2, rb_second () at main.c:5
5 void rb_second(){
(gdb) c
Continuing.
rb_second[Inferior 1 (process 7882) exited normally]
(gdb)
可以看到,通过执行rbreak rb_*
指令,找到了程序中所有以 tb_* 开头的函数,并在这些函数内部的开头位置打上了断点(如上所示,分别为第 2 行和第 5 行)。
GDB查看断点
1. info breakpoint [n]
格式:
(gdb) info breakpoint [n]
(gdb) info break [n]
参数 n 作为可选参数,为某个断点的编号,表示查看指定断点而非全部断点。
可以使用info breakpoints查看断点的情况。包含都设置了那些断点,断点被命中的次数等信息。示例如下:
它将会列出所有已设置的断点,每一个断点都有一个标号,用来代表这个断点。
以调试如下 C 语言程序为例:
#include <stdio.h>
int main(){int num = 0;scanf("%d", &num);printf("%d", num);return 0;
}
程序存储在~/demo/main.c
文件中,并已将其编译为可调试的 main.exe 可执行文件:
[root@bogon demo]# gcc main.c -o main.exe -g
[root@bogon demo]# ls
main.c main.exe
要知道,任何类型的断点在建立时,GDB 调试器都会为其分配一个独一无二的断点编号。以 main.exe 为例,我们尝试建立如下断点:
gdb) b 1
Breakpoint 1 at 0x1189: file main.c, line 2.
(gdb) r
Starting program: ~/demo/main.exeBreakpoint 1, main () at main.c:2
2 int main(){
(gdb) watch num
Hardware watchpoint 2: num
(gdb) catch throw int
Catchpoint 3 (throw)
可以看到,我们通过 break 命令建立了一个普通断点,其编号为 1;通过 watch 命令建立了一个观察断点,其编号为 2;通过 catch 命令建立了一个捕捉断点,其编号为 3。
在此基础上,可以通过执行 info break 或者 info breakpoint 命令,查看所有断点的具体信息:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint keep y num
3 catchpoint keep y exception throw matching: int
(gdb)
借助每个断点不同的编号,也可以进行精准查询:
(gdb) info break 1
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040053c in main at main.c:2 breakpoint already hit 1 time
以上输出信息中各列的含义分别是:断点编号(Num)、断点类型(Type)、是临时断点还是永久断点(Disp)、目前是启用状态还是禁用状态(Enb)、断点的位置(Address)、断点当前的状态(作用的行号、已经命中的次数等,用 What 列表示)。
2. info watchpoint [n]
除此之外,对于调试环境中已经建好且未删除的观察断点,也可以使用 info watchpoint 命令进行查看,语法格式如下:
(gdb) info watchpoint [n]
n 为可选参数,为某个观察断点的编号,功能是只查看该编号的观察断点的信息,而不是全部的观察断点。
继续在上面的调试环境中,执行如下指令:
(gdb) info watchpoint
Num Type Disp Enb Address What
2 hw watchpoint keep y num
(gdb) info watchpoint 1
No watchpoint number 1.
由于当前环境中仅有 1 个观察断点,因此 info watchpoint 命令仅罗列了编号为 2 的观察断点的信息。需要注意的是,该命令仅能用于查看观察断点,普通断点和捕捉断点无法使用该命令。
GDB删除断点
无论是普通断点、观察断点还是捕捉断点,都可以使用 clear 或者 delete 命令进行删除。
1. clear location
clear 命令可以删除指定位置处的所有断点,常用的语法格式如下所示:
(gdb) clear location
参数 location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。
在上面调试环境中,继续执行如下命令:
(gdb) clear 2(gdb) info break
Deleted breakpoint 1
Num Type Disp Enb Address What
2 hw watchpoint keep y num
3 catchpoint keep y exception throw matching: int
(gdb)
可以看到,断点编号为 1、位于程序第 2 行代码处的普通断点已经被删除了。
2. delete breakpoints
delete 命令(可以缩写为 d )通常用来删除所有断点,也可以删除指定编号的各类型断点,语法格式如下:
delete [breakpoints] [num]
其中,breakpoints 参数可有可无,num 参数为指定断点的编号,其可以是 delete 删除某一个断点,而非全部。
举个例子:
(gdb) delete 2
(gdb) info break
Num Type Disp Enb Address What
3 catchpoint keep y exception throw matching: int
可以看到,delete 命令删除了编号为 2 的观察断点。
如果不指定 num 参数,则 delete 命令会删除当前程序中存在的所有断点。例如:
(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) info break
No breakpoints or watchpoints.
GDB禁用断点
所谓禁用,就是使目标断点暂时失去作用,必要时可以再将其激活,恢复断点原有的功能。
1.disable breakpoints
禁用断点可以使用 disable 命令,语法格式如下:
disable [breakpoints] [num...]
breakpoints 参数可有可无;num... 表示可以有多个参数,每个参数都为要禁用断点的编号。如果指定 num...,disable 命令会禁用指定编号的断点;反之若不设定 num...,则 disable 会禁用当前程序中所有的断点。
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint keep y num
3 catchpoint keep y exception throw matching: int
(gdb) disable 1 2
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint keep n num
3 catchpoint keep y exception throw matching: int
(gdb)
可以看到,对于用 disable 命令禁用的断点,Enb 列用 n 表示其处于禁用状态,用 y 表示该断点处于激活状态。
2. enable breakpoints
对于禁用的断点,可以使用 enable 命令激活,该命令的语法格式有多种,分别对应有不同的功能:
enable [breakpoints] [num...] 激活用 num... 参数指定的多个断点,如果不设定 num...,表示激活所有禁用的断点
enable [breakpoints] once num… 临时激活以 num... 为编号的多个断点,但断点只能使用 1 次,之后会自动回到禁用状态
enable [breakpoints] count num... 临时激活以 num... 为编号的多个断点,断点可以使用 count 次,之后进入禁用状态
enable [breakpoints] delete num… 激活 num.. 为编号的多个断点,但断点只能使用 1 次,之后会被永久删除。
其中,breakpoints 参数可有可无;num... 表示可以提供多个断点的编号,enable 命令可以同时激活多个断点。
仍以上面的调试环境为例,当下程序停止在第 2 行(main() 函数开头处),此时执行如下命令:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint keep n num
3 catchpoint keep y exception throw matching: int
(gdb) enable delete 2
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint del y num
3 catchpoint keep y exception throw matching: int
(gdb) c
Continuing.Hardware watchpoint 2: numOld value = 32767
New value = 0
main () at main.c:4
4 scanf("%d", &num);
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
3 catchpoint keep y exception throw matching: int
(gdb)
查看源码(list)
1.查看源码
断点设置完后,当程序运行到断点处就会暂停。暂停的时候,我们可以查看断点附近的代码。查看代码的子命令是list,缩写形式为l。
2.指定行号查看代码
格式:
list first,last
例如,要列出6到21行之间的源码:
3.列出指定文件的源码
前面执行l命令时,默认列出test.c的源码,如果想要看指定文件的源码呢?可以
list 【文件名加行号或函数名】
总结
本文介绍了GDB调试中的断点设置、源码查看。断点设置可以便于我们后期观察变量,堆栈等信息,为进一步的定位与调试做准备。源码查看可以通过指定行号或者方法名来查看相关代码。
watch命令:实时监控变量值的变化
《GDB break命令》一节,给大家介绍了使用 break 命令在程序某一行的位置打断点。但还有一些场景,我们需要监控某个变量或者表达式的值,通过值的变化情况判断程序的执行过程是否存在异常或者 Bug。这种情况下,break 命令显然不再适用,推荐大家使用 watch 命令。
要知道,GDB 调试器支持在程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点。其中 break 命令打的就是普通断点,而 watch 命令打的为观察断点,关于捕捉断点,后续章节会做详细讲解。
使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置。
所谓表达式,就是包含多个变量的式子,比如 a+b 就是一个表达式,其中 a、b 为变量。
对于监控 C、C++ 程序中某变量或表达式的值是否发生改变,watch 命令的语法非常简单,如下所示:
(gdb) watch cond
其中,conde 指的就是要监控的变量或表达式。
和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:
-
rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
-
awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
举个例子:
gdb) l <--列出要调试的程序源码
1 #include<stdio.h>
2 int main(int argc,char* argv[])
3 {
4 int num = 1;
5 while(num<=100)
6 {
7 num *= 2;
8 }
9 printf("%d",num);
10 return 0;
(gdb)
11 }
(gdb) b 4 <-- 使用 break 命令打断点
Breakpoint 1 at 0x115c: file main.c, line 4.
(gdb) r <-- 执行程序
Starting program: /home/ubuntu64/demo/main.exeBreakpoint 1, main (argc=1, argv=0x7fffffffe088) at main.c:4
4 int num = 1;
(gdb) watch num <-- 监控程序中 num 变量的值
Hardware watchpoint 2: num
(gdb) c <-- 继续执行,当 num 值发生改变时,程序才停止执行
Continuing.Hardware watchpoint 2: num
Old value = 0
New value = 2
main (argc=1, argv=0x7fffffffe088) at main.c:5
5 while(num<=100)
(gdb) c <-- num 值发生了改变,继续执行程序
Continuing.Hardware watchpoint 2: numOld value = 2
New value = 4
main (argc=1, argv=0x7fffffffe088) at main.c:5
5 while(num<=100)
(gdb)
可以看到在程序运行过程中,通过借助 watch 命令监控 num 的值,后续只要 num 的值发生改变,程序都会停止。感兴趣的读者,可自行尝试使用 awatch 和 rwatch 命令,这里不再给出具体的示例。
如果我们想查看当前建立的观察点的数量,借助如下指令即可:
(gdb) info watchpoints
值得一提的是,对于使用 watch(rwatch、awatch)命令监控 C、C++ 程序中变量或者表达式的值,有以下几点需要注意:
-
当监控的变量(表达式)为局部变量(表达式)时,一旦局部变量(表达式)失效,则监控操作也随即失效;
-
如果监控的是一个指针变量(例如 *p),则 watch *p 和 watch p 是有区别的,前者监控的是 p 所指数据的变化情况,而后者监控的是 p 指针本身有没有改变指向;
-
这 3 个监控命令还可以用于监控数组中元素值的变化情况,例如对于 a[10] 这个数组,watch a 表示只要 a 数组中存储的数据发生改变,程序就会停止执行。
watch命令的实现原理
watch 命令实现监控机制的方式有 2 种,一种是为目标变量(表达式)设置硬件观察点,另一种是为目标变量(表达式)设置软件观察点。
所谓软件观点(software watchpoint),即用 watch 命令监控目标变量(表达式)后,GDB 调试器会以单步执行的方式运行程序,并且每行代码执行完毕后,都会检测该目标变量(表达式)的值是否发生改变,如果改变则程序执行停止。
可想而知,这种“实时”的判别方式,一定程度上会影响程序的执行效率。但从另一个角度看,调试程序的目的并非是为了获得运行结果,而是查找导致程序异常或 Bug 的代码,因此即便软件观察点会影响执行效率,一定程度上也是可以接受的。
所谓硬件观察点(Hardware watchpoint),和前者最大的不同是,它在实现监控机制的同时不影响程序的执行效率。简单的理解,系统会为 GDB 调试器提供少量的寄存器(例如 32 位的 Intel x86 处理器提供有 4 个调试寄存器),每个寄存器都可以作为一个观察点协助 GDB 完成监控任务。
需要注意的是,基于寄存器个数的限制,如果调试环境中设立的硬件观察点太多,则有些可能会失去作用,这种情况下,GDB 调试器会发出如下警告:
Hardware watchpoint num: Could not insert watchpoint
解决方案也很简单,就是删除或者禁用一部分硬件观察点。
除此之外,受到寄存器数量的限制,可能会出现:无法使用硬件观察点监控数据类型占用字节数较多的变量(表达式)。比如说,某些操作系统中,GDB 调试器最多只能监控 4 个字节长度的数据,这意味着 C、C++ 中 double 类型的数据是无法使用硬件观察点监测的。这种情况下,可以考虑将其换成占用字符串少的 float 类型。
目前,大多数 PowerPC 或者基于 x86 的操作系统,都支持采用硬件观点。并且 GDB 调试器在建立观察断点时,会优先尝试建立硬件观察点,只有当前环境不支持硬件观察点时,才会建立软件观察点。借助如下指令,即可强制 GDB 调试器只建立软件观察点:
set can-use-hw-watchpoints 0
注意,在执行此命令之前建立的硬件观察点,不会受此命令的影响。
注意,awatch 和 rwatch 命令只能设置硬件观察点,如果系统不支持或者借助如上命令禁用,则 GDB 调试器会打印如下信息:
Expression cannot be implemented with read/access watchpoint.
catch命令:建立捕捉断点
要知道,GDB 调试器支持在被调试程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点,其中普通断点用 break 命令建立(可阅读《GDB break》一节),观察断点用 watch 命令建立(可阅读《GDB watch》一节),本节将讲解如何使用 catch 命令建立捕捉断点。
和前 2 种断点不同,普通断点作用于程序中的某一行,当程序运行至此行时停止执行,观察断点作用于某一变量或表达式,当该变量(表达式)的值发生改变时,程序暂停。而捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。
用捕捉断点监控某一事件的发生,等同于在程序中该事件发生的位置打普通断点。
建立捕捉断点的方式很简单,就是使用 catch 命令,其基本格式为:
其中,event 参数表示要监控的具体事件。对于使用 GDB 调试 C、C++ 程序,常用的 event 事件类型如表 1 所示
表 1 常见的 event 事件
event 事件 | 含 义 |
---|---|
throw [exception] | 当程序中抛出 exception 指定类型异常时,程序停止执行。如果不指定异常类型(即省略 exception),则表示只要程序发生异常,程序就停止执行。 |
catch [exception] | 当程序中捕获到 exception 异常时,程序停止执行。exception 参数也可以省略,表示无论程序中捕获到哪种异常,程序都暂停执行。 |
load [regexp] unload [regexp] | 其中,regexp 表示目标动态库的名称,load 命令表示当 regexp 动态库加载时程序停止执行;unload 命令表示当 regexp 动态库被卸载时,程序暂停执行。regexp 参数也可以省略,此时只要程序中某一动态库被加载或卸载,程序就会暂停执行。 |
除表中罗列的以外,event 参数还有其它一些写法,感兴趣的读者可查看 GDB官网进行了解,这里不再过多赘述。
注意,当前 GDB 调试器对监控 C++ 程序中异常的支持还有待完善,使用 catch 命令时,有以下几点需要说明:
- 对于使用 catch 监控指定的 event 事件,其匹配过程需要借助 libstdc++ 库中的一些 SDT 探针,而这些探针最早出现在 GCC 4.8 版本中。也就是说,想使用 catch 监控指定类型的 event 事件,系统中 GCC 编译器的版本最低为 4.8,但即便如此,catch 命令是否能正常发挥作用,还可能受到系统中其它因素的影响。
- 当 catch 命令捕获到指定的 event 事件时,程序暂停执行的位置往往位于某个系统库(例如 libstdc++)中。这种情况下,通过执行 up 命令,即可返回发生 event 事件的源代码处。
- catch 无法捕获以交互方式引发的异常。
如同 break 命令和 tbreak 命令的关系一样(前者的断点是永久的,后者是一次性的),catch 命令也有另一个版本,即 tcatch 命令。tcatch 命令和 catch 命令的用法完全相同,唯一不同之处在于,对于目标事件,catch 命令的监控是永久的,而 tcatch 命令只监控一次,也就是说,只有目标时间第一次触发时,tcath 命令才会捕获并使程序暂停,之后将失效。
接下来就以下面的 C++ 程序为例,给大家演示 catch(tcatch)命令的用法:
#include <iostream>
using namespace std;
int main(){int num = 1;while(num <= 5){try{throw 100;}catch(int e){num++;cout << "next" << endl;}}cout << "over" << endl;return 0;
}
此程序存储于 ~/demo/main.cpp 文件中( ~ 表示当前登陆用户的主目录)。
在此基础上,对 main.cpp 文件进行编译,并启动对该程序的调试:
[root@bogon demo]$ ls
main.cpp
[root@bogon demo]# g++ main.cpp -o main.exe -g
[root@bogon demo]$ ls
main.cpp main.exe
[root@bogon demo]# gdb main.exe -q
Reading symbols from main.exe...done.
(gdb)
通过观察程序可以看出,当前程序中通过 throw 手动抛出了 int 异常,此异常能够被 catch 成功捕获。假设我们使用 catch 命令监控:只要程序中引发 int 异常,程序就停止执行:
(gdb) catch throw int <-- 指定捕获“throw int”事件
Catchpoint 1 (throw)
(gdb) r <-- 执行程序
Starting program: ~/demo/main.exeCatchpoint 1 (exception thrown), 0x00007ffff7e81762 in __cxa_throw ()from /lib/x86_64-linux-gnu/libstdc++.so.6 <-- 程序暂停执行
(gdb) up <-- 回到源码
#1 0x0000555555555287 in main () at main.cpp:8
8 throw 100;
(gdb) c <-- 继续执行程序
Continuing.
nextCatchpoint 1 (exception thrown), 0x00007ffff7e81762 in __cxa_throw ()from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x0000555555555287 in main () at main.cpp:8
8 throw 100;
(gdb)
如上所示,借助 catch 命令设置了一个捕获断点,该断点用于监控 throw int 事件,只要发生程序就会暂停执行。由此当程序执行时,其会暂停至 libstdc++ 库中的某个位置,借助 up 指令我们可以得知该异常发生在源代码文件中的位置。
同理,我们也可以监控 main.cpp 程序中发生的 catch event 事件:
(gdb) catch catch int
Catchpoint 1 (catch)
(gdb) r
Starting program: ~/demo/main.exeCatchpoint 1 (exception caught), 0x00007ffff7e804d3 in __cxa_begin_catch ()from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x00005555555552d0 in main () at main.cpp:9
9 }catch(int e){
(gdb) c
Continuing.
nextCatchpoint 1 (exception caught), 0x00007ffff7e804d3 in __cxa_begin_catch ()from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x00005555555552d0 in main () at main.cpp:9
9 }catch(int e){
(gdb)
GDB frame和backtrace命令:查看栈信息
当程序因某种异常停止运行时,我们要做的就是找到程序停止的具体位置,分析导致程序停止的原因。
对于 C、C++ 程序而言,异常往往出现在某个函数体内,例如 main() 主函数、调用的系统库函数或者自定义的函数等。要知道,程序中每个被调用的函数在执行时,都会生成一些必要的信息,包括:
- 函数调用发生在程序中的具体位置;
- 调用函数时的参数;
- 函数体内部各局部变量的值等等。
这些信息会集中存储在一块称为“栈帧”的内存空间中。也就是说,程序执行时调用了多少个函数,就会相应产生多少个栈帧,其中每个栈帧自函数调用时生成,函数调用结束后自动销毁。
注意,这些栈帧所在的位置也不是随意的,它们集中位于一个大的内存区域里,我们通常将其称为栈区或者栈。
每个 C、C++ 程序在执行时,都会占用一整块内存空间。不仅如此,整块内存空间还会进行更细致的划分,例如栈区、堆区、全局数据区、常量区等,以便于将程序中不同的资源存放在不同的的内存区域中。有关各个区域的具体作用,读者可阅读《Linux下C语言程序的内存布局》做详细了解。
这也就意味着,当程序因某种异常暂停执行时,如果其发生在某个函数内部,我们可以尝试借助该函数对应栈帧中记录的信息,找到程序发生异常的原因。
庆幸的是,GDB 调试器为了方便用户在调试程序时查看某个栈帧中记录的信息,提供了 frame 和 backtrace 命令。接下来,我将给读者详细讲解一下这 2 个命令的功能和用法。
GDB frame命令
通过阅读上文我们知道,任何一个被调用的函数,执行时都会生成一个存储必要信息的栈帧。对于 C、C++ 程序而言,其至少也要包含一个函数,即 main() 主函数,这意味着程序执行时至少会生成一个栈帧。
main() 主函数对应的栈帧,又称为初始帧或者最外层的帧。
除此之外,每当程序中多调用一个函数,执行过程中就会生成一个新的栈帧。更甚者,如果该函数是一个递归函数,则会生成多个栈帧。
在程序内部,各个栈帧用地址作为它们的标识符,注意这里的地址并不一定为栈帧的起始地址。我们知道,每个栈帧往往是由连续的多个字节构成,每个字节都有自己的地址,不同操作系统为栈帧选定地址标识符的规则不同,它们会选择其中一个字节的地址作为栈帧的标识符。
然而,GDB 调试器并没有套用地址标识符的方式来管理栈帧。对于当前调试环境中存在的栈帧,GDB 调试器会按照既定规则对它们进行编号:当前正被调用函数对应的栈帧的编号为 0,调用它的函数对应栈帧的编号为 1,以此类推。
frame 命令的常用形式有 2 个:
- 根据栈帧编号或者栈帧地址,选定要查看的栈帧,语法格式如下:
(gdb) frame spec
该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值,常用的指定方法有 3 种:
- 通过栈帧的编号指定。0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;
- 借助栈帧的地址指定。栈帧地址可以通过 info frame 命令(后续会讲)打印出的信息中看到;
- 通过函数的函数名指定。注意,如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。
除此之外,对于选定一个栈帧作为当前栈帧,GDB 调试器还提供有 up 和 down 两个命令。其中,up 命令的语法格式为:
(gdb) up n
其中 n 为整数,默认值为 1。该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m+n 为编号的栈帧作为新的当前栈帧。
相对地,down 命令的语法格式为:
(gdb) down n
其中 n 为整数,默认值为 1。该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m-n 为编号的栈帧作为新的当前栈帧。
- 借助如下命令,我们可以查看当前栈帧中存储的信息:
(gdb) info frame
该命令会依次打印出当前栈帧的如下信息:
- 当前栈帧的编号,以及栈帧的地址;
- 当前栈帧对应函数的存储地址,以及该函数被调用时的代码存储的地址
- 当前函数的调用者,对应的栈帧的地址;
- 编写此栈帧所用的编程语言;
- 函数参数的存储地址以及值;
- 函数中局部变量的存储地址;
- 栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用 eip 表示)、堆栈基指针寄存器(64位环境用 rbp 表示,32位环境用 ebp 表示)等。
除此之外,还可以使用info args
命令查看当前函数各个参数的值;使用info locals
命令查看当前函数中各局部变量的值。
GDB backtrace命令
backtrace 命令用于打印当前调试环境中所有栈帧的信息,常用的语法格式如下:
(gdb) backtrace [-full] [n]
其中,用 [ ] 括起来的参数为可选项,它们的含义分别为:
- n:一个整数值,当为正整数时,表示打印最里层的 n 个栈帧的信息;n 为负整数时,那么表示打印最外层 n 个栈帧的信息;
- -full:打印栈帧信息的同时,打印出局部变量的值。
除此之外,backtrace 命令还有其它可选参数,感兴趣的读者可自行前往 GDB 官网查看。
注意,当调试多线程程序时,该命令仅用于打印当前线程中所有栈帧的信息。如果想要打印所有线程的栈帧信息,应执行thread apply all backtrace
命令。
基于以上对 frame 和 backtrace 命令的介绍,这里以调试如下 C 语言程序为例,给大家演示这 2 个命令的作用。
#include <stdio.h>
int func(int num){if(num==1){return 1;}else{return num*func(num-1);}
}
int main ()
{int n = 5;int result = func(n);printf("%d! = %d",n,result);return 0;
}
不难发现,func() 是一个递归函数。该程序存储在~/demo/main.c
文件中,并已编译为可供 GDB 调试的 main.exe 可执行文件。在此基础上,进行如下调试:
(gdb) b 3
Breakpoint 1 at 0x4004cf: file main.c, line 3.
(gdb) r
Starting program: ~/demo/main.exeBreakpoint 1, func (num=5) at main.c:3
3 if(num==1){
(gdb) c
Continuing.
Breakpoint 1, func (num=4) at main.c:3
3 if(num==1){
(gdb) p num
$1 = 4
(gdb) backtrace <-- 打印所有的栈帧信息
#0 func (num=4) at main.c:3
#1 0x00000000004004e9 in func (num=5) at main.c:6
#2 0x0000000000400508 in main () at main.c:12
(gdb) info frame <-- 打印当前栈帧的详细信息
Stack level 0, frame at 0x7fffffffe240: <-- 栈帧编号 0,地址 0x7fffffffe240
rip = 0x4004cf in func (main.c:3); saved rip 0x4004e9 <-- 函数的存储地址 0x4004cf,调用它的函数地址为 0x4004e9
called by frame at 0x7fffffffe260 <-- 当前栈帧的上一级栈帧(编号 1 的栈帧)的地址为 0x7fffffffe260
source language c.
Arglist at 0x7fffffffe230, args: num=4 <-- 函数参数的地址和值
Locals at 0x7fffffffe230, Previous frame's sp is 0x7fffffffe240 <--函数内部局部变量的存储地址
Saved registers: <-- 栈帧内部存储的寄存器rbp at 0x7fffffffe230, rip at 0x7fffffffe238
(gdb) info args <-- 打印当前函数参数的值
num = 4
(gdb) info locals <-- 打印当前函数内部局部变量的信息(这里没有)
No locals.
(gdb) up <-- 查看编号为 1 的栈帧
#1 0x00000000004004e9 in func (num=5) at main.c:6
6 return num*func(num-1);
(gdb) frame 1 <-- 当编号为 1 的栈帧作为当前栈帧
#1 0x00000000004004e9 in func (num=5) at main.c:6
6 return num*func(num-1);
(gdb) info frame <-- 打印 1 号栈帧的详细信息
Stack level 1, frame at 0x7fffffffe260:
rip = 0x4004e9 in func (main.c:6); saved rip 0x400508
called by frame at 0x7fffffffe280, caller of frame at 0x7fffffffe240 <--上一级栈帧地址为 0x7fffffffe280,下一级栈帧地址为 0x7fffffffe240
source language c.
Arglist at 0x7fffffffe250, args: num=5
Locals at 0x7fffffffe250, Previous frame's sp is 0x7fffffffe260
Saved registers:rbp at 0x7fffffffe250, rip at 0x7fffffffe258
(gdb)
GDB编辑和搜索源码
本节主要讲解的是在 GDB 内对源文件中的代码进行修改和查找,分别对应 GDB 中的 edit 命令和 search 命令,下面是对这两个命令的详细介绍。
GDB edit命令:编辑文件
在 GDB 中编辑源文件中使用 edit 命令,该命令的语法格式如下:
(gdb) edit [location]
(gdb) edit [filename] : [location]
location 表示程序中的位置。这个命令表示激活文件的指定位置,然后进行编辑。
举个例子:
(gdb) edit 16 //表示激活文件中的第16 行的代码(光标定位到第 16 行代码的开头位置)
(gdb) edit func //表示激活文件中的 func 处的代码(光标定位到 func 函数所在行的开头位置)
(gdb) edit test.c : 16 //表示激活 test.c 文件的第16 行。
值得一提的是,GDB edit 命令编辑文件默认使用的是 ex 编译器,使用的时候可能会遇见下面的情况:
(gdb) edit
bash: /bin/ex: 没有那个文件或目录
遇到这种问题时,我们可以指定任意的编辑器(例如 Vim)去编辑文件。进入 GDB 调试器前,执行如下命令设置 EDITOR 环境变量:
export EDITOR=/usr/bin/vim
由此,当在 GDB 调试器中执行 edit 命令时,就会自动进入 Vim 编辑器,从而对当前调试的程序进行修改。
注意,上面修改编辑器的方法只是临时生效,当退出 shell 终端后配置就会被还原,下次使用的时候还要再次使用这条命令。想要永久的实现配置,需要去修改配置文件,具体做法是:修改当前 home 目录下的”~/.bashrc”文件,在文件的最后添加上述的命令就可以实现文件的永久配置(修改的是当前用户的配置文件,一般不建议修改系统的配置文件,出现问题不容易恢复)。配置完成重启 shell 终端,就可以完成配置。
GDB search命令:搜索文件
在调试文件时,某些时候可能会去找寻找某一行或者是某一部分的代码。可以使用 list 显示全部的源码,然后进行查看。当源文件的代码量较少时,我们可以使用这种方式搜索。如果源文件的代码量很大,使用这种方式寻找效率会很低。所以 GDB 中提供了相关的源代码搜索的的 search 命令。
search 命令的语法格式为:
search <regexp>
reverse-search <regexp>
第一项命令格式表示从当前行的开始向前搜索,后一项表示从当前行开始向后搜索。其中 regexp 就是正则表达式,正则表达式描述了一种字符串匹配的模式,可以用来检查一个串中是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串。很多的编程语言都支持使用正则表达式。
使用命令时可能会下面的情况:
(gdb) search func
Expression not found
表示搜索的范围没有出现要寻找的字符串或者定位到了代码行的末尾。
Signals
信号是程序中可能发生的异步事件。操作系统定义了可能的信号类型,并为每种信号提供了名称和数字。例如,在Unix中,SIGINT是程序在键入中断字符(通常是Ctrl-c)时获得的信号;SIGSEGV是程序从引用内存中远离所有使用区域的位置获得的信号;SIGALRM发生在闹钟计时器关闭时(仅当程序请求警报时发生)。
一些信号,包括SIGALRM,是程序功能的正常部分。其他的,如SIGSEGV,指示错误;如果程序没有事先指定其他处理信号的方法,这些信号是致命的(它们会立即杀死您的程序)。SIGINT并不指示程序中的错误,但它通常是致命的,因此它可以执行中断的目的:杀死程序。
gdb能够检测程序中信号的任何出现。你可以提前告诉gdb对每种信号做什么。
通常,gdb的设置是让像SIGALRM这样的非错误信号静默传递给程序(以免干扰它们在程序运行中的作用),但在错误信号发生时立即停止程序。您可以使用handle命令更改这些设置。
info signals
info handle
打印一张表格,列出所有类型的信号,以及gdb如何被告知处理每一个信号。您可以使用此选项查看所有定义类型信号的信号编号。
info signals
sig
类似,但只打印有关指定信号编号的信息。
info handle是info signals的别名。
handle
signal [keywords...
]
更改gdb处理信号的方式。信号可以是信号的编号或其名称(开头有或没有SIG');形式为'low-high'的信号编号列表;或单词'all',意思是所有已知信号。
下面描述的可选参数关键字说明要进行的更改。
handle
命令允许的keywords可以缩写。他们的全名是:
nostop
当出现此信号时,gdb不应停止程序。它可能仍然会打印一条消息,告诉你信号已经进来。
stop
当这个信号发生时,gdb应该停止你的程序。需要注意的是,设置 stop 的同时,默认也会设置 print。
print
gdb应在此信号发生时打印一条消息。
noprint
gdb根本不应提及信号的发生。需要注意的是,设置 noprint 的同时,默认也会设置 nostop
pass
noignore
gdb应该允许您的程序看到此信号;您的程序可以处理该信号,否则,如果信号是致命的且未处理,它可能会终止。pass和noignore是同义词。
nopass
ignore
gdb不应该允许您的程序看到此信号。nopass和ignore是同义词。
当信号停止程序时,在您继续之前,该信号对程序是不可见的。如果pass
在当时对有关信号有效,则程序将看到信号。换句话说,在gdb报告信号后,您可以使用带有pass或nopass的句柄命令来控制您的程序在继续时是否看到该信号。
对于非错误信号,比如 SIGALRM
, SIGWINCH
and SIGCHLD
,默认设置为 nostop
, noprint
, pass
,对于错误信号默认设置为 stop
, print
, pass
。
您还可以使用signal
命令防止程序看到信号,或使其看到通常看不到的信号,或随时向其提供任何信号。例如,如果您的程序因某种内存引用错误而停止,您可能会将正确的值存储到错误的变量中,并继续,希望看到更多的执行;但您的程序可能会在看到致命信号后立即终止。要防止这种情况,您可以继续使用 ``signal 0`'。请参阅Giving your Program a Signal.。
在某些目标上,gdb可以在实际传送到正在调试的程序之前检查与拦截信号相关的额外信号信息。此信息由便利变量$_siginfo
导出,并由内核在接收信号时传递给信号处理器的数据组成。信息本身的数据类型取决于目标。您可以使用ptype
$_siginfo
命令查看数据类型。在Unix系统上,它通常对应于signal.h
系统报头中定义的标准siginfo_t
类型。
这里有一个示例,在gnu/Linux系统上,打印引起分段错误的杂散引用地址。
(gdb) continue
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400766 in main ()
69 *(int *)p = 0;
(gdb) ptype $_siginfo
type = struct {int si_signo;int si_errno;int si_code;union {int _pad[28];struct {...} _kill;struct {...} _timer;struct {...} _rt;struct {...} _sigchld;struct {...} _sigfault;struct {...} _sigpoll;} _sifields;
}
(gdb) ptype $_siginfo._sifields._sigfault
type = struct {void *si_addr;
}
(gdb) p $_siginfo._sifields._sigfault.si_addr
$1 = (void *) 0x7ffff7ff7000
根据目标支持,$_siginfo也可能是可写的。
Commods
set step-mode on
进入不带调试信息的函数
finish
退出正在调试的函数. 当单步调试一个函数时,如果不想继续跟踪下去了,可以有两种方式退出。 第一种用“ finish ”命令,这样函数会继续执行完,并且打印返回值,然后等待输入接下来的命令
return
第二种用“ return ”命令,这样函数不会继续执行下面的语句,而是直接返回。也可以用“ return expression ”命令指定函数的返回值
disassemble
查看函数汇编代码
save breakpoints file-name-to-save
保存已经设置的断点
source file-name-to-save
下次调试时,可以使用如下命令批量设置保存的断点
break … if cond
只有在条件满足时,断点才会被触发
ignore bnum count
在设置断点以后,可以忽略断点,命令是“ ignore bnum count ”:意思是接下来 count 次编号 为 bnum 的断点触发都不会让程序中断,只有第 count + 1 次断点触发才会让程序中断print
elements number-of-elements
如果要打印大数组的内容,缺省最多会显示200个元素,可以使用命令,设置这个最大限制数
p array[index]@num
打印数组中任意连续元素的值,其中 index 是数组索引(从0开始计数), num 是连续多少个元素
set print array-indexes on
打印数组的索引下标
set follow-fork-mode child
调试子进程
set detach-on-fork off
同时调试父进程和子进程,并且在调试一个进程时,另外一个进程处于挂起状态
set schedule-multiple on
让父子进程都同时运行
i inferiors
查看进程状 态
inferior infno
切换进程
disas /m fun
将源程序和汇编指令映射起来,每一条C语句下面是对应的汇编代码
i signals
查看信号处理信息
第一项( Signal ):标示每个信号。
第二项( Stop ):表示被调试的程序有对应的信号发生时,gdb是否会暂停程序。
第三项( Print ):表示被调试的程序有对应的信号发生时,gdb是否会打印相关信息。
第四项( Pass to program ):gdb是否会把这个信号发给被调试的程序。
第五项( Description ):信号的描述信息。
handle signal stop/nostop
信号发生时是否暂停程序, 需要注意的是,设置 stop 的同时,默 认也会设置 print
handle signal print/noprint
信号发生时是否打印信号信息,需要注意的是,设置 noprint 的同时,默认也会设置 nostop
handle signal pass(noignore)/nopass(ignore)
信号发生时是否把信号丢给程序处理
signal signal_name
给程序发送信号,当被调试程序停止后,可以用“ signal signal_name ”命令让程序继续运 行,但会立即给程序发送信号
可以使用“ signal 0 ”命令使程序重新运行,但不发送任何信号给进程
gdb -tui program
启动gdb时指定“ -tui ”参数(例如: gdb -tui program ),或者运行gdb过程中使用“ Crtl+X+A ”组 合键,都可以进入图形化调试界面,退出图形化调试界面也是用“ Crtl+X+A ”组合键
layout asm
使用gdb图形化调试界面时,可以使用“ layout asm ”命令显示汇编代码窗口
layout split
如果既想显示源代码,又想显示汇编代码,可以使用“ layout split ”命令