各位编程爱好者、未来的软件架构师们,晚上好!
欢迎来到今天的技术讲座。我是你们的讲师,一位在软件开发领域摸爬滚打了多年的老兵。今天,我们将共同探索一个对所有开发者来说都至关重要的技能——调试。具体来说,我们将聚焦于开源世界中最强大、最经典的调试器之一:GDB。
想象一下,你辛辛苦苦写了几百行甚至几千行代码,满怀信心地编译运行,结果程序崩溃了,或者输出了一个完全不符合预期的结果。这时候,你是不是感到头疼?是不是想知道程序在执行到哪一行时出了问题?某个变量在那一刻的值到底是什么?函数是如何被层层调用的?
这就是调试器大显身手的时候。它就像一个X光机,能够穿透你的代码,让你在程序运行时,暂停它,检查它,甚至修改它。而GDB,就是这台X光机中的“瑞士军刀”。对于初学者来说,掌握GDB的基本用法,尤其是如何在线查看变量值和追踪函数调用栈,是迈向独立解决问题的第一步,也是成为一名高效程序员的必经之路。
今天的讲座,我将以实战为导向,结合大量的代码示例,深入浅出地讲解GDB的核心调试指令。我们将从最基础的GDB启动与退出,到设置断点,控制程序执行流,再到今天的主题:如何精确地检查变量状态,以及如何清晰地理解函数间的调用关系。
无需担心,即使你是GDB的零基础新手,只要跟着我的讲解,一步步操作,你将很快掌握这些强大的调试技能。让我们开始吧!
第一章:GDB调试的基石——环境准备与基本操作
在深入变量查看和栈追踪之前,我们必须确保我们的程序能够被GDB调试,并且了解GDB的一些最基本的操作。
1.1 编译你的代码:加上调试信息
GDB之所以能让你看到源代码、变量名,并能根据行号设置断点,是因为编译器在生成可执行文件时,额外嵌入了调试信息。这些信息告诉GDB源代码与机器码之间的对应关系。
要包含这些调试信息,你需要在编译时加上 -g 选项。
示例代码:simple_program.c
#include <stdio.h>
#include <stdlib.h> // For malloc and free
int calculate_sum(int a, int b) {
int sum = a + b;
return sum;
}
void process_array(int* arr, int size) {
printf("Processing array elements:n");
for (int i = 0; i <= size; ++i) { // Potential off-by-one error
if (i < size) {
printf("arr[%d] = %dn", i, arr[i]);
} else {
printf("Attempting to access arr[%d] (out of bounds for size %d)n", i, size);
}
}
}
int main() {
int x = 10;
int y = 20;
int result = calculate_sum(x, y);
printf("Sum of %d and %d is: %dn", x, y, result);
int data_size = 5;
int* my_array = (int*)malloc(data_size * sizeof(int));
if (my_array == NULL) {
fprintf(stderr, "Memory allocation failed!n");
return 1;
}
for (int i = 0; i < data_size; ++i) {
my_array[i] = i * 10;
}
process_array(my_array, data_size);
free(my_array); // Don't forget to free allocated memory
my_array = NULL; // Best practice: set pointer to NULL after freeing
printf("Program finished successfully.n");
return 0;
}
编译指令:
gcc -g simple_program.c -o simple_program
现在,simple_program 可执行文件就包含了调试信息,可以被GDB完美地调试了。
1.2 启动与退出GDB
启动GDB非常简单,只需在终端中输入 gdb 后面跟着你的可执行文件名。
启动GDB:
gdb simple_program
你会看到GDB的提示符 (gdb),表示你已进入GDB环境。
退出GDB:
要退出GDB,你可以输入 quit 或 q。
(gdb) quit
1.3 GDB调试的基本流程控制
在能够查看变量和栈之前,我们首先需要学会如何控制程序的执行。
核心控制命令:
| 命令 | 缩写 | 描述 |
|---|---|---|
break |
b |
设置断点。程序执行到断点处会暂停。 |
run |
r |
启动程序执行。如果程序已经启动过,会重新启动。 |
continue |
c |
让程序从当前暂停点继续执行,直到遇到下一个断点或程序结束。 |
next |
n |
单步执行,不进入函数内部(跳过函数调用)。 |
step |
s |
单步执行,会进入函数内部(如果当前行是函数调用,则进入该函数的第一行)。 |
finish |
运行到当前函数结束并返回。 | |
list |
l |
查看源代码。list <line_number> 或 list <function_name>。 |
示例:设置断点并单步执行
(gdb) b main
Breakpoint 1 at 0x1183: file simple_program.c, line 23.
(gdb) run
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) n
24 int y = 20;
(gdb) n
25 int result = calculate_sum(x, y);
(gdb) s
calculate_sum (a=10, b=20) at simple_program.c:6
6 int sum = a + b;
(gdb) n
7 return sum;
(gdb) finish
Run till exit from #0 calculate_sum (a=10, b=20) at simple_program.c:7
0x0000555555555193 in main () at simple_program.c:25
25 int result = calculate_sum(x, y);
Value returned is $1 = 30
(gdb)
通过这些基本命令,我们现在已经掌握了在GDB中暂停程序执行流的能力。接下来,就是如何在程序暂停时,深入探查其内部状态的关键时刻。
第二章:在线查看变量值——洞察程序内部状态
当程序在断点处暂停时,GDB允许你“窥视”任何可访问的变量的值,无论是基本类型、指针、数组还是结构体。这是调试中最常用,也是最有力的功能之一。
2.1 print 命令:随时查看变量
print (缩写 p) 是GDB中查看变量值的核心命令。你可以在程序暂停的任何时候使用它来打印表达式的值。
语法: print <expression>
2.1.1 查看基本类型变量
当程序暂停在 main 函数的第25行(即 int result = calculate_sum(x, y); 之后),我们可以查看 x, y, result 的值。
(gdb) b 26
Breakpoint 2 at 0x555555555198: file simple_program.c, line 26.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Breakpoint 2, main () at simple_program.c:26
26 printf("Sum of %d and %d is: %dn", x, y, result);
(gdb) print x
$1 = 10
(gdb) print y
$2 = 20
(gdb) print result
$3 = 30
GDB会为每次 print 命令的结果分配一个编号(如 $1, $2),你可以在后续的GDB会话中通过这些编号引用它们。
2.1.2 查看指针及其指向的值
在 main 函数中,我们有 int* my_array。当程序暂停在 process_array 调用之后,我们可以查看 my_array 指针本身的值(内存地址)和它所指向的内容。
(gdb) b 39
Breakpoint 3 at 0x55555555523a: file simple_program.c, line 39.
(gdb) c
Continuing.
Breakpoint 3, main () at simple_program.c:39
39 free(my_array);
(gdb) print my_array
$4 = (int *) 0x5555555592a0 // my_array 的内存地址
(gdb) print *my_array
$5 = 0 // my_array[0] 的值
(gdb) print my_array[0]
$6 = 0
(gdb) print my_array[1]
$7 = 10
(gdb) print my_array[4]
$8 = 40
2.1.3 查看数组
要查看整个数组的内容,你可以使用 print <array_name>@<length>。
(gdb) print my_array@5
$9 = {0, 10, 20, 30, 40} // 打印 my_array 从起始地址开始的5个int值
2.1.4 格式化打印
print 命令支持多种格式化输出,这在查看不同类型的数据时非常有用。
| 格式符 | 描述 | 示例 |
|---|---|---|
/x |
十六进制 | print/x x (显示 x 的十六进制值) |
/d |
十进制 | print/d x (显示 x 的十进制值) |
/u |
无符号十进制 | print/u x |
/o |
八进制 | print/o x |
/t |
二进制 | print/t x |
/c |
字符 | print/c 65 (显示字符 ‘A’) |
/s |
字符串 | print/s "hello" (显示字符串) |
示例:
(gdb) print/x x
$10 = 0xa // 10的十六进制是A
(gdb) print/t x
$11 = 1010 // 10的二进制是1010
(gdb) print/c 65
$12 = 65 'A'
2.2 display 命令:自动追踪变量
如果你想在每次程序暂停时,自动打印某个变量的值,而不是每次都手动输入 print,那么 display 命令是你的好帮手。
语法: display <expression>
示例:追踪 i 变量在循环中的变化
我们来调试 process_array 函数,看看循环变量 i 的变化。
(gdb) b process_array
Breakpoint 4 at 0x5555555551c6: file simple_program.c, line 10.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:10
10 printf("Processing array elements:n");
(gdb) n
Processing array elements:
11 for (int i = 0; i <= size; ++i) {
(gdb) display i
1: i = 0
(gdb) display size
2: size = 5
(gdb) n
12 if (i < size) {
1: i = 0
2: size = 5
(gdb) n
13 printf("arr[%d] = %dn", i, arr[i]);
1: i = 0
2: size = 5
(gdb) c
Continuing.
arr[0] = 0
Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
1: i = 1
2: size = 5
(gdb) c
Continuing.
arr[1] = 10
Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
1: i = 2
2: size = 5
(gdb) c
Continuing.
arr[2] = 20
Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
1: i = 3
2: size = 5
(gdb) c
Continuing.
arr[3] = 30
Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
1: i = 4
2: size = 5
(gdb) c
Continuing.
arr[4] = 40
Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
1: i = 5 // i 已经等于 size,但循环条件是 i <= size,这是潜在的越界访问!
2: size = 5
(gdb) n
12 if (i < size) { // 条件 i < size 为假
1: i = 5
2: size = 5
(gdb) n
15 printf("Attempting to access arr[%d] (out of bounds for size %d)n", i, size);
1: i = 5
2: size = 5
(gdb) c
Continuing.
Attempting to access arr[5] (out of bounds for size 5)
通过 display 命令,我们清晰地看到了 i 如何从0变化到5,最终导致了数组越界访问的提示信息。这是一个典型的“差一错误”(off-by-one error)。
管理 display 列表:
info display: 查看所有正在display的表达式。undisplay <display_number>: 停止自动显示某个表达式。disable display <display_number>: 禁用某个表达式的自动显示(但不移除)。enable display <display_number>: 重新启用。
2.3 x 命令:检查内存内容
有时候,你可能想直接查看某个内存地址的内容,而不是通过变量名。这在处理指针、内存损坏或汇编级别调试时尤其有用。x (examine) 命令允许你做到这一点。
语法: x/<nfu> <address>
n: (可选) 要显示的内存单元数量。默认为1。f: (可选) 格式。与print类似,如x(十六进制),d(十进制),u(无符号十进制),o(八进制),t(二进制),a(地址),c(字符),s(字符串),i(机器指令)。默认为上次使用的格式。u: (可选) 内存单元大小。b: byte (1字节)h: halfword (2字节)w: word (4字节)g: giant word (8字节)
默认为上次使用的大小。
示例:检查 my_array 的内存
当程序暂停在 free(my_array); 之前,my_array 指向的是我们分配的5个 int 元素的内存块。
(gdb) b 39
Breakpoint 3 at 0x55555555523a: file simple_program.c, line 39.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Breakpoint 3, main () at simple_program.c:39
39 free(my_array);
(gdb) print my_array
$13 = (int *) 0x5555555592a0
(gdb) x/5xw my_array // 从 my_array 地址开始,以十六进制格式显示5个word (4字节)
0x5555555592a0: 0x00000000 0x0000000a 0x00000014 0x0000001e
0x5555555592b0: 0x00000028
这里 0x00000000 是0,0x0000000a 是10,0x00000014 是20,0x0000001e 是30,0x00000028 是40。这与我们用 print my_array@5 看到的结果完全一致。
2.3.1 检查栈上的局部变量内存
我们可以通过 print &x 获取变量 x 的地址,然后用 x 命令查看其内存内容。
(gdb) b main
Breakpoint 1 at 0x555555555183: file simple_program.c, line 23.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) n
24 int y = 20;
(gdb) print &x
$14 = (int *) 0x7fffffffdc2c // 变量 x 在栈上的地址
(gdb) x/d &x // 以十进制格式查看 x 的值
0x7fffffffdc2c: 10
(gdb) x/x &x // 以十六进制格式查看 x 的值
0x7fffffffdc2c: 0x0000000a
2.4 set variable 命令:运行时修改变量值
GDB不仅能让你查看变量,还能让你在程序暂停时修改它们的值。这对于测试不同输入、修复临时错误或跳过某些逻辑非常有用。
语法: set variable <variable_name> = <new_value>
示例:修改 x 的值
假设我们想看看如果 x 是100,calculate_sum 会返回什么。
(gdb) b 25
Breakpoint 5 at 0x55555555518e: file simple_program.c, line 25.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Breakpoint 5, main () at simple_program.c:25
25 int result = calculate_sum(x, y);
(gdb) print x
$15 = 10
(gdb) set variable x = 100
(gdb) print x
$16 = 100
(gdb) s // 进入 calculate_sum
calculate_sum (a=100, b=20) at simple_program.c:6
6 int sum = a + b;
(gdb) n
7 return sum;
(gdb) print sum
$17 = 120
(gdb) finish
Run till exit from #0 calculate_sum (a=100, b=20) at simple_program.c:7
0x0000555555555193 in main () at simple_program.c:25
25 int result = calculate_sum(x, y);
Value returned is $18 = 120
(gdb) print result
$19 = 120
可以看到,x 的值成功被修改,并且 calculate_sum 函数也使用了新的 x 值进行计算。
第三章:追踪函数调用栈——理解程序执行路径
程序在执行过程中,函数会一层一层地调用。当一个函数调用另一个函数时,当前的执行上下文(包括参数、局部变量、返回地址等)会被“压入”一个称为“调用栈”(Call Stack)的内存区域。当函数返回时,其上下文会被“弹出”。
理解调用栈对于理解程序的执行路径、定位错误来源至关重要,尤其是当程序崩溃时,调用栈可以告诉你崩溃发生之前,程序都经过了哪些函数。
3.1 backtrace 命令:查看整个调用栈
backtrace (缩写 bt) 命令是GDB中查看调用栈最核心的命令。它会显示从当前执行点到 main 函数(或程序入口点)的所有函数调用。
语法: backtrace 或 bt
更详细的语法: backtrace full 或 bt full (会显示每个栈帧的局部变量和参数)
让我们故意让程序在 process_array 函数内部暂停,然后查看调用栈。
(gdb) b 13 // 在 process_array 内部设置断点
Breakpoint 6 at 0x5555555551e1: file simple_program.c, line 13.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Breakpoint 6, process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13 printf("arr[%d] = %dn", i, arr[i]);
(gdb) bt
#0 process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
#1 0x0000555555555235 in main () at simple_program.c:37
#2 0x00007ffff7a06d0a in __libc_start_call_main (main=0x555555555160 <main>, argc=1, argv=0x7fffffffde18) at ../sysdeps/nptl/libc_start_call_main.h:58
#3 0x00007ffff7a06dbc in __libc_start_main_impl (main=0x555555555160 <main>, argc=1, argv=0x7fffffffde18, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffde08) at ../csu/libc-start.c:360
#4 0x00005555555550a5 in _start ()
解析 backtrace 输出:
- 每一行代表一个“栈帧”(Stack Frame),也就是一个函数调用。
#0是当前正在执行的函数(最顶层)。#1是调用#0的函数。- 依此类推,直到程序入口点(通常是
main函数,再往上是系统启动代码)。 - 每帧会显示函数名、参数值(如果GDB能解析),以及源代码文件名和行号。
从上面的输出可以看出:
- 当前程序执行在
process_array函数的第13行。 process_array是由main函数在simple_program.c的第37行调用的。- 再往上是C标准库的启动函数,最终由
_start启动。
使用 bt full 查看详细信息:
(gdb) bt full
#0 process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
i = 0
#1 0x0000555555555235 in main () at simple_program.c:37
data_size = 5
my_array = 0x5555555592a0
x = 10
y = 20
result = 30
#2 0x00007ffff7a06d0a in __libc_start_call_main (main=0x555555555160 <main>, argc=1, argv=0x7fffffffde18) at ../sysdeps/nptl/libc_start_call_main.h:58
ret = <optimized out>
main_map = <optimized out>
#3 0x00007ffff7a06dbc in __libc_start_main_impl (main=0x555555555160 <main>, argc=1, argv=0x7fffffffde18, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffde08) at ../csu/libc-start.c:360
__invoke_main = <optimized out>
__libc_csu_init = <optimized out>
__libc_csu_fini = <optimized out>
__rtld_fini = <optimized out>
unwind_buf = {__cancel_jmp_buf = {{__jmpbuf = {140737488340992, 4016503730704029288, 140737488340960, 140737488340968, 140737488340976, 140737488340984, -481440628}, __mask_was_saved = 0}}, __setjmp_result = 0}
#4 0x00005555555550a5 in _start ()
bt full 显示了每个栈帧中的局部变量和参数值,这对于理解每个函数调用时的具体状态非常有用。例如,在 main 函数的栈帧中,我们可以看到 x=10, y=20, result=30, my_array=0x..., data_size=5。
3.2 frame 命令:切换和查看栈帧
backtrace 让你看到整个调用栈,而 frame (缩写 f) 命令则允许你在这些栈帧之间切换,以便查看不同函数调用上下文中的变量和状态。
语法:
frame <frame_number>: 切换到指定的栈帧。frame: 显示当前栈帧的信息。info frame: 显示当前栈帧的详细信息。
示例:在 process_array 和 main 之间切换
我们继续停留在 process_array 函数的第13行。
(gdb) b 13
Breakpoint 6 at 0x5555555551e1: file simple_program.c, line 13.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Breakpoint 6, process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13 printf("arr[%d] = %dn", i, arr[i]);
(gdb) frame // 查看当前栈帧
#0 process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13 printf("arr[%d] = %dn", i, arr[i]);
(gdb) print i // 查看当前栈帧的局部变量
$20 = 0
(gdb) frame 1 // 切换到调用者 main 函数的栈帧
#1 0x0000555555555235 in main () at simple_program.c:37
37 process_array(my_array, data_size);
(gdb) print x // 查看 main 函数的局部变量
$21 = 10
(gdb) print my_array // 查看 main 函数的局部变量
$22 = (int *) 0x5555555592a0
(gdb) frame 0 // 切换回 process_array 栈帧
#0 process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13 printf("arr[%d] = %dn", i, arr[i]);
(gdb) print x // 此时在 process_array 栈帧中,x 是 main 的局部变量,不可访问
No symbol "x" in current context.
可以看到,当你切换栈帧后,print 命令会查询当前栈帧上下文中的变量。这正是 frame 命令的强大之处:让你能够完整地检查程序在不同调用层级的状态。
3.3 up 和 down 命令:相对移动栈帧
up 和 down 命令提供了一种更方便的方式来在栈帧之间移动,特别是当你不想记住具体的帧编号时。
语法:
up <n>: 向上移动n个栈帧(向调用者方向)。n默认为1。down <n>: 向下移动n个栈帧(向被调用者方向)。n默认为1。
继续以上面的例子为例:
(gdb) frame // 当前在 process_array (#0)
#0 process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13 printf("arr[%d] = %dn", i, arr[i]);
(gdb) up // 向上移动一个栈帧,到达 main (#1)
#1 0x0000555555555235 in main () at simple_program.c:37
37 process_array(my_array, data_size);
(gdb) print x
$23 = 10
(gdb) down // 向下移动一个栈帧,回到 process_array (#0)
#0 process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13 printf("arr[%d] = %dn", i, arr[i]);
(gdb) print i
$24 = 0
3.4 info args 和 info locals:快速查看参数和局部变量
当你在某个栈帧中时,GDB提供了专门的命令来快速查看该函数的所有参数和局部变量。
info args: 显示当前栈帧函数的所有参数。info locals: 显示当前栈帧函数的所有局部变量。
继续在 process_array 栈帧 (#0):
(gdb) frame 0
#0 process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13 printf("arr[%d] = %dn", i, arr[i]);
(gdb) info args
arr = (int *) 0x5555555592a0
size = 5
(gdb) info locals
i = 0
切换到 main 栈帧 (#1):
(gdb) frame 1
#1 0x0000555555555235 in main () at simple_program.c:37
37 process_array(my_array, data_size);
(gdb) info args // main 函数没有参数
No arguments.
(gdb) info locals
x = 10
y = 20
result = 30
data_size = 5
my_array = (int *) 0x5555555592a0
这些命令大大简化了在复杂函数中查找参数和局部变量的过程。
第四章:GDB进阶技巧与最佳实践
掌握了变量查看和栈追踪后,我们可以再了解一些GDB的进阶功能,它们能让你更高效地定位问题。
4.1 观察点(Watchpoints):当变量改变时暂停
断点在代码行上暂停,而观察点则在变量值改变时暂停。这对于调试那些值被意外修改的变量非常有用。
语法:
watch <expression>: 当expression的值改变时暂停。rwatch <expression>: 当expression被读取时暂停。awatch <expression>: 当expression被读取或写入时暂停。
示例:观察 i 的变化
我们来观察 process_array 函数中 i 变量的变化。
(gdb) b process_array
Breakpoint 4 at 0x5555555551c6: file simple_program.c, line 10.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:10
10 printf("Processing array elements:n");
(gdb) n
Processing array elements:
11 for (int i = 0; i <= size; ++i) {
(gdb) watch i
Hardware watchpoint 7: i // GDB会尝试使用硬件观察点,效率更高
(gdb) c
Continuing.
Hardware watchpoint 7: i
Old value = 0
New value = 1
0x00005555555551d0 in process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
(gdb) c
Continuing.
arr[0] = 0
Hardware watchpoint 7: i
Old value = 1
New value = 2
0x00005555555551d0 in process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
每次 i 的值改变时,GDB都会暂停并报告旧值和新值,这非常方便!
管理观察点:
info watchpoints: 查看所有观察点。delete <watchpoint_number>: 删除观察点。
4.2 条件断点:只在特定条件下暂停
有时你只想在某个变量达到特定值,或者某个表达式为真时才暂停程序。条件断点允许你设置一个条件,只有当条件满足时,断点才会触发。
语法: break <location> if <condition>
示例:只在 i == 5 时暂停
我们让 process_array 函数只在 i 达到5时暂停。
(gdb) b 11 if i == 5
Breakpoint 8 at 0x5555555551d0: file simple_program.c, line 11.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Processing array elements:
arr[0] = 0
arr[1] = 10
arr[2] = 20
arr[3] = 30
arr[4] = 40
Breakpoint 8, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
(gdb) print i
$25 = 5
程序直接跳过了 i=0 到 i=4 的循环,直接在 i=5 时暂停。这对于调试循环中的特定迭代或处理大数据集时非常有用。
4.3 断点命令列表:自动化调试动作
你可以为断点设置一系列的GDB命令,当断点触发时,这些命令会自动执行。这可以用来打印变量、继续执行,或者执行其他操作,而无需手动干预。
语法:
commands <breakpoint_number>
<command1>
<command2>
...
end
示例:当 i 变化时自动打印并继续
(gdb) b 11 // 在循环开始处设置断点
Breakpoint 9 at 0x5555555551d0: file simple_program.c, line 11.
(gdb) commands 9
Type commands for breakpoint 9, one per line.
End with a line containing just "end".
>print i
>c
>end
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program
Breakpoint 1, main () at simple_program.c:23
23 int x = 10;
(gdb) c
Continuing.
Processing array elements:
Breakpoint 9, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
$26 = 0
Continuing.
arr[0] = 0
Breakpoint 9, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11 for (int i = 0; i <= size; ++i) {
$27 = 1
Continuing.
arr[1] = 10
... (会一直打印 i 的值并继续,直到循环结束或遇到其他断点)
4.4 TUI模式:文本用户界面
GDB提供了一个文本用户界面(TUI),可以将源代码、汇编代码和GDB命令窗口同时显示在终端中,提供更直观的调试体验。
进入TUI模式: layout src 或 Ctrl+x a
退出TUI模式: layout off 或 Ctrl+x a
(gdb) layout src
这将把屏幕分成两部分,上面显示源代码,下面是GDB的命令行。这对于同时跟踪代码执行和GDB输出非常有用。
4.5 查看源代码:list 命令
虽然TUI模式很方便,但你也可以随时使用 list (缩写 l) 命令来查看源代码。
list: 显示当前行附近的源代码。list <line_number>: 显示指定行号附近的源代码。list <function_name>: 显示指定函数的源代码。list -: 显示前一个list范围之前的代码。
(gdb) list
8 }
9
10 void process_array(int* arr, int size) {
11 printf("Processing array elements:n");
12 for (int i = 0; i <= size; ++i) { // Potential off-by-one error
13 if (i < size) {
14 printf("arr[%d] = %dn", i, arr[i]);
15 } else {
16 printf("Attempting to access arr[%d] (out of bounds for size %d)n", i, size);
第五章:GDB常用命令速查表
为了方便大家快速查阅,我为大家整理了本次讲座中涉及的主要GDB命令:
表 5.1:GDB程序执行控制命令
| 命令 | 缩写 | 描述 |
|---|---|---|
run |
r |
启动或重新启动程序。 |
continue |
c |
从当前暂停点继续执行。 |
next |
n |
单步执行,不进入函数。 |
step |
s |
单步执行,进入函数。 |
finish |
运行到当前函数结束。 | |
until |
u |
运行到指定行或跳出循环。 |
quit |
q |
退出GDB。 |
表 5.2:GDB断点与观察点命令
| 命令 | 缩写 | 描述 |
|---|---|---|
break <location> |
b |
在指定位置设置断点(文件:行号, 函数名)。 |
break <location> if <condition> |
设置条件断点。 | |
info breakpoints |
info b |
查看所有断点。 |
delete <breakpoint_number> |
d |
删除断点。 |
enable <breakpoint_number> |
启用断点。 | |
disable <breakpoint_number> |
禁用断点。 | |
commands <breakpoint_number> |
为断点设置命令列表。 | |
watch <expression> |
当表达式值改变时暂停。 | |
rwatch <expression> |
当表达式被读取时暂停。 | |
awatch <expression> |
当表达式被读取或写入时暂停。 | |
info watchpoints |
查看所有观察点。 |
表 5.3:GDB变量查看命令
| 命令 | 缩写 | 描述 |
|---|---|---|
print <expression> |
p |
打印表达式的值。 |
print/<format> <expression> |
格式化打印表达式的值(如 /x, /d, /c, /s)。 |
|
print <array>@<length> |
打印数组的指定长度元素。 | |
display <expression> |
每次程序暂停时自动打印表达式的值。 | |
info display |
查看所有自动显示的表达式。 | |
undisplay <display_number> |
停止自动显示表达式。 | |
x/<nfu> <address> |
检查指定内存地址的内容。 | |
set variable <var> = <value> |
修改变量的值。 |
表 5.4:GDB调用栈追踪命令
| 命令 | 缩写 | 描述 |
|---|---|---|
backtrace |
bt |
显示整个函数调用栈。 |
backtrace full |
bt full |
显示带有局部变量和参数的完整调用栈。 |
frame <number> |
f |
切换到指定的栈帧。 |
frame |
显示当前栈帧的信息。 | |
up <n> |
向上移动 n 个栈帧(向调用者方向)。 |
|
down <n> |
向下移动 n 个栈帧(向被调用者方向)。 |
|
info args |
显示当前函数的所有参数。 | |
info locals |
显示当前函数的所有局部变量。 |
表 5.5:GDB源代码查看命令
| 命令 | 缩写 | 描述 |
|---|---|---|
list |
l |
显示当前行附近的源代码。 |
list <line_number> |
显示指定行号附近的源代码。 | |
list <function_name> |
显示指定函数的源代码。 | |
layout src |
切换到TUI模式,显示源代码和GDB命令行。 |
结束语
恭喜各位,通过今天的讲座,你们已经掌握了GDB最核心、最实用的调试技能。从现在开始,当你的程序再次“罢工”时,你不再是束手无策的旁观者,而是能够拿起GDB这把利器,深入其内部,探查变量的奥秘,追踪函数的足迹,最终找出问题的根源并加以解决。
调试是一门艺术,也是一门科学。它需要耐心、细致和逻辑思维。GDB作为你的忠实伙伴,将极大提升你的问题解决能力。记住,实践是最好的老师,多动手,多尝试,你将越来越熟练。希望今天的分享能为你们的编程之路点亮一盏明灯,祝愿大家在编程世界中探索愉快,成就非凡!