1. 自动显示变量的值
使用 print
或者 p
命令来显示变量的值,但是有一个问题,即如果想要查看某个变量的值,需要不停地使用 print
命令。这对于需要观察那些不停变化的变量值来说,使用 p
命令就不太方便了,因为需要使用多次。
gdb
还有另外一个 display
命令,每次程序暂停都可以自动显示变量值。语法如下:
display 变量名
后面可以跟多个变量名,比如 display {var1,var2,var3}
。
我们使用 display
来自动观察 fun_test
中的变量 x
,而不是使用 print
命令,源码 demo.cpp
代码如下:
#include <iostream>
#include <string>
void fun_test(int a, const char *str)
{
printf("a is %d, str is %s\n", a, str);
}
int main(int argc, char *argv[])
{
for (int i = 0; i < 10; i++)
{
fun_test(i, "test");
}
}
调试过程如下:
(gdb) b fun_test
Breakpoint 1 at 0x799: file demo.cpp, line 6.
(gdb) r
Starting program: /home/wohu/cppProject/book_debug/chapter_3.1/demo
Breakpoint 1, fun_test (a=0, str=0x5555555548e9 "test") at demo.cpp:6
6 printf("a is %d, str is %s\n", a, str);
(gdb) p a
$1 = 0
(gdb) p a
$2 = 0
(gdb) c
Continuing.
a is 0, str is test
Breakpoint 1, fun_test (a=1, str=0x5555555548e9 "test") at demo.cpp:6
6 printf("a is %d, str is %s\n", a, str);
(gdb) p a
$3 = 1
(gdb) c
Continuing.
a is 1, str is test
Breakpoint 1, fun_test (a=2, str=0x5555555548e9 "test") at demo.cpp:6
6 printf("a is %d, str is %s\n", a, str);
(gdb) p a
$4 = 2
(gdb) display a
1: a = 2
(gdb) c
Continuing.
a is 2, str is test
Breakpoint 1, fun_test (a=3, str=0x5555555548e9 "test") at demo.cpp:6
6 printf("a is %d, str is %s\n", a, str);
1: a = 3
(gdb) c
Continuing.
a is 3, str is test
Breakpoint 1, fun_test (a=4, str=0x5555555548e9 "test") at demo.cpp:6
6 printf("a is %d, str is %s\n", a, str);
1: a = 4
(gdb) c
Continuing.
a is 4, str is test
Breakpoint 1, fun_test (a=5, str=0x5555555548e9 "test") at demo.cpp:6
6 printf("a is %d, str is %s\n", a, str);
1: a = 5
(gdb)
如果 display
命令后面跟多个变量名,则必须要求这些变量的类型相同(比如都是整型变量)。如果长度不相同,则需要分开使用。
可以在 gdb
中输入 info display
命令来查看已经设置的自动显示的变量信息,如图所示。
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1: y a (cannot be evaluated in the current context)
(gdb)
如果不需要某些变量自动显示,则可以使用 undisplay 编号
的方式来取消自动变量的显示。例如,想要取消上面变量 a
的自动显示,则可以在 gdb
里面输入 undisplay 1
命令,a
变量的自动显示被取消,如图所示。这样一来,再次使用 info display
命令的时候不会显示 a
的信息。
(gdb) undisplay 1
(gdb) info display
There are no auto-display expressions now.
(gdb)
如果要取消所有变量的自动显示,可以使用 undisplay
命令来。在使用 undisplay
命令时会收到确认信息,确认是否全部取消自动显示。如果输入 y
,则取消所有的自动显示。delete display
命令也可以删除所有的自动显示。如果只想删除部分变量的自动显示,可以使用 delete display 序号
的方式。
比如要删除中的{x,a}的自动显示,则输入以下命令:
delete display 1
除了删除自动显示,还可以暂时禁用自动显示,在需要的时候可以再次启用某些变量的自动显示。比如图3-64中自动显示的编号 1 和 2,如果要暂时禁用编号为 1 的自动显示,则可以使用以下命令:
disable display 1
如果想要恢复编号为1的变量的自动显示,则可以使用以下命令来恢复:
enable display 1
2. 显示源代码
gdb
可以在调试的时候显示源代码信息。查看源代码的命令是 list
或者 l
。当程序命中断点或者暂停后可以使用 list
命令查看相关的源代码。
因为加上编译选项 -g
后,生成的可执行文件中包含调试信息,并且保存了对应的源文件信息(只是保存了源文件名等信息),所以在查看源代码时,要确保对应的源文件存在,否则无法查看。
在程序中断时,可以使用 l
命令来查看源代码信息。默认情况下,使用 l
命令可以显示10行源代码—当前代码行的前面 5 行和后面 5 行,
(gdb) c
Continuing.
Breakpoint 1, fun_test (a=7, str=0x5555555548e9 "test") at demo.cpp:6
6 printf("a is %d, str is %s\n", a, str);
(gdb) l
1 #include <iostream>
2 #include <string>
3
4 void fun_test(int a, const char *str)
5 {
6 printf("a is %d, str is %s\n", a, str);
7 }
8
9 int main(int argc, char *argv[])
10 {
(gdb)
继续执行 l
命令,则会继续从当前行往后显示10行代码。如果执行 l-
命令,则会从当前代码行往前显示10行代码,
(gdb) l
11 for (int i = 0; i < 10; i++)
12 {
13 fun_test(i, "test");
14 }
15 }
(gdb)
执行 l
命令时,每次默认显示 10 行代码,如果觉得每次显示的代码太少,可以通过 set listsize
命令来改变每次显示代码的行数。比如,希望每次能够显示 20 行代码,可执行下述命令:
set listsize 20
这样,每次调用 list
命令时,就会显示 20 行代码。
还可以使用 list
命令查看指定函数的代码,语法为 list 函数名
。比如,我们要查看 fun_test
函数的源代码,可以使用下述命令:
list fun_test
显示函数与显示普通代码的规则相同。
- 受到行数的限制,比如我们刚设置了每次显示20行代码,则显示
fun_test
函数时只显示20行代码; - 仍然会以上下文的方式显示函数代码,即显示函数前面 10 行代码和函数后面 10 行代码;
(gdb) l fun_test
1 #include <iostream>
2 #include <string>
3
4 void fun_test(int a, const char *str)
5 {
6 printf("a is %d, str is %s\n", a, str);
7 }
8
9 int main(int argc, char *argv[])
10 {
(gdb)
如果想要查看指定文件的指定行代码,则可以使用下述命令:
list 文件名:行号
如果查看的是当前文件的代码行,则可以省略文件名。比如我们要查看 demo.cpp
的第 100 行代码,则可以使用下述命令进行查看:
l 100
示例过程
(gdb) l demo.cpp:9
4 void fun_test(int a, const char *str)
5 {
6 printf("a is %d, str is %s\n", a, str);
7 }
8
9 int main(int argc, char *argv[])
10 {
11 for (int i = 0; i < 10; i++)
12 {
13 fun_test(i, "test");
(gdb) l 13
8
9 int main(int argc, char *argv[])
10 {
11 for (int i = 0; i < 10; i++)
12 {
13 fun_test(i, "test");
14 }
15 }
(gdb)
3. watch
watch
可以用来监视一个变量或者一段内存,当这个变量或者该内存处的值发生变化时,GDB
就会中断下来。被监视的某个变量或者某个内存地址会产生一个 watch point
(观察点)。
适用场景:
有一个变量其值被意外地改掉了,通过单步调试或者挨个检查使用该变量的代码工作量会非常大,如何快速地定位到该变量在哪里被修改了?其实使用 watch
命令就可以通过添加硬件断点来达到监视数据变化的目的。
watch
命令的使用方式是 watch 变量名或内存地址
,一般有以下几种形式:
- 形式一:整型变量
int i;
watch i
- 形式二:指针类型
char *p;
watch p 与 watch *p
注意:watch p
与 watch *p
是有区别的,前者是查看 *(&p)
,是 p
变量本身;后者是 p
所指内存的内容。我们需要查看地址,因为目的是要看某内存地址上的数据是怎样变化的。
- 形式三:
watch
一个数组或内存区间
char buf[128];
watch buf
这里是对 buf
的 128 个数据进行了监视,此时不是采用硬件断点,而是用软中断实现的。用软中断方式去检查内存变量是比较耗费 CPU
资源的,精确地指明地址是硬件中断。
注意:当设置的观察点是一个局部变量时,局部变量无效后,观察点也会失效。在观察点失效时 GDB
可能会提示如下信息:
Watchpoint 2 deleted because the program has left the block in which its expression is valid.
4. 查看内存
test_memory
函数内容
定义了字符串类型、int 变量和一个结构体,使用 gdb
启动调试,执行 b test_memory
命令为test_memory
函数设置一个断点,或者直接在 124 行设置一个断点。断点命中时,使用 x
命令查看各个变量的内存信息。x
命令的语法如下:
x /选项 地址
先查看字符串变量 str
的内存信息。执行 x str
,默认以十六进制显示。由于 str
是字符串,所以也可以使用字符串的方式查看。使用命令 x /s str
还可以以十进制方式查看、设定显示的宽度等,如图所示。
以十六进制方式查看时,显示的内存内容为0x74736574,对应的字符分别为t、s、e、t,即在内存中存储的内容刚好与我们看到的情况相反。如果以 x /s str
的方式查看,则会直接显示字符串的内容。
再来查看 int
型变量 number
在内存中的信息。因为 number
不是一个指针,所以我们要首先找到它的地址。可以使用 p &number
命令来查看 number
对应的地址,然后再使用 x
来查看 number
地址对应的内存数据。当然也可以直接使用 x &number
的方式来查看 number
地址对应的内存信息,如图所示。
在代码中,我们为 number
赋值为 0x12345678
,但是内存中显示的却是 0x78563412
,原因是字节在x86
架构中是按照小端方式存储的,第2章中已经介绍过。小端存储是指字节序数据的尾端数据存放在低地址部分,所以与我们看到的顺序刚好相反。
变量node存储的数据是一个结构体类型。来看看node在内存中到底是如何存储的。在gdb中输入命令x/16s node,如图3-73所示。
从图中可以看到,node在内存中的存储顺序与结构体中声明成员的顺序一致,即按照性别、ID和姓名来存储。显示性别的起始地址是0x614e70,显示ID的起始地址是0x614e74,“d”对应的是十进制的100。可以发现,ID的起始地址和性别的起始地址相差4,但是我们定义gender成员时使用的是char gender[3],明明只声明了3字节,最后却在内存中占用了4字节。
这是因为结构体在内存中会进行对齐和补齐操作,默认是按照4字节对齐。尽管声明的是3字节,但是要按照4字节去对齐,所以需要补齐1字节,这导致gender占用了4字节空间。同样,成员Name也会补齐到8字节,所以整个结构体在内存中会占用16字节,可以使用p sizeof(TEST_NODE)命令进行查看。在图3-73中也可以发现,整个结构体的大小是16字节。
命令x并不局限于查看变量的内存信息,无论是函数地址、变量地址,还是其他地址,只要地址合法而且可以访问,都可以使用 x 命令来查看。