44、深入探索GDB调试技巧与C/C++代码调试

深入探索GDB调试技巧与C/C++代码调试

1. 随机数与缓冲区溢出模拟

在调试过程中,有时需要模拟一些复杂情况,比如缓冲区溢出。以下是一段模拟缓冲区溢出的代码:

// Seed the random number generator so that each run is different.
srand(time(NULL));

// Loop count - a nice high number.
int n = 800000;

// We want the chance of overrun to be 1 in N just to make this hard
// to catch.
int thresh = RAND_MAX / n;

for (i = 0; i < n; i++) {
    // Overrun if the random number is less than the threshold
    int len = (rand() < thresh) ? buflen + 1 : buflen;
    ovrrun(buf, text, len);
}

// Overrun is easy to detect but hard to catch.
int overran = (strlen(buf) > buflen);
if (overran)
    printf("OVERRUN!\n");
else
    printf("No overrun\n");

free(buf);
return overran;

这段代码的流程如下:

graph TD;
    A[初始化随机数种子] --> B[设置循环次数n];
    B --> C[计算溢出阈值thresh];
    C --> D[进入循环];
    D --> E{随机数是否小于阈值};
    E -- 是 --> F[设置长度为buflen + 1];
    E -- 否 --> G[设置长度为buflen];
    F --> H[调用ovrrun函数];
    G --> H;
    H --> I{循环是否结束};
    I -- 否 --> D;
    I -- 是 --> J[检查是否溢出];
    J -- 是 --> K[输出OVERRUN!];
    J -- 否 --> L[输出No overrun];
    K --> M[释放缓冲区];
    L --> M;
    M --> N[返回溢出结果];

通过随机数来控制是否发生缓冲区溢出,并且在循环结束后检查是否发生了溢出。

2. GDB基本命令

GDB(GNU Debugger)是一个强大的调试工具,以下是一些基本的GDB命令:
- print :提供独特且丰富的格式化语法,可显示各种类型的数据,如字符串和数组,打印的对象可以是内存中的对象或任何有效的C或C++表达式。
- x :examine的缩写,与print命令类似,但x用于处理内存地址和原始数据,而print可以处理抽象表达式。
- printf :与C语言中的同名函数一样,遵循相同的格式化规则。
- whatis :告诉你GDB所知道的给定符号的类型信息。
- backtrace :显示当前程序的调用栈,可选择显示局部变量。
- up, down :更改栈帧,以便检查调用栈不同部分的局部变量。
- frame :是up和down命令的替代,允许指定要跳转的特定帧,帧通过backtrace命令中列出的编号指定。
- info locals :info命令的子命令,显示当前栈帧中的所有局部变量。

3. print和x命令的表达式语法
  • 基本用法区别
  • print (缩写为 p )命令可以接受几乎任何有效的C或C++表达式作为参数。例如:
(gdb) whatis foo
type = long long int
(gdb) p foo
$2 = 4096
  • x 命令接受一个地址作为参数,并显示该地址的内存内容。当将变量作为 x 命令的参数时,即使该变量不是指针,也会被视为地址。例如:
(gdb) x foo
0x1000: Cannot access memory at address 0x1000
(gdb) x &foo
0xbf9af240:     0x00001000
  • 修饰符的使用
    print x 命令都允许使用修饰符来改变输出行为,修饰符与命令之间用斜杠分隔。例如:
(gdb) p/x foo
$2 = 0x1000
(gdb) x/d &foo
0x22eec4:       4096

以下是 print x 命令的输出修饰符列表:
| Modifier | Format | print | x |
| — | — | — | — |
| x | Hexadecimal | Yes | Yes |
| d | Signed decimal | Yes | Yes |
| u | Unsigned decimal | Yes | Yes |
| o | Octal | Yes | Yes |
| t | Binary | Yes | Yes |
| a | Address | Prints hexadecimal and shows its relationship to nearby symbols. | Yes |
| c | Character | Dumps memory in pairs—an ASCII character with a decimal byte. | Yes |
| f | Floating point | Display memory in floating point, using the current word size. Use g for IEEE double and w for IEEE float on 32 - bit machines. | Yes |
| i | Instructions | No | Disassembly memory at the given location. |
| s | Null - terminated ASCII string | No | Display memory as an ASCII string. Output stops at the first NUL character. |

此外, x 命令还允许指定转储内存时使用的字大小以及要转储的字数。例如:

(gdb) x/8bx &foo
0x22eec4:       0x00    0x10    0x00    0x00    0xd8    0xef    0x22    0x00

x 命令使用的字大小后缀如下表所示:
| Suffix | Word Size |
| — | — |
| b | Byte (8 bits) |
| h | Half word (2 bytes) |
| w | Word (4 bytes) |
| g | Giant (8 bytes) |

4. print命令示例
  • 调用函数 :GDB可以调用函数,例如:
(gdb) p getpid()
$1 = 12903
(gdb) p kill(getpid(),0)
$2 = 0
(gdb) p kill(getpid(),9)
Program terminated with signal SIGKILL, Killed.
  • 打印数组 :对于不同类型的数组, print x 命令有不同的输出效果。
    c double dblarr[] = {1,2,3,4}; float fltarr[] = {1,2,3,4}; int intarr[] = {1,2,3,4};
    (gdb) p intarr $5 = {10, 20, 30, 40} (gdb) x/4wx intarr 0x8049610 <intarr>: 0x0000000a 0x00000014 0x0000001e 0x00000028 (gdb) x/2gx intarr 0x8049610 <intarr>: 0x000000140000000a 0x000000280000001e
  • 处理浮点数数组 :使用 x 命令处理浮点数数组时要注意字大小的选择。
    (gdb) p fltarr $7 = {10, 20, 30, 40} (gdb) x/4wf fltarr 0x8049600 <fltarr>: 10 20 30 40 (gdb) x/2gf fltarr 0x8049600 <fltarr>: 134217760.5625 34359746808
  • 打印数组元素 :可以使用C语法打印数组的单个值或多个元素。
    (gdb) p *intarr $4 = 10 (gdb) p intarr[1] $5 = 20 (gdb) p intarr[1]@2 $6 = {20, 30}
  • 打印字符串 :不同类型的字符串变量在 print x 命令下有不同的输出。
    c const char ccarr[] = "This is NUL terminated.\0Oops! you shouldn't see this."; const char *ccptr = ccarr;
    (gdb) p ccarr $1 = "This is NUL terminated.\000Oops! you shouldn't see this." (gdb) p ccptr $2 = 0x8048440 "This is NUL terminated." (gdb) x/s ccarr 0x8048440 <ccarr>: "This is NUL terminated."
    还可以使用C语法强制转换类型来改变输出。
    (gdb) p (char*) ccarr $3 = 0x403040 "This is NUL terminated."
  • 反汇编机器代码 :使用 x 命令的 i 格式可以反汇编内存中的机器代码。
    (gdb) x/10i main 0x401050 <main>: push %ebp 0x401051 <main+1>: mov %esp,%ebp 0x401053 <main+3>: sub $0x28,%esp ...
5. 从GDB调用函数

GDB允许调用程序中可见的任何函数,函数在运行进程的上下文中执行,并消耗被调试进程的栈和其他资源。
- 使用 call 命令调用函数
(gdb) call getpid() $1 = 27274
- 使用返回值调用其他函数
(gdb) call kill($1,0) $2 = 0
- 使用 set 命令修改变量值 set 命令可以修改运行程序空间中的值,它接受几乎任何有效的C表达式作为参数。

6. C++模板调试注意事项
  • 模板函数的实例化 :C++模板允许以通用方式定义代码,编译器根据具体类型生成源代码。例如:
    c++ template <class Typ> void swapvals( Typ &a, Typ &b) { Typ tmp = a; a = b; b = tmp; }
    使用时需要指定具体类型进行实例化,如 swapvals<double>(a,b);
  • 设置断点 :由于模板会生成多个不同类型的函数,设置断点需要一些技巧。可以使用 info functions 命令查找函数,然后使用 b 命令设置断点。例如:
    (gdb) info func swapvals All functions matching regular expression "swapvals": File templ.cpp: void void swapvals<Foo>(Foo&, Foo&); void void swapvals<double>(double&, double&); void void swapvals<int>(int&, int&); (gdb) b 'void swapvals<int>(int&, int&)' Breakpoint 3 at 0x8048434: file templ.cpp, line 8.
    也可以使用 rbreak 命令为所有匹配正则表达式的函数设置断点,但要注意可能会设置不必要的断点。
7. C++标准模板库调试
  • 容器的调试 :C++标准模板库(STL)中的容器是使用模板实现的动态存储结构,调试使用容器的代码可能具有挑战性,因为容器会隐藏底层实现。可以使用GDB调用容器的方法来检查容器的状态。
    c++ int myarray[3]; std::vector<int> myvector(3);
    使用 whatis 命令查看类型信息:
    (gdb) whatis myarray type = int [3] (gdb) whatis myvector type = std::vector<int,std::allocator<int> >
    调用容器的方法:
    (gdb) p myvector.size() $1 = 3
    但要注意,C++模板只有在使用时才会生成代码,如果代码中没有使用某个方法,可能无法调用。
  • 迭代器的使用 :GDB可以理解迭代器,使用与指针类似的语法打印迭代器指向的数据。
    c++ std::list<int> mylist; std::list<int>::iterator x = mylist.begin();
    (gdb) p *x $1 = (int &) @0x804c1f8: 100
    但在GDB中不能使用指针运算或 ++ 运算符遍历容器。
8. display命令

display 命令会在程序每次停止时打印指定的表达式,可以显示多个表达式,并使用与 x 命令相同的语法进行格式化。
例如,对于以下程序:

#include <stdio.h>
#include <string>
#include <algorithm>

int main(int argc, char *argv[])
{
    int i = 0;
    std::string token = "ABCD";

    // Simple loop goes through every permutation of a string
    // using std::next_permutation.
    do {
        i++;
    } while (std::next_permutation(token.begin(), token.end()));

    printf("%d permutations\n", i);
    return 0;
}

在GDB中设置断点并使用 display 命令:

Breakpoint 2, main (argc=1, argv=0xbffce1a4) at permute.cpp:13
13                      i++;
(gdb) display/xw token.c_str()
1: x/xw token.c_str ()  0x804a014:      0x44434241
(gdb) display/s token.c_str()
2: x/s token.c_str ()  0x804a014:        "ABCD"
(gdb) cont
...

每次程序停止时,都会显示指定的表达式。可以使用 undisplay 命令停止显示某个表达式。

通过以上介绍,我们深入了解了GDB的各种调试技巧,包括随机数与缓冲区溢出模拟、基本命令使用、表达式语法、函数调用、C++模板和标准模板库的调试以及 display 命令的使用。这些技巧将帮助我们更高效地调试C和C++代码。

深入探索GDB调试技巧与C/C++代码调试(续)

9. 综合运用GDB调试技巧

在实际的调试过程中,往往需要综合运用前面介绍的各种GDB调试技巧。以下是一个较为复杂的示例,展示如何将这些技巧结合起来解决问题。

假设我们有一个包含多个函数和复杂数据结构的C++程序,程序在运行过程中出现了崩溃。我们可以按照以下步骤进行调试:

  1. 启动GDB并加载程序
    gdb ./your_program
  2. 设置断点
    • 可以根据程序的逻辑和可能出现问题的位置设置断点。例如,如果怀疑某个函数 func1 有问题,可以使用 b func1 设置断点。
    • 对于C++模板函数,如前面提到的 swapvals ,可以使用 info functions 查找函数并使用 b 设置断点。
  3. 运行程序
    run
  4. 查看调用栈
    当程序在断点处停止或崩溃时,使用 backtrace 命令查看调用栈,了解程序的执行路径。
    (gdb) backtrace
  5. 检查局部变量
    使用 info locals 命令查看当前栈帧中的局部变量。如果需要查看特定变量的类型,可以使用 whatis 命令。
    (gdb) info locals (gdb) whatis variable_name
  6. 打印变量和内存
    • 使用 print 命令打印变量的值, x 命令查看内存内容。可以结合修饰符改变输出格式。
      (gdb) p variable_name (gdb) x/d &variable_name
  7. 调用函数
    如果需要测试某个函数或检查程序的状态,可以使用 call 命令调用函数。
    (gdb) call function_name()
  8. 使用 display 命令
    如果需要在每次程序停止时查看某个变量或表达式的值,可以使用 display 命令。
    (gdb) display variable_name
  9. 修改变量值
    使用 set 命令修改变量的值,以便测试不同的情况。
    (gdb) set variable_name = new_value
10. 调试技巧总结与建议
  • 养成良好的调试习惯
    • 在编写代码时,添加适当的注释和日志输出,方便调试时追踪程序的执行流程。
    • 尽量将复杂的逻辑拆分成多个小函数,这样在调试时可以更容易定位问题。
  • 合理使用断点
    • 不要设置过多的断点,以免影响程序的执行效率。可以根据程序的逻辑和可能出现问题的位置有针对性地设置断点。
    • 对于循环中的代码,可以设置条件断点,只在满足特定条件时停止程序。
  • 注意数据类型和内存管理
    • 在使用 print x 命令时,要注意数据类型和内存布局,避免因错误的格式或字大小导致输出结果不准确。
    • 对于动态分配的内存,要确保在使用完后及时释放,避免内存泄漏。
  • 充分利用GDB的帮助文档
    GDB提供了详细的帮助文档,可以使用 help 命令查看各种命令的使用方法和说明。
11. 总结

本文深入介绍了GDB调试工具的各种技巧,包括随机数与缓冲区溢出模拟、基本命令使用、表达式语法、函数调用、C++模板和标准模板库的调试以及 display 命令的使用。通过实际的代码示例和操作步骤,展示了如何在不同的场景下运用这些技巧进行调试。

在调试C和C++代码时,GDB是一个强大的工具,但要熟练掌握它需要不断的实践和经验积累。希望本文介绍的调试技巧能够帮助读者更高效地发现和解决程序中的问题,提高编程效率和代码质量。

以下是一个简单的调试流程总结:

graph TD;
    A[启动GDB并加载程序] --> B[设置断点];
    B --> C[运行程序];
    C --> D{程序是否停止};
    D -- 是 --> E[查看调用栈];
    E --> F[检查局部变量];
    F --> G[打印变量和内存];
    G --> H[调用函数];
    H --> I[使用display命令];
    I --> J[修改变量值];
    J --> K{是否解决问题};
    K -- 否 --> B;
    K -- 是 --> L[结束调试];
    D -- 否 --> C;

通过遵循这个流程,结合前面介绍的各种调试技巧,我们可以更系统地进行调试工作,提高调试效率。

总之,掌握GDB调试技巧对于C和C++程序员来说是非常重要的,它可以帮助我们快速定位和解决程序中的问题,确保程序的稳定性和可靠性。在实际的开发过程中,不断运用这些技巧,积累经验,我们将能够更加熟练地使用GDB,成为更优秀的程序员。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值