GDB(GNU Debugger)是由Richard Stallman在1986年编写的开源调试器,作为GNU系统的一部分。它是一款功能强大的便携式调试工具,可以在许多类UNIX系统上运行,并支持包括C、C++在内的多种编程语言。
GDB的核心工作原理基于Linux内核提供的ptrace系统调用。当启动GDB调试程序时,会创建一个父子进程关系:GDB作为父进程,被调试程序作为子进程。子进程通过调用ptrace(PTRACE_TRACEME)将自己设置为被跟踪模式,而父进程(GDB)则可以通过ptrace系统调用来监控和控制子进程的执行流程,包括检查和修改其内存映像及寄存器。
GDB调试的实现完全建立在信号机制的基础上。当使用ptrace系统调用建立调试关系后,交付给目标程序的任何信号都会首先被GDB截获。例如,当设置断点时,GDB会在断点位置写入断点指令INT3(0xCC),当目标程序执行到该指令时会触发SIGTRAP信号,GDB捕获该信号并暂停程序执行。
一、GDB调试配置
在开始调试前,必须确保程序编译时包含调试信息。这是GDB调试的前提条件,没有调试信息的程序无法进行有效的调试。
1. 编译选项设置
使用GCC/G++编译时,必须添加-g选项来生成调试信息:
gcc -g -o program program.c
-g选项会告诉编译器生成调试符号信息,包括:
- 变量的名字和类型
- 源代码中对应的行号
- 函数调用栈的信息
调试信息通常嵌入到ELF可执行文件的特殊节区中,如
.debug_info、.debug_line等。GCC默认生成DWARF格式的调试信息。
2. 优化选项与调试
在调试时,建议使用-O0选项(无优化)来确保调试体验最佳:
gcc -O0 -g -o program program.c
优化选项会影响调试体验,因为优化可能会改变程序的执行流程。如果需要保留部分优化但仍能调试,可以使用较低的优化级别如-O1或-O2。
3. Makefile配置
在项目中,通常使用Makefile进行构建。要在Makefile中启用调试支持,需要在编译选项中添加-g:
# 基本Makefile配置
CFLAGS = -Wall -O0 -g # C语言编译选项
CXXFLAGS = -Wall -O0 -g # C++编译选项
LDFLAGS = -g # 链接选项
TARGET = my_program
SOURCES = main.c utils.c
OBJECTS = $(SOURCES:.c=.o)
all: $(TARGET)
$(TARGET): $(OBJECTS)
$(CC) $(LDFLAGS) -o $@ $^
clean:
rm -f $(OBJECTS) $(TARGET)
二、GDB操作使用
2.1 启动GDB与加载程序
2.1.1 启动GDB的多种方式
GDB提供了多种启动和加载程序的方式:
- 直接启动并加载程序:
gdb <program>
- 启动时指定参数:
gdb <program> --args <arguments>
- 先启动GDB再加载程序:在
gdb环境中使用file <program>命令
- 调试core文件:
gdb <program> core,用于分析程序崩溃时生成的core dump文件
- attach到正在运行的进程:
gdb attach <pid>,可以调试一个已经在运行的进程,这是一个非常有用的调试手段。
2.1.2 命令行参数传递
- 设置参数:
(gdb) set args arg1 arg2 arg3
- 查看参数:
(gdb) show args
- 在启动时传递参数:
(gdb) run arg1 arg2 arg3
2.2 GDB命令基础与帮助系统
2.2.1 命令格式与语法
GDB命令基本语法为:command [arguments] [options]
GDB支持命令缩写(如break可缩写为b)、Tab补全、历史记录和快捷键。
2.2.2 帮助系统使用
学会使用帮助系统是掌握GDB的关键:
(gdb) help:查看所有命令分类
(gdb) help <class>:查看指定分类的命令,如help breakpoints
(gdb) help <command>:查看指定命令的详细说明
(gdb) apropos <keyword>:搜索包含指定关键词的命令
(gdb) show version:显示当前GDB版本
2.3 基本调试流程示例
为了更好地理解GDB的基本操作,这里通过一个简单的C程序来演示完整的调试流程。
2.3.1 示例程序准备
创建一个简单的C程序sum.c:
#include <stdio.h>
int sum(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = 5;
int y = 3;
int z = sum(x, y);
printf("The sum of %d and %d is %d\n", x, y, z);
return 0;
}
使用GCC编译并包含调试信息:gcc -g -o sum sum.c
2.3.2 启动GDB并加载程序
- 启动GDB并加载程序:
gdb sum
- 查看程序信息:
(gdb) info files
- 查看源代码:
(gdb) list main
2.4 设置断点与程序执行控制
2.4.1 断点设置方法
断点是调试中最基本也是最重要的功能之一:
- 函数入口:
(gdb) break main 或 (gdb) b main
- 指定行号:
(gdb) break sum.c:10
- 内存地址:
(gdb) break *0x4005a3
- 临时断点(触发一次后自动删除):
(gdb) tbreak <location>
- 硬件断点:
(gdb) hbreak <location>,适用于ROM/EPROM代码调试
2.4.2 断点管理命令
- 查看所有断点:
(gdb) info breakpoints 或 (gdb) info b
- 删除断点:
(gdb) delete 1 或 (gdb) d 1
- 删除所有断点:
(gdb) delete breakpoints 或 (gdb) d
- 禁用/启用断点:
(gdb) disable <number> / (gdb) enable <number>
2.4.3 程序执行控制
- 启动程序:
(gdb) run 或 (gdb) r
- 继续执行:
(gdb) continue 或 (gdb) c,直到遇到下一个断点
- 单步执行(进入函数):
(gdb) step 或 (gdb) s
- 单步执行(不进入函数):
(gdb) next 或 (gdb) n
- 执行到指定位置:
(gdb) until <location>
- 跳转到指定位置:
(gdb) jump <location>,可用于跳过错误代码
2.5 查看变量与内存
2.5.1 查看变量值
2.5.2 内存查看命令
- 查看内存命令:
(gdb) x/<format> <address>,x是examine的缩写
- 内存查看格式说明:
x/<n/f/u> <addr>
n:要显示的单元数量
f:显示格式(x-十六进制, d-十进制, s-字符串等)
u:每个单元的大小(b-字节, h-半字, w-字)
- 示例:
(gdb) x/3uh 0x54320 表示从地址0x54320开始,读取3个单元,每个单元2字节,按无符号十六进制显示。
- 查看字符串:
(gdb) x/s <address>
- 查看汇编指令:
(gdb) x/i <address>
2.6 函数调用栈分析
函数调用栈分析是调试复杂程序的重要工具,特别是在程序崩溃时。
2.7 简单调试案例演示
下面通过一个具体的例子来演示完整的调试流程。假设我们有一个排序程序gdb_sort_demo.c:
#include <stdio.h>
void select_sort(int arr[], int len) {
int i, j, min_idx;
for (i = 0; i < len - 1; i++) {
min_idx = i;
for (j = i + 1; j < len; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}
int temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int len = sizeof(arr) / sizeof(arr[0]);
select_sort(arr, len);
printf("Sorted array: ");
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
编译并启动调试:
gcc -g -o gdb_sort_demo gdb_sort_demo.c
gdb gdb_sort_demo
调试步骤:
- 在
select_sort函数入口设置断点:(gdb) b select_sort
- 运行程序:
(gdb) run
- 程序停在函数入口,查看参数:
(gdb) p arr, (gdb) p len
- 单步进入循环:
(gdb) s
- 在
min_idx赋值后查看其值:(gdb) p min_idx
- 继续执行直到排序完成:
(gdb) c
- 查看排序后的数组:
(gdb) p arr
通过这个例子,我们演示了如何设置断点、单步调试、查看变量等基本操作。在实际的Linux环境下,你可以根据程序的复杂度和问题类型,灵活运用这些功能进行高效的调试。