1. next
next
命令(简写为 n
)是让 GDB
调到下一条命令去执行,这里的下一条命令不一定是代码的下一行,而是根据程序逻辑跳转到相应的位置。举个例子:
int a = 0;
if (a == 9)
{
print("a is equal to 9.\n");
}
int b = 10;
print("b = %d.\n", b);
如果当前 GDB
中断在上述代码第 2 行,此时输入 next
命令 GDB
将调到第 7 行,因为这里的 if
条件并不满足。
这里有一个小技巧,在 GDB
命令行界面如果直接按下回车键,默认是将最近一条命令重新执行一遍,因此,当使用 next
命令单步调试时,不必反复输入 n
命令,直接回车就可以了。
next
命令调试遇到函数调用直接跳过,不进入函数体内部。
2. step
step
命令(简写为 s
)就是“单步步入”(step into
),顾名思义,就是遇到函数调用,进入函数内部。
还有一个需要注意的地方,就是当函数的参数也是函数调用时,我们使用 step
命令会依次进入各个函数,那么顺序是什么呢?举个例子,看下面这段代码:
1 int fun1(int a, int b)
2 {
3 int c = a + b;
4 c += 2;
5 return c;
6 }
7
8 int func2(int p, int q)
9 {
10 int t = q * p;
11 return t * t;
12 }
13
14 int func3(int m, int n)
15 {
16 return m + n;
17 }
18
19 int main()
20 {
21 int c;
22 c = func3(func1(1, 2), func2(8, 9));
23 printf("c=%d.\n", c);
24 return 0;
25 }
上述代码,程序入口是 main()
函数,在第 22 行 func3
使用 func1
和 func2
的返回值作为自己的参数,在第 22 行输入 step 命令,会先进入哪个函数呢?这里就需要补充一个知识点了—— 函数调用方式,我们常用的函数调用方式有 _cdecl
和 _stdcall
,C++
非静态成员函数的调用方式是 _thiscall
。在这些调用方式中,函数参数的传递本质上是函数参数的入栈过程,而这三种调用方式参数的入栈顺序都是从右往左的,因此,这段代码中并没有显式标明函数的调用方式,采用默认 _cdecl
方式。
当我们在第 22 行代码处输入 step
先进入的是 func2()
,当从 func2()
返回时再次输入 step
命令会接着进入 func1()
,当从 func1
返回时,此时两个参数已经计算出来了,这时候会最终进入 func3()
。理解这一点,在遇到这样的代码时,才能根据需要进入我们想要的函数中去调试。
3. finish 和 return
实际调试时,我们在某个函数中调试一段时间后,不需要再一步步执行到函数返回处,希望直接执行完当前函数并回到上一层调用处,就可以使用 finish
命令。与 finish
命令类似的还有 return
命令,return
命令的作用是结束执行当前函数,还可以指定该函数的返回值。
这里需要注意一下二者的区别:
finish
命令会执行函数到正常退出该函数;return
命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了;
我们用一个例子来验证一下:
1 #include <stdio.h>
2
3 int func()
4 {
5 int a = 9;
6 printf("a=%d.\n", a);
7
8 int b = 8;
9 printf("b=%d.\n", b);
10 return a + b;
11 }
12
13 int main()
14 {
15 int c = func();
16 printf("c=%d.\n");
17
18 return 0;
19 }
在 main()
函数处加一个断点,然后运行程序,在第 15 行使用 step
命令进入 func()
函数,接着单步到代码第 8 行,直接输入 return
命令,这样 func()
函数剩余的代码就不会继续执行了,因此
printf("b=%d.\n", b);
这一行就没有输出。同时由于我们没有在 return
命令中指定这个函数的返回值,因而最终在 main()
函数中得到的变量 c
的值是一个脏数据。这也就验证了我们上面说的:return
命令在当前位置立即结束当前函数的执行,并返回到上一层调用。
(gdb) b main
Breakpoint 1 at 0x40057d: file test.c, line 15.
(gdb) r
Starting program: /root/testreturn/test
Breakpoint 1, main () at test.c:15
15 int c = func();
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64
(gdb) s
func () at test.c:5
5 int a = 9;
(gdb) n
6 printf("a=%d.\n", a);
(gdb) n
a=9.
8 int b = 8;
(gdb) return
Make func return now? (y or n) y
#0 0x0000000000400587 in main () at test.c:15
15 int c = func();
(gdb) n
16 printf("c=%d.\n");
(gdb) n
c=-134250496.
18 return 0;
(gdb)
再次用 return
命令指定一个值试一下,这样得到变量 c
的值应该就是我们指定的值。
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/testreturn/test
Breakpoint 1, main () at test.c:15
15 int c = func();
(gdb) s
func () at test.c:5
5 int a = 9;
(gdb) n
6 printf("a=%d.\n", a);
(gdb) n
a=9.
8 int b = 8;
(gdb) return 9999
Make func return now? (y or n) y
#0 0x0000000000400587 in main () at test.c:15
15 int c = func();
(gdb) n
16 printf("c=%d.\n");
(gdb) n
c=-134250496.
18 return 0;
(gdb) p c
$1 = 9999
(gdb)
仔细观察上述代码应该会发现,虽然用 return
命令修改了函数的返回值,当使用 print
命令打印 c
值的时候,c
值也确实被修改成了 9999 ,但是 GDB
本身认为的程序执行逻辑中,打印出来的 c
仍然是脏数据。这点在实际调试时需要注意一下。
我们再对比一下使用 finish
命令来结束函数执行的结果。
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/testreturn/test
Breakpoint 1, main () at test.c:15
15 int c = func();
(gdb) s
func () at test.c:5
5 int a = 9;
(gdb) n
6 printf("a=%d.\n", a);
(gdb) n
a=9.
8 int b = 8;
(gdb) finish
Run till exit from #0 func () at test.c:8
b=8.
0x0000000000400587 in main () at test.c:15
15 int c = func();
Value returned is $3 = 17
(gdb) n
16 printf("c=%d.\n");
(gdb) n
c=-134250496.
18 return 0;
(gdb)
结果和我们预期的一样,finish
正常结束函数,剩余的代码也会被正常执行。
4. until
实际调试时,还有一个 until
命令(简写为 u
)可以指定程序运行到某一行停下来,想直接跳到第 1839 行,可以直接输入 u 1839,这样就能快速执行完中间的代码。当然,也可以先在第 1839 行加一个断点,然后使用 continue
命令运行到这一行,但是使用 until
命令会更简便。
(gdb) n
1815 signal(SIGHUP, SIG_IGN);
(gdb) u 1839
initServer () at server.c:1839
1839 createSharedObjects();
(gdb)
5. jump
jump
命令基本用法是:
jump <location>
location
可以是程序的行号或者函数的地址,jump
会让程序执行流跳转到指定位置执行,当然其行为也是不可控制的,例如您跳过了某个对象的初始化代码,直接执行操作该对象的代码,那么可能会导致程序崩溃或其他意外行为。jump
命令可以简写成 j
,但是不可以简写成 jmp
,其使用有一个注意事项,即如果 jump
跳转到的位置后续没有断点,那么 GDB
会执行完跳转处的代码会继续执行。举个例子:
1 int somefunc()
2 {
3 //代码A
4 //代码B
5 //代码C
6 //代码D
7 //代码E
8 //代码F
9 }
假设我们的断点初始位置在行号 3 处(代码 A),这个时候我们使用 jump 6,那么程序会跳过代码 B 和 C 的执行,执行完代码 D( 跳转点),程序并不会停在代码 6 处,而是继续执行后续代码,因此如果我们想查看执行跳转处的代码后的结果,需要在行号 6、7 或 8 处设置断点。
jump 命令除了跳过一些代码的执行外,还有一个妙用就是可以执行一些我们想要执行的代码,而这些代码在正常的逻辑下可能并不会执行(当然可能也因此会产生一些意外的结果,这需要读者自行斟酌使用)。举个例子,假设现在有如下代码:
1 #include <stdio.h>
2 int main()
3 {
4 int a = 0;
5 if (a != 0)
6 {
7 printf("if condition\n");
8 }
9 else
10 {
11 printf("else condition\n");
12 }
13
14 return 0;
15 }
我们在行号 4 、14 处设置一个断点,当触发行号 4 处的断点后,正常情况下程序执行流会走 else 分支,我们可以使用 jump 7 强行让程序执行 if 分支,接着 GDB 会因触发行号 14 处的断点而停下来,此时我们接着执行 jump 11,程序会将 else 分支中的代码重新执行一遍。整个操作过程如下:
[root@localhost testcore]# gdb test
Reading symbols from /root/testcore/test...done.
(gdb) b main
Breakpoint 1 at 0x400545: file main.cpp, line 4.
(gdb) b 14
Breakpoint 2 at 0x400568: file main.cpp, line 14.
(gdb) r
Starting program: /root/testcore/test
Breakpoint 1, main () at main.cpp:4
4 int a = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libstdc++-4.8.5-36.el7.x86_64
(gdb) jump 7
Continuing at 0x400552.
if condition
Breakpoint 2, main () at main.cpp:14
14 return 0;
(gdb) jump 11
Continuing at 0x40055e.
else condition
Breakpoint 2, main () at main.cpp:14
14 return 0;
(gdb) c
Continuing.
[Inferior 1 (process 13349) exited normally]
(gdb)
gdb
的命令成百上千,下表中只列出了一些在调试过程中比较常用的命令。