GDB 入门--断点, 信息浏览, 运行过程控制

GDB 入门--断点, 信息浏览, 运行过程控制

[TOC]
本文介绍 GDB 中比较核心的操作: 断点操作, 信息浏览, 运行过程控制的入门.

断点

GDB 调试器支持在程序中打 4 种断点: 普通断点(breakpoints) , 观察断点(watchpoints), 捕捉断点(catchpoints), 跟踪断点(tracepoints).

每个断点都有其编号(记为 bnum, 正整数序列), 方便后面的管理与操作.

普通断点

创建断点: break( b ) 命令

  1. 位置断点: (gdb) break location. location 可以为以下类型.
  • linenum: 是一个整数, 表示当前代码文件的行号. 如何知道程序中各行代码都有对应的行号 => 可通过执行 l命令看到.

  • + offset - offset: offset 为整数, +offset 表示以当前程序暂停位置为准, 向后数 offset 行处打断点 . -offset 表示以当前程序暂停位置为准, 向前数 offset 行处打断点.

  • filename::linenum : 文件 filename 中的第 linenum 行打断点. 如果 filename 是相对路径, 会匹配所有的尾部与 filename 一致的文件. For example, if filename is gcc/expr.c, then it will match source file name of /build/trunk/gcc/expr.c, but not /build/trunk/libcpp/expr.c or /build/trunk/gcc/x-expr.c.

  • function: 表示程序中包含的函数的函数名, 即 break 命令会在该函数内部的开头位置打断点, 程序会执行到该函数第一行代码处暂停. 包括 :: 修饰的范围. 也是贪心地匹配: break func 会匹配 A::B::funcB::func.

    • 不贪心的选项: break -qualified func sets a breakpoint on a free-function named func ignoring any C++ class methods and namespace functions called func.
    • 贪心的意义: 在 C/C++ 中除了使用 static/命名空间等方式限制函数的链接性, 函数一般都是具有外部链接性的, 也就是很多文件都可以去使用函数, 为了方便对跨文件的所有函数进行追踪, 默认设置成贪心的.

  • function:label: 在 function 中 label 所在的行.

  • filename:function: filename 中的 function 入口.

  • label: 从当前栈帧匹配某个函数中含有 label 的行.

  • address: 内存地址, 可以是变量/函数/寄存器的地址.

  1. 条件断点: (gdb) break location if cond
    每次程序执行到 location 位置时都计算 cond 的值, 如果为 True, 则程序在该位置暂停 . 反之, 程序继续执行.

创建断点: tbreak 命令

打的断点仅会作用 1 次.
用法同 break.

创建断点: rbreak 命令

rbreak 命令的作用对象是 C/C++ 程序中的函数, 它会在指定函数的开头位置打断点.

1
(gdb) rbreak regex
  • regex 为一个正则表达式, 程序中函数的函数名只要满足 regex 条件, tbreak 命令就会其内部的开头位置打断点.
  • 断点不会自动消失.

注意: 通过行号进行设置断点的一个弊端是, 如果你更改了源程序, 那么之前设置的断点就可能不是你想要的了.

一些特殊情况下的断点

  • 在函数的第一条汇编命令打断点

    • 通常给函数打断点的命令: b func, 不会把断点设置在汇编命令层次函数的开头. 如果要把断点设置在汇编命令层次函数的开头 b *func.
  • 在匿名空间设置断点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    namespace Foo
    {
    void foo()
    {
    }
    }

    namespace
    {
    void bar()
    {
    }
    }
    • 对 namespace Foo 中的 foo 函数设置断点

      1
      (gdb) b Foo::foo
    • 对匿名空间中的 bar 函数设置断点

      1
      (gdb) b (anonymous namespace)::bar
  • 在程序地址上打断点

    • 当调试汇编程序, 或者没有调试信息的程序时, 经常需要在程序地址上打断点, 方法为 b *address.
      1
      2
      3
      4
      5
      6
      7
      8
      0000000000400522 <main>:
      400522: 55 push %rbp
      400523: 48 89 e5 mov %rsp,%rbp
      400526: 8b 05 00 1b 00 00 mov 0x1b00(%rip),%eax # 40202c <he+0xc>
      40052c: 85 c0 test %eax,%eax
      40052e: 75 07 jne 400537 <main+0x15>
      400530: b8 7c 06 40 00 mov $0x40067c,%eax
      400535: eb 05 jmp 40053c <main+0x1a>
      1
      (gdb) b *0x400522

查看断点信息

1
2
(gdb) info breakpoint [bnum]
(gdb) info break [bnum]
  • bnum 作为可选参数, 为某个断点的编号.

  • 输出信息含义

    • 断点编号( Num ) , 断点类型( Type ) , 是临时断点还是永久断点( Disp ) , 目前是启用状态还是禁用状态( Enb ) , 断点的位置( Address ) , 断点当前的状态( 作用的行号 , 已经命中的次数等, 用 What 列表示 ).

删除断点

  • clear 命令
    删除指定位置处的所有断点.

    1
    (gdb) clear location
  • delete(d) 命令

1
delete breakpoints [bnum]

不指定 bnum 参数, 则 delete 命令会删除当前程序中存在的所有断点.

禁用/启用断点

  • enable 命令

激活用 bnum… 参数指定的多个断点, 如果不设定 bnum…, 表示激活所有禁用的断点.

1
enable breakpoints [bnum...]

临时激活以 bnum… 为编号的多个断点, 但断点只能使用 1 次, 之后会自动回到禁用状态.

1
enable breakpoints once [bnum...]

临时激活以 bnum… 为编号的多个断点, 断点可以使用 count 次, 之后进入禁用状态.

1
enable breakpoints count [bnum...]

激活 bnum.. 为编号的多个断点, 但断点只能使用 1 次, 之后会被永久删除.

1
enable breakpoints delete [bnum...]
  • disable 命令
1
disable breakpoints [bnum...]

观察断点: watch 命令

监控某个变量或者表达式的值的变化情况.

1
(gdb) watch cond

分为 3 类命令:

  • rwatch 命令: 只要程序中出现读取目标变量( 表达式 )的值的操作, 程序就会停止运行.
  • awatch 命令: 只要程序中出现读取目标变量( 表达式 )的值或者改变值的操作, 程序就会停止运行.
  • watch 命令: 只有当被监控变量( 表达式 )的值发生改变, 程序才会停止运行.

几点需要注意:

  • 当监控的变量( 表达式 )为局部变量( 表达式 )时, 一旦局部变量( 表达式 )失效, 则监控操作也随即失效.
  • 如果监控的是一个指针变量( 例如 *p ), 则 watch *pwatch p 是有区别的, 前者监控的是 p 所指数据的变化情况, 而后者监控的是 p 指针本身有没有改变指向.

这 3 个监控命令还可以用于监控数组中元素值的变化情况, 例如对于 a[10] 这个数组, watch a 表示只要 a 数组中存储的数据发生改变, 程序就会停止执行.

watch 命令的 2 种实现原理

  • 硬件观察点 Hardware watchpoint

    • 系统会为 GDB 调试器提供少量的寄存器( 例如 32 位的 Intel x86 处理器提供有 4 个调试寄存器 ), 每个寄存器都可以作为一个观察点协助 GDB 完成监控任务.
    • 基于寄存器个数的限制, 如果调试环境中设立的硬件观察点太多, 则有些可能会失去作用.
    • 可能会出现: 无法使用硬件观察点监控数据类型占用字节数较多的变量( 表达式 ). 比如说, 某些操作系统中, GDB 调试器最多只能监控 4 个字节长度的数据, 这意味着 C/C++ 中 double 类型的数据是无法使用硬件观察点监测的. 这种情况下, 可以考虑将其换成占用字符串少的 float 类型.
  • 软件观察点 software watchpoint

    • GDB 调试器会以单步执行的方式运行程序, 并且每行代码执行完毕后, 都会检测该目标变量( 表达式 )的值是否发生改变, 如果改变则程序执行停止.
    • 这种”实时”的判别方式, 一定程度上会影响程序的执行效率.

GDB 会优先尝试建立硬件观察点, 只有当前环境不支持硬件观察点时, 才会建立软件观察点.

  • 强制 GDB 调试器只建立软件观察点

    1
    set can-use-hw-watchpoints 0
  • awatchrwatch 命令只能设置硬件观察点, 如果系统不支持或者借助如上命令禁用, 则 GDB 调试器会打印如下信息: Expression cannot be implemented with read/access watchpoint.

异常捕捉断点: catch 命令

捕捉断点监控异常的发生

1
(gdb) catch exception
exception 类型 含义
throw(exception) 当程序中抛出 exception 指定类型异常时, 程序停止执行. 如果不指定异常类型( 即省略 exception ), 则表示只要程序发生异常, 程序就停止执行.
catch(exception) 当程序中捕获到 exception 异常时, 程序停止执行. exception 参数也可以省略, 表示无论程序中捕获到哪种异常, 程序都暂停执行.
load(regexp)
unload(regexp)
其中, regexp 表示目标动态库的名称, load 命令表示当 regexp 动态库加载时程序停止执行 . unload 命令表示当 regexp 动态库被卸载时, 程序暂停执行. regexp 参数也可以省略, 此时只要程序中某一动态库被加载或卸载, 程序就会暂停执行.

当前 GDB 调试器对监控 C++ 程序中异常的支持还有待完善. 要求 GCC 编译器的版本最低为 4.8. 因为匹配过程需要借助 libstdc++ 库中的一些 SDT 探针, 而这些探针最早出现在 GCC 4.8 版本中.

catch 无法捕获以交互方式引发的异常.

对特定函数设置 catchpoint

  • catch fork 命令为 fork 调用设置 catchpoint. 当 fork 调用发生后, gdb 会暂停程序的运行.
  • catch vfork 命令为 vfork 调用设置c catchpoint. 当 vfork 调用发生后, gdb 会暂停程序的运行.
  • catch exec 命令为 exec 系列系统调用设置 catchpoint. 当 exec 调用发生后, gdb 会暂停程序的运行.
  • 目前只有 HP-UX 和 GNU/Linux 支持.

为系统调用设置 catchpoint

1
catch syscall [name | number]

例子:

1
2
3
(gdb) catch syscall mmap
#OR
(gdb) catch syscall 9

mmap 调用发生后, gdb 会暂停程序的运行.

系统调用和编号的映射参考具体的 xml 文件: Ubuntu 下在 /usr/local/share/gdb/syscalls 文件夹下的 amd64-linux.xml.

通过为 ptrace 调用设置 catchpoint 破解 anti-debugging 的程序

代码例子

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/ptrace.h>
#include <stdio.h>

int main()
{
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0 ) {
printf("Gdb is debugging me, exit.\n");
return 1;
}
printf("No debugger, continuing\n");
return 0;
}

有些程序不想被 gdb 调试, 它们就会在程序中调用 ptrace 函数, 一旦返回失败, 就证明程序正在被 gdb 等类似的程序追踪, 所以就直接退出.

1
2
3
4
5
6
7
8
9
10
11
(gdb) start
Temporary breakpoint 1 at 0x400508: file a.c, line 6.
Starting program: /data2/home/nanxiao/a

Temporary breakpoint 1, main () at a.c:6
6 if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0 ) {
(gdb) n
7 printf("Gdb is debugging me, exit.\n");
(gdb)
Gdb is debugging me, exit.
8 return 1;

破解这类程序的办法就是为 ptrace 调用设置 catchpoint, 通过修改 ptrace 的返回值, 达到目的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) catch syscall ptrace
Catchpoint 2 (syscall 'ptrace' [101])
(gdb) r
Starting program: /data2/home/nanxiao/a

Catchpoint 2 (call to syscall ptrace), 0x00007ffff7b2be9c in ptrace () from /lib64/libc.so.6
(gdb) c
Continuing.

Catchpoint 2 (returned from syscall ptrace), 0x00007ffff7b2be9c in ptrace () from /lib64/libc.so.6
(gdb) set $rax = 0
(gdb) c
Continuing.
No debugger, continuing
[Inferior 1 (process 11491) exited normally]

通过修改 rax 寄存器的值, 达到修改返回值的目的, 从而让 gdb 可以继续调试程序.

tcatch 命令

只监控一次.

up 命令

catch 命令捕获到指定的 exception 时, 程序暂停执行的位置往往位于某个系统库( 例如 libstdc++ )中. up 命令, 即可返回发生 exception 的源代码处.

条件设置: condition 命令

为现有的普通断点, 观察断点以及捕捉断点添加条件表达式, 也可以对条件断点的条件表达式进行修改.

  • 添加或修改 expression 条件表达式.
    1
    (gdb) condition bnum expression
  • 删除 bnum 编号断点的条件表达式, 使其变成普通的无条件断点.
    1
    (gdb) condition bnum

不同类型的断点设置条件

  • 条件断点

    • (gdb) break location if cond
  • 观察条件断点

    • (gdb) watch expr if cond
  • 捕捉条件断点

    • 无法直接生成, 需要借助 condition 命令为现有捕捉断点增添一个 cond 表达式, 才能使其变成条件断点.

ignore 命令

使目标断点暂时失去作用, 当断点失效的次数超过指定次数时, 断点的功能会自动恢复.

1
ignore bnum count

跟踪断点: tracecollect 命令

在不打断原程序运行的前提下, 检测运行情况. 比较适合注重实时性的场景. 这里不展开.

断点的保存与读取

应用场景: 很多情况下我们都是 debug 后修改代码再次 debug 确认修改的效果, 这时候多次打的断点是一样的, 因此反复打同样断点, 进行重复操作是浪费时间, 我们希望将断点信息保存下来, 下次直接读取.

  • 保存
1
2
save breakpoints [filename]
save br [filename]

将当下所有的断点信息(4 类)包括断点附带的条件判断, 有效的次数等保存到 filename 文件里.
注意, watchpoints 里面的一些局部变量可能再次读入后无效, 因为再次运行时不一定会执行到上次的地方.
filename 文件是文本(相当于配置文件/脚本文件), 可以按需更改, 修改断点信息, 增加删除断点等.

  • 读取
    1
    source [-s] [-v] filename 

信息浏览

打印变量/表达式值

  • 手动打印一次: print(p)
1
(gdb) print expr
  • 打印更多信息/格式
1
(gdb) print [options --] [/fmt] expr

options 参数和 /fmt 或者 expr 之间, 必须用–( 2 个 - 字符 )分隔.
options: 选项可以控制 print 命令输出指定内容的变量或者表达式的值. 一些常用选项:

  • -address on|off: 查看某一指针变量的值时, 是否同时打印其占用的内存地址, 默认值为 on. 该选项等同于单独执行 set print address on|off 命令.

  • -array on|off: 是否以便于阅读的格式输出数组中的元素, 默认值为 off. 该选项等同于单独执行 set printf array on|off 命令.

  • -array-indexes on|off: 对于非字符类型数组, 在打印数组中每个元素值的同时, 是否同时显示每个元素对应的数组下标, 默认值为 off. 该选项等同于单独执行 set print array-indexes on|off 命令.

  • -pretty on|off: 以便于阅读的格式打印某个结构体变量的值, 默认值为 off. 该选项等同于单独执行 set print pretty on|off 命令.

  • fmt: 指定输出变量或表达式值时所采用的格式.

@ 运算符

输出数组中指定区域的元素.

1
(gdb) print first@len

参数 first 用于指定数组查看区域内的首个元素的值, 参数 len 用于命令自 first 元素开始查看的元素个数.

:: 运算符

与 C++ 中的作用域解析符类似. 明确指定要查看的目标变量或表达式或行号所属的文件/函数.

1
2
(gdb) print file::variable
(gdb) print function::variable

应用举例: 打印静态变量的值

代码: 两个源文件中存在相同名称的静态变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* main.c */
extern void print_var_1(void);
extern void print_var_2(void);

int main(void)
{
print_var_1();
print_var_2();
return 0;
}

/* static-1.c */
#include <stdio.h>

static int var = 1;

void print_var_1(void)
{
printf("var = %d\n", var);
}

/* static-2.c */
#include <stdio.h>

static int var = 2;

void print_var_2(void)
{
printf("var = %d\n", var);
}

如果直接打印静态变量, 则结果并不一定是你想要的:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 第一次
$ gcc -g main.c static-1.c static-2.c
$ gdb -q ./a.out
(gdb) start
(gdb) p var
$1 = 2

# 第二次
$ gcc -g main.c static-2.c static-1.c
$ gdb -q ./a.out
(gdb) start
(gdb) p var
$1 = 1

显式地指定文件名(上下文)就可以明确到底是哪个了.

1
2
3
4
(gdb) p 'static-1.c'::var
$1 = 1
(gdb) p 'static-2.c'::var
$2 = 2

打印大数组中的内容

在gdb中, 如果要打印大数组的内容, 缺省最多会显示200个元素.

1
2
3
4
5
6
7
8
9
10
int main()
{
int array[401];
int i;

for (i = 0; i < 401; i++)
array[i] = i;

return 0;
}

设置最大限制数

1
2
3
(gdb) set print elements number-of-elements #指定数目
(gdb) set print elements 0 # 无限制
(gdb) set print elements unlimited #无限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) print array 
$1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,
177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199...}
(gdb) set print elements 0
(gdb) print array
$2 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,
177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228,
229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280,
281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332,
333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384,
385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400}

打印索引下标

前面介绍 print 函数提到的 (gdb) set print array-indexes on

1
2
3
4
5
6
(gdb) p num
$1 = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512}
(gdb) set print array-indexes on

(gdb) p num
$2 = {[0] = 1, [1] = 2, [2] = 4, [3] = 8, [4] = 16, [5] = 32, [6] = 64, [7] = 128, [8] = 256, [9] = 512}

自动打印: display

每当程序暂停执行( 例如单步执行 )时, GDB 调试器都会自动帮我们打印出来, 而 print 命令则不会.

1
2
(gdb) display expr
(gdb) display/fmt expr

fmt 用于指定输出变量或表达式的格式

选项 含义
/x 以十六进制的形式打印出整数.
/d 以有符号 , 十进制的形式打印出整数.
/u 以无符号 , 十进制的形式打印出整数.
/o 以八进制的形式打印出整数.
/t 以二进制的形式打印出整数.
/f 以浮点数的形式打印变量或表达式的值.
/c 以字符形式打印变量或表达式的值.

注意: display 和 /fmt 之间不要留有空格.

自动显示列表

对于使用 display 命令查看的目标变量或表达式, 都会被记录在一张列表.
info dispaly :打印此表

1
2
3
4
5
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: y /t result
1: y num
  • Num 列为各变量或表达式的编号, GDB 调试器为每个变量或表达式都分配有唯一的编号 .
  • Enb 列表示当前各个变量( 表达式 )是处于激活状态还是禁用状态.
  • Expression 列: 表示查看的变量或表达式.

删除自动显示

1
2
(gdb) undisplay num
(gdb) delete display num

禁用自动显示

1
2
(gdb) disable display num
(gdb) enable display num

栈信息查看

栈帧的概念

程序执行时调用了多少个函数, 就会相应产生多少个栈帧, 其中每个栈帧自函数调用时生成, 函数调用结束后自动销毁.
main() 主函数对应的栈帧, 又称为初始帧或者最外层的帧.
不同操作系统为栈帧选定地址标识符的规则不同.
GDB 调试器会按照既定规则对它们进行编号: 当前正被调用函数对应的栈帧的编号为 0, 调用它的函数对应栈帧的编号为 1, 以此类推.

frame 命令

查看当前栈帧
1
(gdb) info frame

显示内容如下:

  • 当前栈帧的编号, 以及栈帧的地址.
  • 当前栈帧对应函数的存储地址, 以及该函数被调用时的代码存储的地址.
  • 当前函数的调用者, 对应的栈帧的地址.
  • 编写此栈帧所用的编程语言.
  • 函数参数的存储地址以及值.
  • 函数中局部变量的存储地址.
  • 栈帧中存储的寄存器变量.
查看指定帧
1
(gdb) frame spec
  • 通过栈帧的编号指定. 0 为当前被调用函数对应的栈帧号, 最大编号的栈帧对应的函数通常就是 main() 函数.
  • 借助栈帧的地址指定. 栈帧地址可以通过 info frame 命令打印出的信息中看到.
  • 通过函数的函数名指定. 注意, 如果是类似递归函数, 其对应多个栈帧的话, 通过此方法指定的是编号最小的那个栈帧.

updown 命令

1
2
(gdb) up n
(gdb) down n

n 为整数, 默认值为 1. 该命令表示在当前栈帧编号( 假设为 m )的基础上, 选定 m+n / m-n 为编号的栈帧作为新的当前栈帧.

backtrace 命令

1
(gdb) backtrace [-full] [n]
  • n: 可选, 一个整数值, 当为正整数时, 表示打印最里层的 n 个栈帧的信息 . n 为负整数时, 那么表示打印最外层 n 个栈帧的信息.
  • -full: 可选, 打印栈帧信息的同时, 打印出局部变量的值.

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>

void fun_a(void)
{
int a = 0;
printf("%d\n", a);
}

void fun_b(void)
{
int b = 1;
fun_a();
printf("%d\n", b);
}

void fun_c(void)
{
int c = 2;
fun_b();
printf("%d\n", c);
}

void fun_d(void)
{
int d = 3;
fun_c();
printf("%d\n", d);
}

int main(void)
{
int var = -1;
fun_d();
return 0;
}

在函数 fun_a 里打上断点, 当程序断住时, 显示调用栈信息:

1
2
3
4
5
6
(gdb) bt
#0 fun_a () at a.c:6
#1 0x000109b0 in fun_b () at a.c:12
#2 0x000109e4 in fun_c () at a.c:19
#3 0x00010a18 in fun_d () at a.c:26
#4 0x00010a4c in main () at a.c:33

bt full 命令显示各个函数的局部变量值:

1
2
3
4
5
6
7
8
9
10
11
(gdb) bt full
#0 fun_a () at a.c:6
a = 0
#1 0x000109b0 in fun_b () at a.c:12
b = 1
#2 0x000109e4 in fun_c () at a.c:19
c = 2
#3 0x00010a18 in fun_d () at a.c:26
d = 3
#4 0x00010a4c in main () at a.c:33
var = -1

使用 bt full n, 从内向外显示 n 个栈桢, 及其局部变量, 例如:

1
2
3
4
5
6
(gdb) bt full 2
#0 fun_a () at a.c:6
a = 0
#1 0x000109b0 in fun_b () at a.c:12
b = 1
(More stack frames follow...)

bt full -n 从外向内显示n个栈桢, 及其局部变量, 例如:

1
2
3
4
5
(gdb) bt full -2
#3 0x00010a18 in fun_d () at a.c:26
d = 3
#4 0x00010a4c in main () at a.c:33
var = -1

打印尾调用堆栈帧信息

启动 尾调用(Tail call) 优化(介绍可以参考链接). 本部分是对 optimized code 进行 debug 设置的一个例子, gdb 中有更多的针对优化后代码进行 debug 的场景.

1
gcc -g -O -o test test.c

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
void a(void)
{
printf("Tail call frame\n");
}

void b(void)
{
a();
}

void c(void)
{
b();
}

int main(void)
{
c();
return 0;
}

查看 main 函数汇编代码. 可以看到 main函数直接调用了函数 a, 根本看不到函数 b 和函数 c 的影子.

1
2
3
4
5
6
7
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000400565 <+0>: sub $0x8,%rsp
0x0000000000400569 <+4>: callq 0x400536 <a>
0x000000000040056e <+9>: mov $0x0,%eax
0x0000000000400573 <+14>: add $0x8,%rsp
0x0000000000400577 <+18>: retq

在函数 a 入口处打上断点, 程序停止后, 打印堆栈帧信息. 看不到尾调用的相关信息.

1
2
3
4
5
6
7
8
9
(gdb) i frame
Stack level 0, frame at 0x7fffffffe590:
rip = 0x400536 in a (test.c:4); saved rip = 0x40056e
called by frame at 0x7fffffffe5a0
source language c.
Arglist at 0x7fffffffe580, args:
Locals at 0x7fffffffe580, Previous frame's sp is 0x7fffffffe590
Saved registers:
rip at 0x7fffffffe588

设置 debug entry-values 选项为非 0 的值, 这样除了输出正常的函数堆栈帧信息以外, 还可以输出尾调用的相关信息. 可以看到输出了”tailcall: initial:”信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(gdb) set debug entry-values 1
(gdb) b test.c:4
Breakpoint 1 at 0x400536: file test.c, line 4.
(gdb) r
Starting program: /home/nanxiao/test

Breakpoint 1, a () at test.c:4
4 {
(gdb) i frame
tailcall: initial:
Stack level 0, frame at 0x7fffffffe590:
rip = 0x400536 in a (test.c:4); saved rip = 0x40056e
called by frame at 0x7fffffffe5a0
source language c.
Arglist at 0x7fffffffe580, args:
Locals at 0x7fffffffe580, Previous frame's sp is 0x7fffffffe590
Saved registers:
rip at 0x7fffffffe588

函数相关变量查看

  • 查看当前函数各个参数的值
1
(gdb) info args
  • 只查看当前函数中各局部变量的值
1
(gdb) info locals

打印进程内存信息

如果想查看进程的内存映射信息, 可以使用 i proc mappings 命令.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(gdb) i proc mappings
process 27676 flags:
PR_STOPPED Process (LWP) is stopped
PR_ISTOP Stopped on an event of interest
PR_RLC Run-on-last-close is in effect
PR_MSACCT Microstate accounting enabled
PR_PCOMPAT Micro-state accounting inherited on fork
PR_FAULTED : Incurred a traced hardware fault FLTBPT: Breakpoint trap

Mapped address spaces:

Start Addr End Addr Size Offset Flags
0x8046000 0x8047fff 0x2000 0xfffff000 -s--rwx
0x8050000 0x8050fff 0x1000 0 ----r-x
0x8060000 0x8060fff 0x1000 0 ----rwx
0xfee40000 0xfef4efff 0x10f000 0 ----r-x
0xfef50000 0xfef55fff 0x6000 0 ----rwx
0xfef5f000 0xfef66fff 0x8000 0x10f000 ----rwx
0xfef67000 0xfef68fff 0x2000 0 ----rwx
0xfef70000 0xfef70fff 0x1000 0 ----rwx
0xfef80000 0xfef80fff 0x1000 0 ---sr--
0xfef90000 0xfef90fff 0x1000 0 ----rw-
0xfefa0000 0xfefa0fff 0x1000 0 ----rw-
0xfefb0000 0xfefb0fff 0x1000 0 ----rwx
0xfefc0000 0xfefeafff 0x2b000 0 ----r-x
0xfeff0000 0xfeff0fff 0x1000 0 ----rwx
0xfeffb000 0xfeffcfff 0x2000 0x2b000 ----rwx
0xfeffd000 0xfeffdfff 0x1000 0 ----rwx

也可以用 i files(还有一个同样作用的命令: i target)命令, 它可以更详细地输出进程的内存信息, 包括引用的动态链接库等等.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) i files
Symbols from "/data1/nan/a".
Unix /proc child process:
Using the running image of child Thread 1 (LWP 1) via /proc.
While running this, GDB does not access memory from...
Local exec file:
`/data1/nan/a', file type elf32-i386-sol2.
Entry point: 0x8050950
0x080500f4 - 0x08050105 is .interp
0x08050108 - 0x08050114 is .eh_frame_hdr
0x08050114 - 0x08050218 is .hash
0x08050218 - 0x08050418 is .dynsym
0x08050418 - 0x080507e6 is .dynstr
0x080507e8 - 0x08050818 is .SUNW_version
0x08050818 - 0x08050858 is .SUNW_versym
0x08050858 - 0x08050890 is .SUNW_reloc
0x08050890 - 0x080508c8 is .rel.plt
0x080508c8 - 0x08050948 is .plt
......
0xfef5fb58 - 0xfef5fc48 is .dynamic in /usr/lib/libc.so.1
0xfef5fc80 - 0xfef650e2 is .data in /usr/lib/libc.so.1
0xfef650e2 - 0xfef650e2 is .bssf in /usr/lib/libc.so.1
0xfef650e8 - 0xfef65be0 is .picdata in /usr/lib/libc.so.1
0xfef65be0 - 0xfef666a7 is .data1 in /usr/lib/libc.so.1
0xfef666a8 - 0xfef680dc is .bss in /usr/lib/libc.so.1

打印特定范围内存的值: x 命令

格式为 x/nfu addr. 含义为以 f 格式打印从 addr 开始的 n 个长度单元为 u 的内存值.
参数:

  • n: 输出单元的个数.
  • f: 输出格式. 比如 x 是以 16 进制形式输出, o 是以 8 进制形式输出, 等等.
  • u: 标明一个单元的长度. b 是一个 byte, h 是两个 byte(halfword), w 是四个 byte(word), g是八个 byte(giant word).

例子: 以无符号 10 进制格式打印数组 a 前 16 个 byte 的值:

1
2
3
(gdb) x/16tb a
0x7fffffffe4a0: 00000000 00000001 00000010 00000011 00000100 00000101 00000110 00000111
0x7fffffffe4a8: 00001000 00001001 00001010 00001011 00001100

打印程序动态分配内存的信息

gdb 是不支持打印动态分配的内存的, 可以通过自定义的命令实现此功能.

例子代码:

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

int main(void)
{
char *p[10];
int i = 0;

for (i = 0; i < sizeof(p) / sizeof(p[0]); i++)
{
p[i] = malloc(100000);
}
return 0;
}

编译

1
g++ -g -fpermissive t.cpp

自定义 GDB 命令

1
2
3
4
5
define mallocinfo
set $__f = fopen("/dev/tty", "w")
call malloc_info(0, $__f)
call fclose($__f)
end

运行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Temporary breakpoint 5, main () at a.c:7
7 int i = 0;
(gdb) mallocinfo
<malloc version="1">
<heap nr="0">
<sizes>
</sizes>
<total type="fast" count="0" size="0"/>
<total type="rest" count="0" size="0"/>
<system type="current" size="135168"/>
<system type="max" size="135168"/>
<aspace type="total" size="135168"/>
<aspace type="mprotect" size="135168"/>
</heap>
<total type="fast" count="0" size="0"/>
<total type="rest" count="0" size="0"/>
<system type="current" size="135168"/>
<system type="max" size="135168"/>
<aspace type="total" size="135168"/>
<aspace type="mprotect" size="135168"/>
</malloc>
$20 = 0
$21 = 0
(gdb) n
9 for (i = 0; i < sizeof(p)/sizeof(p[0]); i++)
(gdb)
11 p[i] = malloc(100000);
(gdb)
9 for (i = 0; i < sizeof(p)/sizeof(p[0]); i++)
(gdb)
11 p[i] = malloc(100000);
(gdb)
9 for (i = 0; i < sizeof(p)/sizeof(p[0]); i++)
(gdb)
11 p[i] = malloc(100000);
(gdb)
9 for (i = 0; i < sizeof(p)/sizeof(p[0]); i++)
(gdb)
11 p[i] = malloc(100000);
(gdb)
9 for (i = 0; i < sizeof(p)/sizeof(p[0]); i++)
(gdb)
11 p[i] = malloc(100000);
(gdb) mallocinfo
<malloc version="1">
<heap nr="0">
<sizes>
</sizes>
<total type="fast" count="0" size="0"/>
<total type="rest" count="0" size="0"/>
<system type="current" size="532480"/>
<system type="max" size="532480"/>
<aspace type="total" size="532480"/>
<aspace type="mprotect" size="532480"/>
</heap>
<total type="fast" count="0" size="0"/>
<total type="rest" count="0" size="0"/>
<system type="current" size="532480"/>
<system type="max" size="532480"/>
<aspace type="total" size="532480"/>
<aspace type="mprotect" size="532480"/>
</malloc>
$22 = 0
$23 = 0

可以看到 gdb 输出了动态分配内存的变化信息.

打印变量的类型和所在文件

1
2
(gdb) whatis he
type = struct child

复合类型详细的类型信息:

1
2
3
4
5
(gdb) ptype he
type = struct child {
char name[10];
enum {boy, girl} gender;
}

查看定义该变量的文件: i variables regex

1
2
3
4
5
6
7
8
9
(gdb) i variables he
All variables matching regular expression "he":

File variable.c:
struct child he;

Non-debugging symbols:
0x0000000000402030 she
0x00007ffff7dd3380 __check_rhosts_file

用正则表达式进行约束后:

1
2
3
4
5
(gdb) i variables ^he$
All variables matching regular expression "^he$":

File variable.c:
struct child he;

i variables 不会显示局部变量.

C++ 中按照派生类型打印对象

例子代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;

class Shape {
public:
virtual void draw () {}
};

class Circle : public Shape {
int radius;
public:
Circle () { radius = 1; }
void draw () { cout << "drawing a circle...\n"; }
};

class Square : public Shape {
int height;
public:
Square () { height = 2; }
void draw () { cout << "drawing a square...\n"; }
};

void drawShape (class Shape &p)
{
p.draw ();
}

int main (void)
{
Circle a;
Square b;
drawShape (a);
drawShape (b);
return 0;
}

在 gdb 中, 当打印一个对象时, 缺省是按照声明的类型进行打印:

1
2
3
4
5
(gdb) frame
#0 drawShape (p=...) at object.cxx:25
25 p.draw ();
(gdb) p p
$1 = (Shape &) @0x7fffffffde90: {_vptr.Shape = 0x400a80 <vtable for Circle+16>}

明确派生类类型: set print object on.

1
2
3
4
(gdb) set print object on

(gdb) p p
$2 = (Circle &) @0x7fffffffde90: {<Shape> = {_vptr.Shape = 0x400a80 <vtable for Circle+16>}, radius = 1}

当打印对象类型信息时, 该设置也会起作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(gdb) whatis p
type = Shape &
(gdb) ptype p
type = class Shape {
public:
virtual void draw(void);
} &

(gdb) set print object on
(gdb) whatis p
type = /* real type = Circle & */
Shape &
(gdb) ptype p
type = /* real type = Circle & */
class Shape {
public:
virtual void draw(void);
} &

打印源代码行: list( l) 命令

list(简写为 l )命令来显示源代码以及行号.
list 命令可以指定行号, 函数:

1
2
(gdb) l 24
(gdb) l main

指定向前或向后打印:

1
2
(gdb) l -
(gdb) l +

指定范围:

1
(gdb) l 1,10

info(i) 命令

显示一些关于被调试程序的通用信息(Generic command for showing things about the program being debugged).

help info 产生的详细命令一览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
info address -- Describe where symbol SYM is stored
info all-registers -- List of all registers and their contents
info args -- Argument variables of current stack frame
info auto-load -- Print current status of auto-loaded files
info auto-load-scripts -- Print the list of automatically loaded Python scripts
info auxv -- Display the inferior's auxiliary vector
info bookmarks -- Status of user-settable bookmarks
info breakpoints -- Status of specified breakpoints (all user-settable breakpoints if no argument)
info checkpoints -- IDs of currently known checkpoints
info classes -- All Objective-C classes
info common -- Print out the values contained in a Fortran COMMON block
info copying -- Conditions for redistributing copies of GDB
info dcache -- Print information on the dcache performance
info display -- Expressions to display when program stops
info exceptions -- List all Ada exception names
info extensions -- All filename extensions associated with a source language
info files -- Names of targets and files being debugged
info float -- Print the status of the floating point unit
info frame -- All about selected stack frame
info frame-filter -- List all registered Python frame-filters
info functions -- All function names
info guile -- Prefix command for Guile info displays
info handle -- What debugger does when program gets various signals
info inferiors -- IDs of specified inferiors (all inferiors if no argument)
info line -- Core addresses of the code for a source line
info locals -- Local variables of current stack frame
info macro -- Show the definition of MACRO
info macros -- Show the definitions of all macros at LINESPEC
info mem -- Memory region attributes
info os -- Show OS data ARG
info pretty-printer -- GDB command to list all registered pretty-printers
info probes -- Show available static probes
info proc -- Show /proc process information about any running process
info program -- Execution status of the program
info record -- Info record options
info registers -- List of integer registers and their contents
info scope -- List the variables local to a scope
info selectors -- All Objective-C selectors
info set -- Show all GDB settings
info sharedlibrary -- Status of loaded shared object libraries
info signals -- What debugger does when program gets various signals
info skip -- Display the status of skips
info source -- Information about the current source file
info sources -- Source files in the program
info stack -- Backtrace of the stack
info static-tracepoint-markers -- List target static tracepoints markers
info symbol -- Describe what symbol is at location ADDR
info target -- Names of targets and files being debugged
---Type <return> to continue, or q <return> to quit---
info tasks -- Provide information about all known Ada tasks
info terminal -- Print inferior's saved terminal status
info threads -- Display currently known threads
info tracepoints -- Status of specified tracepoints (all tracepoints if no argument)
info tvariables -- Status of trace state variables and their values
info type-printers -- GDB command to list all registered type-printers
info types -- All type names
info unwinder -- GDB command to list unwinders
info variables -- All global and static variable names
info vector -- Print the status of the vector unit
info vtbl -- Show the virtual function table for a C++ object
info warranty -- Various kinds of warranty you do not have
info watchpoints -- Status of specified watchpoints (all watchpoints if no argument)
info win -- List of all displayed windows
info xmethod -- GDB command to list registered xmethod matchers

常用介绍

  • i registers

    • 不包括浮点寄存器和向量寄存器的内容. 使用 i all-registers 命令, 可以输出所有寄存器的内容.
    • 单个寄存器的值, 可以使用 i registers regname 或者 p $regname.
  • i sharedlibrary [regex]

    • 显示程序加载的共享链接库信息, 其中 regex 可以是正则表达式.
  • i functions [regex]

    • 列出可执行文件的所有函数名称.
    • 列出函数原型以及不带调试信息的函数.

show 命令

显示 gdb 本身的相关信息(Generic command for showing things about the debugger). 很多 show 命令其实是和 set 命令对应的. 这里不展示其命令 list.
常用介绍

  • show version: 查看 gdb 版本信息.
  • show architecture: 显示执行程序的架构, 例如 i386:x86-64.

运行过程控制

跳转

单步调试

  • next(n)

    • step out: 当遇到包含调用函数的语句时, 无论函数内部包含多少行代码, next 命令都会一步执行完.
  • step

    • step in: 所执行的代码行中包含函数时, 会进入该函数内部, 并在函数第一行代码处停止执行.
  • until(u)

    • GDB 调试器快速运行完当前的循环体, 并运行至循环体外停止.

      1
      (gdb) until
      • 注意, until 命令并非任何情况下都会发挥这个作用, 只有当执行至循环体尾部( 最后一行代码 )时, until 才会发生此作用 . 反之, until 命令和 next 命令的功能一样, 只是单步执行程序.

指示 GDB 调试器直接执行至指定位置后停止

1
(gdb) until location

jump 命令

略过某些代码, 直接跳到 location 处的代码继续执行程序.

1
(gdb) jump location

注意代码本身的逻辑, 随意跳转可能会导致程序崩溃或出现调试过程中产生的 Bug(实际运行可能不会产生).
jump 跳转到的位置后续没有断点, 那么 GDB 会直接执行自跳转处开始的后续代码.

修改程序中的值

改变字符串的值

  1. set VAR="STRING" 设置.
  2. 也可以通过访问内存地址的方法改变字符串的值.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Temporary breakpoint 2, main () at a.c:5
5 char p1[] = "Sam";
(gdb) n
6 char *p2 = "Bob";
(gdb) p p1
$1 = "Sam"
(gdb) p &p1
$2 = (char (*)[4]) 0x80477a4
(gdb) set {char [4]} 0x80477a4 = "Ace"
(gdb) n
8 printf("p1 is %s, p2 is %s\n", p1, p2);
(gdb)
p1 is Ace, p2 is Bob
9 return 0;

在改变字符串的值时候, 一定要注意内存越界的问题.

设置变量的值

  1. set var VAR=expr
  2. set {type}address=expr: 给存储地址在 address, 变量类型为 type 的变量赋值.
1
2
3
4
5
6
7
8
9
Breakpoint 2, func () at a.c:5
5 int i = 2;
(gdb) n
7 return i;
(gdb) p &i
$5 = (int *) 0x8047a54
(gdb) set {int}0x8047a54 = 8
(gdb) p i
$6 = 8
  1. 寄存器也可以作为变量, 因此同样可以修改寄存器的值: set var $REG = 8.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Breakpoint 2, func () at a.c:5
5 int i = 2;
(gdb)
(gdb) n
7 return i;
(gdb)
8 }
(gdb) set var $eax = 8
(gdb) n
main () at a.c:15
15 printf("%d\n", a);
(gdb)
8
16 return 0;

修改 PC 寄存器的值

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(void)
{
int a =0;

a++;
a++;
printf("%d\n", a);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(gdb) disassemble main
Dump of assembler code for function main():
0x0000000000400526 <+0>: push %rbp
0x0000000000400527 <+1>: mov %rsp,%rbp
0x000000000040052a <+4>: sub $0x10,%rsp
0x000000000040052e <+8>: movl $0x0,-0x4(%rbp)
0x0000000000400535 <+15>: addl $0x1,-0x4(%rbp)
0x0000000000400539 <+19>: addl $0x1,-0x4(%rbp)
0x000000000040053d <+23>: mov -0x4(%rbp),%eax
0x0000000000400540 <+26>: mov %eax,%esi
0x0000000000400542 <+28>: mov $0x4005e4,%edi
0x0000000000400547 <+33>: mov $0x0,%eax
0x000000000040054c <+38>: callq 0x400400 <printf@plt>
0x0000000000400551 <+43>: mov $0x0,%eax
0x0000000000400556 <+48>: leaveq
0x0000000000400557 <+49>: retq
End of assembler dump.
(gdb) i line 6
Line 6 of "t.cpp" starts at address 0x400535 <main()+15> and ends at 0x400539 <main()+19>.
(gdb) i line 7
Line 7 of "t.cpp" starts at address 0x400539 <main()+19> and ends at 0x40053d <main()+23>.
Breakpoint 1, main () at t.cpp:4
4 int a = 0;
(gdb) n
6 a++;
(gdb) n
7 a++;
(gdb) p a
$3 = 1
(gdb) set var $pc=0x40053d
(gdb) n
1
9 return 0;
(gdb) p a
$4 = 1

通过 info line 6info line 7 命令可以知道两条 a++; 语句的汇编命令起始地址与结束地址, 进而可以跳过其中一个.

断点处执行命令改变程序的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

void drawing (int n)
{
if (n != 0)
puts ("Try again?\nAll you need is a dollar, and a dream.");
else
puts ("You win $3000!");
}

int main (void)
{
int n;

srand (time (0));
n = rand () % 10;
printf ("Your number is %d\n", n);
drawing (n);

return 0;
}

可以看到, 当程序运行到断点处, 会自动把变量 n 的值修改为0, 然后继续执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) b drawing
Breakpoint 1 at 0x40064d: file win.c, line 6.
(gdb) command 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>silent
>set variable n = 0
>continue
>end
(gdb) r
Starting program: /home/xmj/tmp/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Your number is 6
You win $3000!
[Inferior 1 (process 4134) exited normally]

如果你在调试一个大程序, 重新编译一次会花费很长时间, 比如调试编译器的 bug, 那么你可以用这种方式在 gdb 中先实验性的修改下试试, 而不需要修改源码, 重新编译.

修改被调试程序的二进制文件

例子代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

void drawing (int n)
{
if (n != 0)
puts ("Try again?\nAll you need is a dollar, and a dream.");
else
puts ("You win $3000!");
}

int main (void)
{
int n;

srand (time (0));
n = rand () % 10;
printf ("Your number is %d\n", n);
drawing (n);

return 0;
}

gdb 默认是以只读方式加载程序的. 有 2 种方式可以修改程序的二进制文件.

  1. 命令行选项指定为可写:
    1
    2
    3
    $ gcc -write ./a.out
    (gdb) show write
    Writing into executable and core files is on.
  2. 运行中设置
    1
    2
    (gdb) set write on
    (gdb) file ./a.out

查看反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(gdb) disassemble /mr drawing 
Dump of assembler code for function drawing:
5 {
0x0000000000400642 <+0>: 55 push %rbp
0x0000000000400643 <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000400646 <+4>: 48 83 ec 10 sub $0x10,%rsp
0x000000000040064a <+8>: 89 7d fc mov %edi,-0x4(%rbp)

6 if (n != 0)
0x000000000040064d <+11>: 83 7d fc 00 cmpl $0x0,-0x4(%rbp)
0x0000000000400651 <+15>: 74 0c je 0x40065f <drawing+29>

7 puts ("Try again?\nAll you need is a dollar, and a dream.");
0x0000000000400653 <+17>: bf e0 07 40 00 mov $0x4007e0,%edi
0x0000000000400658 <+22>: e8 b3 fe ff ff callq 0x400510 <[email protected]>
0x000000000040065d <+27>: eb 0a jmp 0x400669 <drawing+39>

8 else
9 puts ("You win $3000!");
0x000000000040065f <+29>: bf 12 08 40 00 mov $0x400812,%edi
0x0000000000400664 <+34>: e8 a7 fe ff ff callq 0x400510 <[email protected]>

10 }
0x0000000000400669 <+39>: c9 leaveq
0x000000000040066a <+40>: c3 retq

End of assembler dump.

修改二进制代码(注意大小端和命令长度):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(gdb) set variable *(short*)0x400651=0x0ceb
(gdb) disassemble /mr drawing
Dump of assembler code for function drawing:
5 {
0x0000000000400642 <+0>: 55 push %rbp
0x0000000000400643 <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000400646 <+4>: 48 83 ec 10 sub $0x10,%rsp
0x000000000040064a <+8>: 89 7d fc mov %edi,-0x4(%rbp)

6 if (n != 0)
0x000000000040064d <+11>: 83 7d fc 00 cmpl $0x0,-0x4(%rbp)
0x0000000000400651 <+15>: eb 0c jmp 0x40065f <drawing+29>

7 puts ("Try again?\nAll you need is a dollar, and a dream.");
0x0000000000400653 <+17>: bf e0 07 40 00 mov $0x4007e0,%edi
0x0000000000400658 <+22>: e8 b3 fe ff ff callq 0x400510 <[email protected]>
0x000000000040065d <+27>: eb 0a jmp 0x400669 <drawing+39>

8 else
9 puts ("You win $3000!");
0x000000000040065f <+29>: bf 12 08 40 00 mov $0x400812,%edi
0x0000000000400664 <+34>: e8 a7 fe ff ff callq 0x400510 <[email protected]>

10 }
0x0000000000400669 <+39>: c9 leaveq
0x000000000040066a <+40>: c3 retq

End of assembler dump.

可以看到, 条件跳转命令 je 已经被改为无条件跳转 jmp 了.
退出, 运行一下

1
2
3
$ ./a.out 
Your number is 2
You win $3000!

callprint 命令的跳转功能

使用 gdb 调试程序运行时, 可以使用 callprint 命令直接调用特定函数执行. 注意当前上下文是否支持对特定函数的支持.

GDB 入门--断点, 信息浏览, 运行过程控制

https://www.chuxin911.com/GDB_note_1_20220731/

作者

cx

发布于

2022-07-31

更新于

2022-07-31

许可协议