跳到主要内容
SUN / 笔记
← 全部文章

如何启动 & 基本运行

GDB简介

GNU调试器(GNU Debugger,简称GDB)是GNU项目开发的一款功能强大的命令行调试工具,主要用于C、C++等编程语言的调试。作为Linux/Unix系统下最常用的调试器之一,GDB可以帮助开发者:

  • 控制程序的执行
  • 设置断点观察程序状态
  • 分析程序崩溃时的核心转储
  • 动态修改变量值
  • 反向调试(从错误点回溯)

GDB支持多种编程语言(C、C++、Go、Rust等)和多种处理器架构(x86、ARM等),是系统程序员和应用程序开发者的必备工具。

安装与基本配置

1、安装GDB

在大多数Linux发行版上,GDB可以通过包管理器安装:

shell
# Debian/Ubuntu
sudo apt-get install gdb
 
# CentOS/Redhat
sudo yum install gdb

2、编译可调试的程序

要使用GDB调试程序,需要在编译时加入调试信息(使用-g选项):

shell
gcc -g program.c -o program
g++ -g program.cpp -o program

调试信息会包含源代码行号、变量类型等关键信息,使调试更加方便。

三、 基本命令与使用

1、启动GDB

shell
gdb [可执行文件]  # 直接启动
gdb -p [进程ID]   # 附加到运行中的进程
gdb [可执行文件] core  # 分析核心转储文件

2、常用命令

命令 缩写 功能描述
run r 开始执行程序
break b 设置断点
continue c 继续执行直到下一个断点
next n 执行下一行(不进入函数)
step s 执行下一行(进入函数)
print p 打印变量或表达式值
backtrace bt 显示调用栈
frame f 选择栈帧
list l 显示源代码
info i 显示各种信息(如断点、变量)
quit q 退出GDB

3、断点设置与管理

shell
break main.c:10       # 在main.c的第10行设置断点
break function_name   # 在函数入口设置断点
break *0x8048000      # 在内存地址设置断点
 
info breakpoints      # 查看所有断点
delete 2              # 删除编号为2的断点
disable 1             # 禁用编号为1的断点
enable 1              # 启用编号为1的断点
 
# 条件断点
break 15 if x == 5    # 当x等于5时在第15行中断

4、变量查看与修改

shell
print x               # 打印变量x的值
print/x x             # 以十六进制打印x
print array[10]@5     # 打印数组从第10个元素开始的5个元素
print *(int*)0x1234   # 打印内存地址0x1234处的整数值
 
set var x = 10        # 修改变量x的值为10
set {int}0x1234 = 10  # 修改内存地址0x1234处的值为10

四、进阶调试技巧

1、多线程调试

shell
info threads          # 查看所有线程
thread 3              # 切换到线程3
break file.c:123 thread 2  # 仅在线程2上设置断点
 
# 非停止模式(允许其他线程在单步调试时继续运行)
set target-async on
set non-stop on

2、观察点(Watchpoints)

shell
watch x               # 当x被修改时中断
rwatch x              # 当x被读取时中断
awatch x              # 当x被读取或修改时中断
info watchpoints      # 查看所有观察点

3、调用栈分析

shell
backtrace             # 显示调用栈
backtrace full        # 显示调用栈及局部变量
frame 2               # 切换到栈帧2
info frame            # 显示当前栈帧信息
info args             # 显示当前函数的参数
info locals           # 显示当前函数的局部变量

4、处理信号

shell
info signals         # 查看信号处理方式
handle SIGSEGV stop  # 当收到SIGSEGV时中断
handle SIGINT print  # 当收到SIGINT时打印信息

5、反向调试

GDB支持有限的逆向执行功能(需要特殊版本的GDB):

shell
record                # 开始记录执行过程
reverse-step          # 逆向执行一步
reverse-continue      # 逆向继续执行

五、 GDB脚本与自动化

1、命令文件

可以创建包含GDB命令的文本文件,在启动时自动执行:

shell
gdb -x commands.gdb --args ./program arg1 arg2

commands.gdb示例内容:

shell
break main
run
backtrace
quit

2、Python扩展

GDB支持Python脚本扩展,可以编写复杂的调试脚本:

text
# 在GDB中执行python脚本
python
import gdb
 
class MyBreakpoint(gdb.Breakpoint):
    def stop(self):
        val = gdb.parse_and_eval("x")
        print(f"x is {val}")
        return False  # 不中断执行
 
MyBreakpoint("main.c:10")
end

示例1:段错误调试

示例程序:segfault.c

cpp
#include <stdio.h>
 
int main() {
    int *ptr = NULL;
    *ptr = 10; // 这里将导致段错误
    printf("Value: %d\n", *ptr);
    return 0;
}

调试过程:

shell
gcc -g segfault.c -o segfault
gdb ./segfault
 
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x0000555555550768 in main () at segfault.c:5
5           *ptr = 10; // 这里将导致段错误
(gdb) p ptr
$1 = (int *) 0x0

示例2:死锁调试

示例程序:deadlock.c

一个典型的死锁例子:两个线程各自持有对方需要的锁,导致相互等待,死锁发生。

cpp
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
 
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
 
void *thread1(void *arg) {
    pthread_mutex_lock(&mutex1);
    sleep(1);
    pthread_mutex_lock(&mutex2); // 将在此处死锁
    printf("Thread 1\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}
 
void *thread2(void *arg) {
    pthread_mutex_lock(&mutex2);
    sleep(1);
    pthread_mutex_lock(&mutex1); // 将在此处死锁
    printf("Thread 2\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}
 
int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread1, NULL);
    pthread_create(&t2, NULL, thread2, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    return 0;
}

调试过程:

shell
gcc -g deadlock.c -o deadlock -lpthread
./deadlock # 程序由于死锁会卡在这里

提示:新建一个终端,使用attach方式调试进程(正在运行中的程序)。

shell
#在另一个终端中
ps aux | grep deadlock # 获取进程ID
 
hjh@raspberrypi:~/test/gdb $ ps aux | grep deadlock
hjh  3136  0.0  0.0  18816   528 pts/0    Sl+  00:03   0:00 ./deadlock
hjh  3147  0.0  0.0   6480  1552 pts/1    S+   00:06   0:00 grep --color=auto deadlock
 
#命令为gdb attach  [进程ID],注意使用上面获取的进程ID,这里我的进程ID是3136
hjh@raspberrypi:~/test/gdb $ gdb attach 3136
 
(gdb) info threads
#详细结果见下图
(gdb) thread apply all backtrace
#详细结果见下图

img

我们从线程调用栈中可以清楚地看到两个线程都在等待对方持有的锁:

Thread 1 (LWP 3136):主线程,主要负责创建线程thread1和thread2。

Thread 2 (LWP 3137):正在执行 thread1

text
#4  0x000055564ec20940 in thread1 (arg=0x0) at deadlock.c:11 
  • 正在尝试锁定 mutex1,但被阻塞。
  • 在 deadlock.c 第 11 行调用 pthread_mutex_lock(&mutex1)。

Thread 3 (LWP 3138):正在执行 thread2

text
#4  0x000055564ec2099c in thread2 (arg=0x0) at deadlock.c:21 
  • 正在尝试锁定 mutex2,但被阻塞。
  • 在 deadlock.c 第 21 行调用 pthread_mutex_lock(&mutex2)。

__futex_abstimed_wait_common64 和 futex_wait

这是 Linux 内核用于线程同步的基本机制,当线程调用 pthread_mutex_lock 时,如果锁被占用,就会进入 futex_wait 状态,即等待锁释放。

示例3:结合Valgrind检测内存泄漏

虽然GDB不直接检测内存泄漏,但可以结合Valgrind:

valgrind --leak-check=full --show-leak-kinds=all ./program

然后使用GDB分析Valgrind输出的错误信息。

这里内容较多,后面在内存泄漏里整理下。

示例4:递归函数调试

示例程序:factorial.c(阶乘计算)

text
#include <stdio.h>
 
int factorial(int n) {
    if (n <= 1)
        return 1;
    return n * factorial(n-1);
}
 
int main() {
    int num = 5;
    int result = factorial(num);
    printf("Factorial of %d is %d\n", num, result);
    return 0;
}

调试过程:

text
gcc -g factorial.c -o factorial
gdb ./factorial
(gdb) b factorial
Breakpoint 1 at 0x760: file factorial.c, line 4.
(gdb) run
 
Breakpoint 1, factorial (n=5) at factorial.c:4
4           if (n <= 1)
(gdb) next
6           return n * factorial(n-1);
(gdb) print n
$1 = 5
(gdb) continue
Continuing.
 
Breakpoint 1, factorial (n=4) at factorial.c:4
4           if (n <= 1)
(gdb) bt
#0  factorial (n=4) at factorial.c:4
#1  0x0000555555550780 in factorial (n=5) at factorial.c:6
#2  0x00005555555507ac in main () at factorial.c:11

示例5:数组越界访问

示例程序:array_bounds.c

text
#include <stdio.h>
 
int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    for (int i = 0; i <= 5; i++) {  // 错误:i<=5导致越界
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    return 0;
}

调试过程:

text
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./array_bounds...
(gdb) list
1       #include <stdio.h>
2
3       int arr[5] = {1, 2, 3, 4, 5};
4       int arr2[5] = {11, 12, 13, 14, 15};
5
6       int main() {
7           for (int i = 0; i < 10; i++) {  // 错误:当i>=5时数组访问越界
8               printf("arr[%d] = %d\n", i, arr[i]);
9           }
10          return 0;
(gdb) b 6
Breakpoint 1 at 0x75c: file array_bounds.c, line 7.
(gdb) r
Starting program: /home/hjh/test/gdb/array_bounds
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".
 
Breakpoint 1, main () at array_bounds.c:7
7           for (int i = 0; i < 10; i++) {  // 错误:当i>=5时数组访问越界
(gdb) watch arr[i]
Hardware watchpoint 2: arr[i]
(gdb) c
Continuing.
arr[0] = 1
 
Hardware watchpoint 2: arr[i]
 
Old value = 1
New value = 2
0x0000555555550794 in main () at array_bounds.c:7
7           for (int i = 0; i < 10; i++) {  // 错误:当i>=5时数组访问越界
(gdb) c
Continuing.
arr[1] = 2
 
Hardware watchpoint 2: arr[i]
 
Old value = 2
New value = 3
 
(gdb) <回车可以重复上一次操作>  
......
 
arr[4] = 5
 
Hardware watchpoint 2: arr[i]
 
Old value = 5
New value = 0
0x0000555555550794 in main () at array_bounds.c:7
7           for (int i = 0; i < 10; i++) {  // 错误:当i>=5时数组访问越界
(gdb)
Continuing.
arr[5] = 0
 
Hardware watchpoint 2: arr[i]
 
Old value = 0
New value = 11
0x0000555555550794 in main () at array_bounds.c:7
7           for (int i = 0; i < 10; i++) {  // 错误:当i>=5时数组访问越界
(gdb)
Continuing.
arr[6] = 11
......

示例6:条件断点使用

示例程序:condition.c

text
#include <stdio.h>
 
int main() {
    for (int i = 0; i < 100; i++) {
        printf("i = %d\n", i);
    }
    return 0;
}

调试过程:

text
gcc -g -O0 condition.c -o condition
gdb condition
 
(gdb) list
1       #include <stdio.h>
2
3       int main() {
4           for (int i = 0; i < 100; i++) {
5               printf("i = %d\n", i);
6           }
7           return 0;
8       }
(gdb) b  main
Breakpoint 1 at 0x75c: file condition.c, line 4.
(gdb) r
Starting program: /home/hjh/test/gdb/condition
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".
 
Breakpoint 1, main () at condition.c:4
4           for (int i = 0; i < 100; i++) {
(gdb) break 5 if i == 50
Breakpoint 2 at 0x555555550764: file condition.c, line 5.
(gdb) c
Continuing.
 
...
 
i = 48
i = 49
 
Breakpoint 2, main () at condition.c:5
5               printf("i = %d\n", i);
(gdb) p i
$1 = 50
(gdb)

如何启动 & 基本运行

启动可执行文件:

text
gdb ./bomb

直接运行程序(交互式):

text
(gdb) run

用文件作为 stdin:

text
(gdb) run < inputs.txt

把程序以参数运行:

text
(gdb) run arg1 arg2

结束 / 退出:

text
(gdb) quit

常用断点(breakpoints)

在函数入口处断点:

text
(gdb) break phase_1

在具体地址断点(通常从 objdump 中看到的地址):

text
(gdb) break *0x400abc

临时断点(命中一次自动删除):

text
(gdb) tbreak phase_2

条件断点(只在满足条件时触发):

text
(gdb) break func if i==5

在崩溃/“炸弹”函数处断点(便于看为什么会爆炸):

text
(gdb) break explode_bomb

单步执行与控制流程

步过(step over,一般跳过函数内部):

text
(gdb) next    # 或 n

步入(step into,进入函数内部):

text
(gdb) step    # 或 s

逐条汇编指令单步(对汇编级别分析非常有用):

text
(gdb) stepi   # 或 si
(gdb) nexti   # 或 ni

继续运行直到下一个断点:

text
(gdb) continue  # 或 c

跳到某行或指令:

text
(gdb) jump lineno
(gdb) jump *0x400abc

查看调用栈、函数局部、参数

打印当前调用栈:

text
(gdb) backtrace  # 或 bt

查看特定栈帧并切换:

text
(gdb) frame 0    # 顶层
(gdb) frame 1    # 上一层调用

显示该帧的局部变量:

text
(gdb) info locals

打印函数参数/表达式:

text
(gdb) print var
(gdb) p/x var      # 十六进制
(gdb) p/d var      # 十进制
(gdb) p/s var      # 当 var 是 char* 时打印字符串

查看寄存器与内存(Bomb Lab 特别重要)

查看寄存器:

text
(gdb) info registers
(gdb) info reg rdi rsi rax

在 x86_64 的 C 调用约定中,第一个参数在 rdi,第二个在 rsi,返回值在 rax — 在查看 strcmp/sscanf 等很有用:

text
(gdb) x/s $rdi   # 把 rdi 当字符串地址打印
(gdb) x/s $rsi

查看内存(多种格式):

text
(gdb) x/32bx $rsp    # 以字节显示 32 个(十六进制)
(gdb) x/10gx 0x602000 # 以 8 字节显示(地址)
(gdb) x/4wa 0x...     # 以字打印 4 个地址
(gdb) x/s 0x400800    # 打印以该地址开始的字符串
(gdb) x/20cx $rbp-0x30 # 字符格式

格式说明:x/<count><format><size> <addr>,常用 size: b(1)、h(2)、w(4)、g(8),format: x(十六进制)、d、u、s(字符串)、i(指令)。

处理没有调试符号(strip 或 无 .debug)时的技巧

如果二进制被 strip 了(没有函数名或变量名),可以:

  • 用 objdump -d -M intel bomb 找函数标签 <funcname> 或直接搜索 call 到 explode_bomb,从调用关系推测哪个是 phase。
  • 在可疑地址设置断点:break *0x400abc,然后 run,用 bt 看调用栈上地址对应的函数。
  • 在断点处用 disassemble/x/… 检查周围内存/表数据以推断逻辑。

汇编与反汇编配合(GDB 与 objdump)

在 gdb 中查看汇编(Intel 风格):

text
(gdb) set disassembly-flavor intel
(gdb) disassemble /r  # 显示地址和机器码

在函数处反汇编:

text
(gdb) disassemble phase_3

或

text
(gdb) disassemble 0x400800, 0x4008f0

把 objdump 的输出与 gdb 的动态寄存器信息结合,是理解每条指令实际传递数据的好方法。

watchpoint(监视点):当内存值改变就停

监视某个内存或变量:

text
(gdb) watch var
(gdb) watch *(int*)0x602010

条件监视:

text
(gdb) watch var if i>10

常用于检测链表被修改或某全局变量何时改变。

TUI(文本用户界面)与可视化

打开 TUI 源码/汇编视图(在终端中更易读):

text
(gdb) layout src     # 展示源码(如果有)
(gdb) layout asm     # 展示汇编
(gdb) layout split   # 同时显示

在某些终端可能需要先 gdb -tui ./bomb 启动。

自动化与命令文件

把常用命令写到文件 gdb.cmds:

text
break phase_1
break explode_bomb
run < inputs.txt
commands
  x/s $rdi
  continue
end

然后启动 gdb 时直接加载:

text
gdb -x gdb.cmds ./bomb

在 gdb 内也可用 source gdb.cmds。

日志与输出记录

把 gdb 会话记录到文件:

text
(gdb) set logging on
(gdb) set logging file gdb.log

进阶:reverse debugging(回放)与记录

GDB 在某些构建/平台支持“record”功能(让你反向 step):

text
(gdb) record
(gdb) reverse-step
(gdb) reverse-continue

注意:不是所有平台/版本都支持,且性能开销大。替代可用 rr 工具(record & replay),但这属于进阶内容。

与动态库、远程调试有关的命令

在远程 / 目标机上用 gdbserver:

text
# 在目标机上(或容器里):
gdbserver :1234 ./bomb

# 在本地:
gdb ./bomb
(gdb) target remote target-ip:1234

用于在受限环境或不同架构上调试。

常见错误与排查

程序没有停在断点?

  • 确认你断点的函数名存在:info functions 或 nm -C bomb | grep phase。
  • 如果使用了优化编译(-O2),编译器可能内联或重排序,函数边界不明显 —— 这时用地址断点更稳妥。 看不到变量名或源码?
  • 说明二进制没有调试信息(strip 或未用 -g 编译)。使用反汇编 + 寄存器/内存检查代替源码变量。 条件断点总不触发?
  • 检查条件表达式是否用对变量名或寄存器,或先打印条件表达式来验证。

Bomb Lab 常用工作流(把上面命令串起来)

  1. 先用静态工具快速定位:

    text
    objdump -d -M intel bomb > bomb.disasm
    grep -n "phase_" bomb.disasm
    strings bomb | less
    
  2. 在 gdb 中设置关键断点:

    text
    gdb ./bomb
    (gdb) set disassembly-flavor intel
    (gdb) break phase_1
    (gdb) break explode_bomb
    (gdb) run < inputs.txt
    
  3. 当断在 phase_1 时:

    • disassemble 看体内汇编
    • 找到 call strcmp 或 call sscanf 的指令
    • 在那条指令前 si,然后 x/s $rdi、x/s $rsi 读取比较的字符串
    • 把读出的正确字符串写到 inputs.txt 的第一行,继续 continue
  4. 对于复杂递归/查表类,单步或在循环关键处打印寄存器/栈来重构算法。

小技巧(节省时间)

  • 在一个断点的 commands 块里自动打印关键寄存器并 continue,可以采集多处信息而不手动交互。

  • 用 display 自动打印表达式每次停下时:

    text
    (gdb) display $rdi
    (gdb) display/x $rax
    
  • 在爆炸处断点用 bt 看调用链,找到是哪一层传入了错误参数。

相关文章

C++面向对象 →day02——面向对象高级 →day06——Java编程案例(专题) →