文章

gdb高频总结一

什么是GDB?它用于做什么?

GDB(GNU Debugger)是一个功能强大的开源调试工具,用于调试各种编程语言的程序,尤其是C、C++和其他支持的语言(如Fortran)。它是GNU项目的一部分,广泛用于开发和调试软件。

GDB的用途

GDB主要用于以下场景:

  1. 调试程序
    • 在程序运行时监视其行为。
    • 通过逐步执行代码找出问题。
    • 检查和修改程序中的变量值。
  2. 分析崩溃
    • 加载程序生成的核心转储(core dump)文件,分析程序崩溃时的状态。
    • 查看崩溃时的调用栈、寄存器值等信息。
  3. 排查问题
    • 查找运行时的逻辑错误。
    • 验证程序执行是否符合预期。
  4. 调试多线程和并发程序
    • 支持调试多线程程序,可以逐线程调试。
    • 检查线程状态和交互。
  5. 动态修改程序行为
    • 修改程序中的变量值。
    • 在运行时修复代码逻辑。
  6. 反汇编和逆向工程
    • 支持查看和调试程序的汇编代码。
    • 分析程序底层执行情况。

GDB的主要功能

  1. 断点(Breakpoints)
    • 设置、启用、禁用断点,使程序在特定位置暂停。
    • 条件断点可以在满足特定条件时触发。
  2. 逐步执行(Stepping)
    • 单步执行代码。
    • 进入函数内部(step)或跳过函数调用(next)。
  3. 查看调用栈(Backtrace)
    • 查看当前函数调用的堆栈信息。
    • 检查函数的调用顺序及参数。
  4. 变量和表达式检查
    • 查看和修改变量值。
    • 动态评估表达式。
  5. 查看寄存器和内存
    • 检查和修改寄存器值。
    • 查看特定内存地址的值。
  6. 条件调试
    • 在断点触发时执行特定命令。
    • 在满足特定条件时暂停程序。
  7. 远程调试
    • 通过网络调试运行在其他机器上的程序。

使用场景

  1. 开发新程序:帮助开发者在开发阶段快速发现和修复问题。
  2. 调试复杂问题:如内存泄漏、段错误(Segmentation Fault)、竞争条件等。
  3. 学习和理解代码:尤其是调试第三方库或遗留代码时,GDB可以帮助理解代码的执行逻辑。

使用GDB的基本步骤

  1. 编译程序时启用调试信息:

    1
    
    gcc -g -o my_program my_program.c
    
  2. 启动GDB:

    1
    
    gdb ./my_program
    
  3. 在GDB中设置断点并运行程序:

    1
    2
    
    break main
    run
    
  4. 使用调试命令:

    • 单步执行:stepnext
    • 查看变量:print variable_name
    • 查看调用栈:backtrace
  5. 退出GDB:

    1
    
    quit
    

GDB是一个开发人员的重要工具,通过它可以深入理解程序的运行状态和行为,快速定位问题并优化代码。

GDB的常用命令有哪些?

在GDB中,有许多常用的命令可以帮助你调试程序。以下是一些重要的GDB命令及其用途:


启动和运行相关

  1. 启动程序

    • runr

      启动程序执行,可以传递命令行参数,例如:

      1
      
      run arg1 arg2
      
  2. 重新启动程序

    • start 重新启动程序,并在main()函数的第一行暂停。
  3. 设置程序的运行环境

    • set args

      设置程序运行时的参数,例如:

      1
      
      set args arg1 arg2
      
  4. 查看程序参数

    • show args 显示当前设置的运行参数。

断点相关

  1. 设置断点

    • breakb

      在指定位置设置断点,例如:

      1
      2
      3
      
      break main
      break filename:line_number
      break function_name
      
  2. 条件断点

    • break

      设置满足特定条件时触发的断点,例如:

      1
      
      break function_name if x == 5
      
  3. 查看断点

    • info breakpointsi b 显示当前所有断点及其状态。
  4. 删除断点

    • deleted

      删除指定编号的断点,例如:

      1
      
      delete 1
      
  5. 启用/禁用断点

    • enable

      启用某个断点:

      1
      
      enable 1
      
    • disable

      禁用某个断点:

      1
      
      disable 1
      

程序执行控制

  1. 继续执行

    • continuec 从当前暂停位置继续执行程序。
  2. 单步执行

    • steps 单步执行代码,进入函数内部。
    • nextn 单步执行代码,但跳过函数调用。
  3. 运行到函数返回

    • finish 执行当前函数的剩余部分,返回到调用者。
  4. 运行到指定位置

    • until

      执行代码直到当前循环结束或指定的行,例如:

      1
      
      until line_number
      
  5. 退出程序

    • kill 停止正在运行的程序。

堆栈相关

  1. 查看调用栈

    • backtracebt 显示当前调用栈信息。
  2. 切换栈帧

    • framef

      切换到指定栈帧,例如:

      1
      
      frame 1
      
  3. 查看局部变量

    • info locals 显示当前栈帧的局部变量。
  4. 查看参数

    • info args 显示当前栈帧的函数参数。

变量和表达式相关

  1. 打印变量值

    • printp

      打印变量值或表达式结果,例如:

      1
      2
      
      print variable_name
      print x + y
      
  2. 连续监视变量

    • display

      在每次暂停时自动显示某变量的值:

      1
      
      display variable_name
      
  3. 停止监视变量

    • undisplay

      停止监视某个变量:

      1
      
      undisplay 1
      
  4. 修改变量值

    • set

      动态修改变量值,例如:

      1
      
      set variable_name = 10
      
  5. 查看内存内容

    • x

      查看指定地址的内存内容,例如:

      1
      
      x/10xw &variable_name  # 查看变量地址的十六进制内容
      

线程相关

  1. 查看线程信息

    • info threads 显示所有线程的信息。
  2. 切换线程

    • thread

      切换到指定的线程:

      1
      
      thread 2
      

其他常用命令

  1. 加载核心转储文件

    • core

      加载程序崩溃时生成的核心转储文件:

      1
      
      gdb ./program core
      
  2. 查看源代码

    • list

      1
      
      l
      

      查看程序源代码:

      1
      
      list 10  # 显示第10行附近的代码
      
  3. 调试共享库

    • info sharedlibrary 查看加载的共享库。
  4. 退出GDB

    • quitq 退出调试器。

调试快捷键

  • Ctrl + C:中断程序执行,返回GDB控制台。
  • Ctrl + D:退出GDB。

通过熟练掌握这些命令,你可以更高效地使用GDB调试程序,快速定位和解决问题。

如何在GDB中设置断点?

在GDB中设置断点可以让程序在指定的代码位置暂停,方便开发者检查程序的状态和行为。以下是设置断点的各种方法和用法:


设置断点的基本方法

  1. 在函数名处设置断点

    1
    
    break function_name
    

    示例:

    1
    2
    
    break main
    break my_function
    

    程序将在指定函数的入口处暂停。

  2. 在源文件的特定行设置断点

    1
    
    break filename:line_number
    

    示例:

    1
    2
    
    break my_program.c:42
    break utils.cpp:10
    

    程序将在指定文件的特定行暂停。

  3. 在当前文件的特定行设置断点 如果已经加载了当前源文件,可以直接指定行号:

    1
    
    break line_number
    

    示例:

    1
    
    break 25
    
  4. 设置条件断点 程序只有在满足条件时才会暂停:

    1
    
    break filename:line_number if condition
    

    示例:

    1
    2
    
    break main.c:50 if x == 10
    break my_function if y > 100
    
  5. 在地址处设置断点 如果你知道程序的具体内存地址,可以直接在地址处设置断点:

    1
    
    break *address
    

    示例:

    1
    
    break *0x4005d0
    
  6. 在特定类的成员函数处设置断点(C++)

    1
    
    break ClassName::MethodName
    

    示例:

    1
    
    break MyClass::doWork
    

查看断点

使用以下命令查看当前所有设置的断点:

1
info breakpoints

或者:

1
i b

它会显示断点的编号、位置、使能状态等信息。


删除断点

删除一个或多个断点:

1
delete breakpoint_number

示例:

1
2
delete 1      # 删除编号为1的断点
delete        # 删除所有断点

启用/禁用断点

  1. 启用断点

    1
    
    enable breakpoint_number
    

    示例:

    1
    
    enable 2
    
  2. 禁用断点

    1
    
    disable breakpoint_number
    

    示例:

    1
    
    disable 2
    

禁用断点后,程序将忽略该断点。


临时断点

设置断点后自动删除(仅生效一次):

1
tbreak location

示例:

1
2
tbreak main
tbreak my_function

断点命令

在断点触发时自动执行某些命令:

  1. 设置断点命令

    1
    
    commands breakpoint_number
    

    示例:

    1
    2
    3
    4
    
    commands 1
    > print x
    > continue
    > end
    

    在断点1触发时,打印变量x的值并继续执行。

  2. 清除断点命令

    1
    2
    
    commands breakpoint_number
    > end
    

断点的高级用法

  1. 忽略次数 让断点在被触发特定次数后暂停:

    1
    
    ignore breakpoint_number count
    

    示例:

    1
    
    ignore 1 3
    

    断点1会在被触发3次后才暂停。

  2. 查看断点的命中次数

    1
    
    info breakpoints
    

    输出的 Hits 列会显示断点命中次数。


示例操作

假设我们有一个简单的程序 main.c

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
    int x = 0;
    for (int i = 0; i < 10; i++) {
        x += i;
    }
    printf("x = %d\n", x);
    return 0;
}

使用GDB调试:

1
gdb ./a.out

设置断点并调试:

1
2
3
4
(gdb) break main              # 在main函数入口设置断点
(gdb) run                     # 运行程序,暂停在main函数
(gdb) break 6 if i == 5       # 在第6行设置条件断点,当i==5时暂停
(gdb) continue                # 继续执行程序

通过以上操作,可以精准控制程序的暂停位置,快速定位问题。

如何在GDB中查看变量的值?

在 GDB 中,可以通过多种方式查看变量的值,从简单的基本数据类型到复杂的数据结构(如数组、指针、结构体)。以下是查看变量值的常用方法:


1. 使用 print 命令

print 命令是查看变量值最常用的方法,可以显示变量的值或评估表达式。

基本用法

1
print variable_name

示例:

1
2
(gdb) print x
$1 = 42

其中 $1 是 GDB 为结果分配的编号。

查看表达式的值

可以直接评估表达式:

1
2
(gdb) print x + y
$2 = 62

查看指针指向的值

1
(gdb) print *pointer

示例:

1
2
(gdb) print *ptr
$3 = 100

查看数组元素

1
(gdb) print array[index]

示例:

1
2
(gdb) print my_array[2]
$4 = 50

查看结构体或类成员

1
print struct_variable.member

示例:

1
2
(gdb) print my_struct.field1
$5 = 123

2. 使用 info locals 查看局部变量

查看当前函数中所有局部变量的值:

1
info locals

示例:

1
2
3
(gdb) info locals
x = 42
y = 100

3. 使用 info args 查看函数参数

查看当前函数的参数值:

1
info args

示例:

1
2
3
(gdb) info args
a = 10
b = 20

4. 使用 display 命令监视变量值

display 命令可以在每次程序暂停时自动显示变量的值。

设置监视

1
display variable_name

示例:

1
2
(gdb) display x
1: x = 42

停止监视

1
undisplay display_number

示例:

1
(gdb) undisplay 1

显示所有监视的变量

1
info display

5. 使用 whatis 查看变量类型

如果不确定变量的类型,可以用 whatis 命令查看:

1
whatis variable_name

示例:

1
2
(gdb) whatis x
type = int

6. 使用 ptype 查看复杂变量类型

对于结构体或类等复杂类型,ptype 可以显示变量的完整定义:

1
ptype variable_name

示例:

1
2
3
4
5
(gdb) ptype my_struct
type = struct {
    int field1;
    int field2;
}

7. 查看内存内容

对于指针变量,可以通过 x 命令查看特定内存地址的值。

基本用法

1
x/format address

示例:

1
2
(gdb) x/4xw 0x7fffffffdea0
0x7fffffffdea0:  0x0000002a  0x00000000  0x00000001  0x00000000
  • 1
    
    /4xw
    

    的含义:

    • 4:显示 4 个值。
    • x:以十六进制格式显示。
    • w:每个值是 32 位(word)。

查看变量的内存地址

1
print &variable_name

示例:

1
2
(gdb) print &x
$6 = (int *) 0x7fffffffdea0

8. 查看全局变量

如果变量是全局变量,可以直接查看:

1
print global_variable

如果有多个文件,可以加上命名空间或文件名限定:

1
print file_name::global_variable

示例:

1
(gdb) print my_file.c::global_var

示例操作:调试程序中的变量

以下是一个简单的 C 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point p = {10, 20};
    int arr[3] = {1, 2, 3};
    int *ptr = arr;
    printf("Hello, GDB!\n");
    return 0;
}

编译程序并运行 GDB

1
2
gcc -g -o my_program my_program.c
gdb ./my_program

在 GDB 中查看变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(gdb) break main                  # 在 main 函数设置断点
(gdb) run                         # 运行程序
(gdb) print p                     # 查看结构体变量 p 的值
$1 = {x = 10, y = 20}

(gdb) print arr                   # 查看数组
$2 = {1, 2, 3}

(gdb) print arr[1]                # 查看数组元素
$3 = 2

(gdb) print *ptr                  # 查看指针指向的值
$4 = 1

(gdb) print &p                    # 查看变量的地址
$5 = (struct Point *) 0x7fffffffe110

(gdb) ptype p                     # 查看变量类型
type = struct Point {
    int x;
    int y;
}

通过这些方法,可以灵活地查看变量值,深入分析程序运行时的状态。

如何使用GDB进行程序调试时,定位内存泄漏问题?

GDB 本身不能直接检测内存泄漏,但它可以帮助跟踪动态内存分配和释放操作,并结合其他工具(如 Valgrind 或 AddressSanitizer)进行深入分析。以下是一些方法,可以帮助你使用 GDB 以及其他工具来定位和分析内存泄漏问题。


方法 1:利用 GDB 跟踪动态内存分配

通过设置断点或捕获内存分配函数(如 malloccallocreallocfree),可以手动分析内存的分配和释放情况。

步骤

  1. 在动态内存分配和释放函数上设置断点 在 GDB 中设置断点:

    1
    2
    
    (gdb) break malloc
    (gdb) break free
    
  2. 运行程序

    1
    
    (gdb) run
    
  3. 跟踪内存操作 每次触发断点时,查看调用栈和内存分配细节:

    1
    2
    3
    
    (gdb) backtrace         # 查看调用栈,找到分配或释放内存的位置
    (gdb) print size         # 查看 malloc 分配的内存大小
    (gdb) print ptr          # 检查指针地址
    
  4. 记录未释放的内存

    • 观察 malloc 调用是否有对应的 free
    • 如果发现没有释放的分配地址,可以分析代码路径,找到遗漏的释放操作。

示例

假设程序如下:

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>

void leak_memory() {
    int *p = (int *)malloc(sizeof(int) * 10);
}

int main() {
    leak_memory();
    return 0;
}

使用 GDB:

1
2
3
4
5
6
gdb ./a.out
(gdb) break malloc
(gdb) run
(gdb) print size
$1 = 40
(gdb) backtrace

通过调用栈可定位到未释放内存的具体代码位置。


方法 2:结合 Valgrind 检测内存泄漏

Valgrind 是专门检测内存问题的工具,能够生成详细的内存泄漏报告,并结合 GDB 进行调试。

步骤

  1. 运行程序并检测内存泄漏

    1
    
    valgrind --leak-check=full --track-origins=yes ./your_program
    
  2. 查看内存泄漏报告 Valgrind 的输出示例:

    1
    2
    3
    4
    
    ==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
    ==12345==    at 0x4C2F18F: malloc (vg_replace_malloc.c:309)
    ==12345==    by 0x4005F7: leak_memory (example.c:5)
    ==12345==    by 0x40060F: main (example.c:9)
    
  3. 使用 GDB 分析 根据 Valgrind 提供的调用栈,在 GDB 中设置断点,调试内存分配函数:

    1
    2
    3
    
    gdb ./your_program
    (gdb) break leak_memory
    (gdb) run
    

方法 3:结合 AddressSanitizer (ASan)

AddressSanitizer 是 GCC 和 Clang 提供的工具,能够检测内存泄漏和非法内存访问。

步骤

  1. 编译时启用 ASan 使用 -fsanitize=address 选项编译代码:

    1
    
    gcc -fsanitize=address -g -o your_program your_program.c
    
  2. 运行程序

    1
    
    ./your_program
    
  3. 查看内存泄漏报告 ASan 会显示内存泄漏的详细信息,包括泄漏大小、分配位置和调用栈。 示例输出:

    1
    2
    3
    4
    5
    6
    7
    
    =================================================================
    ==12345==ERROR: LeakSanitizer: detected memory leaks
       
    Direct leak of 40 byte(s) in 1 object(s) allocated from:
        #0 0x4c2f18f in malloc (asan_malloc.c:92)
        #1 0x4005f7 in leak_memory (example.c:5)
        #2 0x40060f in main (example.c:9)
    
  4. 结合 GDB 调试 使用 ASan 报告提供的行号和堆栈信息,在 GDB 中设置断点进行调试。


方法 4:使用 GDB 的 catch 命令

GDB 提供了 catch 命令,可以捕获动态内存分配事件。

步骤

  1. 捕获动态内存事件

    1
    2
    
    (gdb) catch malloc
    (gdb) catch free
    
  2. 运行程序

    1
    
    (gdb) run
    
  3. 分析事件触发时的状态 每次内存分配或释放时,GDB 会暂停。使用以下命令分析状态:

    1
    2
    3
    
    (gdb) backtrace    # 查看调用栈
    (gdb) print size   # 检查 malloc 分配大小
    (gdb) print ptr    # 检查指针地址
    

方法 5:手动记录和审查代码

如果工具无法完全定位问题,可以通过以下方法进行手动检查:

  1. 审查动态内存分配和释放的匹配

    • 确保每次 mallocnew 都有对应的 freedelete
    • 检查异常路径是否导致释放操作遗漏。
  2. 添加日志 在动态内存分配和释放函数中记录日志:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    void* my_malloc(size_t size) {
        void* ptr = malloc(size);
        printf("Allocated %p (%zu bytes)\n", ptr, size);
        return ptr;
    }
       
    void my_free(void* ptr) {
        printf("Freed %p\n", ptr);
        free(ptr);
    }
    
  3. 分析日志记录 运行程序后,通过日志记录检查未释放的内存分配。


总结

  • 轻量级检查:使用 GDB 结合断点或 catch 跟踪动态内存操作。
  • 全面分析:结合 Valgrind 或 AddressSanitizer 检测内存泄漏,结合 GDB 调试。
  • 代码优化:通过手动审查动态内存管理代码和日志记录,找出内存泄漏的根本原因。

通过这些方法,可以高效定位内存泄漏问题,并优化程序的内存管理。

本文由作者按照 CC BY 4.0 进行授权