GDB 入门--反汇编, 反向调试, 脚本, 杂项

GDB 入门--反汇编, 反向调试, 脚本, 杂项

[TOC]
本文介绍 GDB 中的反汇编, 反向调试, 脚本, 杂项相关操作.

汇编命令

设置汇编命令格式

在 Intel x86 处理器上, gdb 默认显示汇编命令格式是 AT&T 格式.
目前 set disassembly-flavor 命令只能用在 Intel x86 处理器上, 并且取值只有 intelatt.

查看反汇编命令: disassemble(disas)

  • 反汇编一个函数
    1
    disass func_name
  • 反汇编一段内存地址, 第1个参数是起始地址, 第2个是终止地址
    1
    disass 0×0 0×10
    不带参数的话, 默认的反汇编范围是所选择帧的 pc 附近的函数.
    可以使用 info line 命令来映射一个源码行到程序地址, 然后使用命令 disassemble 显示一个地址范围的机器命令. 例子如下:
1
2
3
4
5
6
(gdb) info line main
Line 34 of "rank.c" starts at address 0x804847f
and ends at 0×8048493 .
(gdb) info line *0x804847f
Line 34 of "rank.c" starts at address 0x804847f
and ends at 0×8048493 .

查看反汇编命令的另一个方法: x

x/3i $pc: 显示 pc 开始的 3 条命令.

自动反汇编后面要执行的代码

  • 在任意情况下反汇编后面要执行的代码
    1
    (gdb) set disassemble-next-line on
  • 在后面的代码没有源码的情况下才反汇编后面要执行的代码
    1
    (gdb) set disassemble-next-line auto

将源程序和汇编命令映射起来

例子代码

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

typedef struct
{
int a;
int b;
int c;
int d;
}ex_st;

int main(void) {
ex_st st = {1, 2, 3, 4};
printf("%d,%d,%d,%d\n", st.a, st.b, st.c, st.d);
return 0;
}

disas /m fun 将函数代码和汇编命令映射起来, 显示代码每行对应的汇编命令.

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
(gdb) disas /m main
Dump of assembler code for function main:
11 int main(void) {
0x00000000004004c4 <+0>: push %rbp
0x00000000004004c5 <+1>: mov %rsp,%rbp
0x00000000004004c8 <+4>: push %rbx
0x00000000004004c9 <+5>: sub $0x18,%rsp

12 ex_st st = {1, 2, 3, 4};
0x00000000004004cd <+9>: movl $0x1,-0x20(%rbp)
0x00000000004004d4 <+16>: movl $0x2,-0x1c(%rbp)
0x00000000004004db <+23>: movl $0x3,-0x18(%rbp)
0x00000000004004e2 <+30>: movl $0x4,-0x14(%rbp)

13 printf("%d,%d,%d,%d\n", st.a, st.b, st.c, st.d);
0x00000000004004e9 <+37>: mov -0x14(%rbp),%esi
0x00000000004004ec <+40>: mov -0x18(%rbp),%ecx
0x00000000004004ef <+43>: mov -0x1c(%rbp),%edx
0x00000000004004f2 <+46>: mov -0x20(%rbp),%ebx
0x00000000004004f5 <+49>: mov $0x400618,%eax
0x00000000004004fa <+54>: mov %esi,%r8d
0x00000000004004fd <+57>: mov %ebx,%esi
0x00000000004004ff <+59>: mov %rax,%rdi
0x0000000000400502 <+62>: mov $0x0,%eax
0x0000000000400507 <+67>: callq 0x4003b8 <[email protected]>

14 return 0;
0x000000000040050c <+72>: mov $0x0,%eax

15 }
0x0000000000400511 <+77>: add $0x18,%rsp
0x0000000000400515 <+81>: pop %rbx
0x0000000000400516 <+82>: leaveq
0x0000000000400517 <+83>: retq

End of assembler dump.

查看某一行对应的汇编命令

行所对应的地址范围

1
2
(gdb) i line 13
Line 13 of "foo.c" starts at address 0x4004e9 <main+37> and ends at 0x40050c <main+72>.

disassemble [Start],[End] 命令反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) disassemble 0x4004e9, 0x40050c
Dump of assembler code from 0x4004e9 to 0x40050c:
0x00000000004004e9 <main+37>: mov -0x14(%rbp),%esi
0x00000000004004ec <main+40>: mov -0x18(%rbp),%ecx
0x00000000004004ef <main+43>: mov -0x1c(%rbp),%edx
0x00000000004004f2 <main+46>: mov -0x20(%rbp),%ebx
0x00000000004004f5 <main+49>: mov $0x400618,%eax
0x00000000004004fa <main+54>: mov %esi,%r8d
0x00000000004004fd <main+57>: mov %ebx,%esi
0x00000000004004ff <main+59>: mov %rax,%rdi
0x0000000000400502 <main+62>: mov $0x0,%eax
0x0000000000400507 <main+67>: callq 0x4003b8 <[email protected]>
End of assembler dump.

显示将要执行的汇编命令

display n/i $pc 命令显示当程序停止时, 将要执行的 n 条汇编命令.
取消显示可以用 undisplay 命令.

显示程序原始机器码

例子代码

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
printf("Hello, world\n");
return 0;
}

disassemble /r 命令可以用 16 进制形式显示程序的原始机器码.

源码操作

edit 命令

作用: 编辑源文件

1
2
(gdb) edit location
(gdb) edit filename : location

默认使用的是 ex 编译器. export EDITOR=/usr/bin/vim: 修改默认编辑器.

search 命令

作用: 搜索源码, 使用正则表达式. 可以实现正向或者反向搜索.

1
2
search regexp
reverse-search regexp

调试多线程程序

常用命令

  • info threads
    查看当前调试环境中包含多少个线程, 并打印出各个线程的相关信息, 包括线程编号( ID ) , 线程名称等.

    1
    (gdb) info threads [id...]

    id 列前标有 * 号的线程即为当前线程

  • thread id
    将线程编号为 id 的线程设置为当前线程.

  • thread apply id command

    • 将 command 命令作用于指定编号 id 的线程. command 指 GDB 命令(包括自定义命令), 如 next , continue 等.
    • 如果想将 command 命令作用于所有线程, id 可以用 all 代替.
    • 注意: 默认情况下, 无论哪个线程暂停执行, 其它线程都会随即暂停 . 反之, 一旦某个线程启动( 借助 next , step , continue 命令 ), 其它线程也随即启动. GDB 调试默认的这种调试模式( 称为全停止模式 all-stop ).
  • break location thread id
    在 location 指定的位置建立普通断点, 并且该断点仅用于暂停编号为 id 的线程.

    1
    2
    (gdb) break location thread id
    (gdb) break location thread id if cond

    当某个线程执行遇到断点时, GDB 调试器会自动将该线程作为当前线程, 并提示用户 [Switching to Thread n]

  • set scheduler-locking off|on|step

    • 默认情况下, 当程序中某一线程暂停执行时, 所有执行的线程都会暂停 . 同样, 当执行 continue 命令时, 默认所有暂停的程序都会继续执行. 该命令可以打破此默认设置, 即只继续执行当前线程, 其它线程仍停止执行.
    • off: 不锁定线程, 任何线程都可以随时执行.
    • on: 锁定线程, 只有当前线程或指定线程可以运行.
    • step: 当单步执行某一线程时, 其它线程不会执行, 同时保证在调试过程中当前线程不会发生改变. 但如果该模式下执行 continue , until , finish 命令, 则其它线程也会执行, 并且如果某一线程执行过程遇到断点, 则 GDB 调试器会将该线程作为当前线程.
  • 设置观察点只针对特定线程生效: watch expr thread threadnum
    只有编号为 threadnum 的线程改变了变量的值, 程序才会停下来, 其它编号线程改变变量的值不会让程序停住.

使用 GDB 调试多线程程序时, 同一时刻我们调试的焦点都只能是某个线程, 被称为当前线程.

输入的调试命令并不仅仅作用于当前线程, 例如 continue , next 等, 默认情况下它们作用于所有线程.

non-stop 模式

all-stop 模式下, continue , next , step 命令的作用对象并不是当前线程, 而是所有的线程 . 但在 non-stop 模式下, continue , next , step 命令只作用于当前线程.
7.0 版本以上的 GDB 调试器, 才支持 non-stop 模式.
应用场景

  • 保持其它线程继续执行的状态下, 单独调试某个线程.
  • 在所有线程都暂停执行的状态下, 单步调试某个线程.
  • 单独执行多个线程等等.

在 non-stop 模式下, 如果想要 continue 命令作用于所有线程, 可以为 continue 命令添加一个 -a 选项, 即执行 continue -a 或者 c -a 命令, 即可实现令所有线程继续执行的目的.

启用开关: (gdb) set non-stop [on/off].

interrupt 命令

在 all-stop 模式下, interrupt 命令作用于所有线程, 即该命令可以令整个程序暂停执行 . 而在 non-stop 模式下, interrupt 命令仅作用于当前线程. 如果想另其作用于所有线程, 可以执行 interrupt -a 命令.

反向调试

反向调试, 指的是临时改变程序的执行方向, 反向执行指定行数的代码, 此过程中 GDB 调试器可以消除这些代码所做的工作, 将调试环境还原到这些代码未执行前的状态.

注意一下几点:

  • 对反向调试的功能支持还不完善, 反向调试尚不能适用于所有的调试场景.
  • 于程序中出现的打印语句( 例如 C 语言中的 printf() 输出函数 ), 虽然可以进行反向调试, 但已经输出到屏幕上的数据不会因反向调试而撤销.
  • 反向调试也不适用于包含 I/O 操作的代码.
  • 需要 7.0 及以上版本的 GDB 调试器.
  • 多线程里不支持反向调试.
  • return 命令不能在 reverse 模式中使用.

反向调试的常用命令

让程序开始记录反向调试所必要的信息, 其中包括保存程序每一步运行的结果等等信息. 进行反向调试之前( 启动程序之后 ), 需执行此命令, 否则是无法进行反向调试.

1
2
(gdb) record
(gdb) record btrace

反向运行程序, 直到遇到使程序中断的事件, 比如断点或者已经退回到 record 命令开启时程序执行到的位置.

1
2
(gdb) reverse-continue
(gdb) rc

反向执行一行代码, 并在上一行代码的开头处暂停. 和 step 命令类似, 当反向遇到函数时, 该命令会回退到函数内部(step in), 并在函数最后一行代码的开头处( 通常为 return 0; )暂停执行.

1
(gdb) reverse-step
  • 反向执行一行代码, 并在上一行代码的开头处暂停. 和 reverse-step 命令不同, 该命令不会进入函数内部, 而仅将被调用函数视为一行代码(step out).
    1
    (gdb) reverse-next

当在函数内部进行反向调试时, 下面命令可以回退到调用当前函数的代码处.

1
(gdb) reverse-finish

选择正向还是反向: mode 参数值可以为 forward ( 默认值 )和 reverse.

1
(gdb) set exec-direction mode

更多支持反向调试的命令

后台( 异步 )执行调试命令

2 种执行方式

同步执行: “一个一个”的执行, 即必须等待前一个命令执行完毕, 才能执行下一个调试命令.
后台执行: 又称”异步执行”, 即当某个调试命令开始执行时, (gdb) 命令提示符会立即出现, 我们无需等待前一个命令执行完毕就可以继续执行下一个调试命令.

1
(gdb) command&

command 和 & 之间没有空格.

支持后台执行的调试命令

  • run,attach,step,stepi,next, nexti, continue, finish, until.

日志记录

set logging on 打开日志记录功能.

默认的日志文件是”gdb.txt”. set logging file file_name: 设置日志记录文件.

set logging overwrite on/off: 覆盖之前的 log 与否.

set logging redirect on/off: 日志不会打印在终端与否.

show logging: 显示当前日志设置.

其他

一个 gdb 会话中同时调试多个程序

在调试程序 a 的过程中使用 add-inferior [ -copies n ] [ -exec executable ] 命令加载可执行文件 b. 其中 n 默认为 1.
也可用 clone-inferior [ -copies n ] [ infno ] 克隆现有的 inferior, 其中 n 默认为 1, infno 默认为当前的 inferior.

命令的缩写形式一览

第一个字母

1
2
3
4
5
6
7
8
9
10
11
12
b -> break
c -> continue
d -> delete
f -> frame
i -> info
j -> jump
l -> list
n -> next
p -> print
r -> run
s -> step
u -> until

多个字母

1
2
3
4
5
6
7
8
9
10
11
12
aw -> awatch
bt -> backtrace
dir -> directory
disas -> disassemble
fin -> finish
ig -> ignore
ni -> nexti
rw -> rwatch
si -> stepi
tb -> tbreak
wa -> watch
win -> winheight

如果直接按回车键, 会重复执行上一次的命令.

保存历史命令

在 gdb 中, 默认是不保存历史命令的. 设置成保存历史命令:

1
(gdb) set history save on

默认保存在了当前目录下的 .gdb_history 文件中. 设置要保存的文件名和路径:

1
(gdb) set history filename fname

可以放到 .gdbinit 文件中, 不用每次都重新输入.

进入不带调试信息的函数

默认情况下, gdb 不会进入不带调试信息的函数.

代码例子

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

typedef struct
{
int a;
int b;
int c;
int d;
pthread_mutex_t mutex;
}ex_st;

int main(void) {
ex_st st = {1, 2, 3, 4, PTHREAD_MUTEX_INITIALIZER};
printf("%d,%d,%d,%d\n", st.a, st.b, st.c, st.d);
return 0;
}

由于 printf 函数不带调试信息, 所以 s 命令(step)无法进入 printf 函数. set step-mode on: gdb 不会跳过没有调试信息的函数.

1
2
3
4
5
6
7
(gdb) set step-mode on
(gdb) n
15 printf("%d,%d,%d,%d\n", st.a, st.b, st.c, st.d);
(gdb) s
0x00007ffff7a993b0 in printf () from /lib64/libc.so.6
(gdb) s
0x00007ffff7a993b7 in printf () from /lib64/libc.so.6

信号处理

handle 命令

查看当前处理策略: info handles. 查看 GDB 可以处理的信号种类, 以及各个信号的具体处理方式.

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) info signals
Signal Stop Print Pass to program Description

SIGHUP Yes Yes Yes Hangup
SIGINT Yes Yes No Interrupt
SIGQUIT Yes Yes Yes Quit
SIGILL Yes Yes Yes Illegal instruction
SIGTRAP Yes Yes No Trace/breakpoint trap
SIGABRT Yes Yes Yes Aborted
SIGEMT Yes Yes Yes Emulation trap
SIGFPE Yes Yes Yes Arithmetic exception
SIGKILL Yes Yes Yes Killed
  • Signal: 各个信号的名称.
  • Stop: 当信号发生时, 是否终止程序执行. Yes 表示终止, No 表示当信号发生时程序认可继续执行.
  • Print: 当信号发生时, 是否要求 GDB 打印出一条提示信息. Yes 表示打印, No 表示不打印.
  • Pass: 当信号发生时, 该信号是否对程序可见. Yes 表示程序可以捕捉到该信息, No 表示程序不会捕捉到该信息.
  • Description: 对信号所表示含义的简单描述.
  • 通过修改目标信号 Stop,Print,Pass 列的值, 调试 GDB 调试器对目标信号的处理方式.

指定对信号处理的方式

1
(gdb) handle signal mode
  • signal 参数表示要设定的目标信号, 它通常为某个信号的全名(SIGINT)或者简称(去除 SIG 后的部分, 如 INT);如果要指定所有信号, 可以用 all 表示.
  • mode 参数用于明确 GDB 处理该目标信息的方式.
    • nostop: 当信号发生时, GDB 不会暂停程序, 其可以继续执行, 但会打印出一条提示信息, 告诉我们信号已经发生.
    • stop: 当信号发生时, GDB 会暂停程序执行.
    • noprint: 当信号发生时, GDB 不会打印出任何提示信息.
    • print: 当信号发生时, GDB 会打印出必要的提示信息.
    • nopass(或者 ignore): GDB 捕获目标信号的同时, 不允许程序自行处理该信号.
    • pass(或者 noignore): GDB 调试在捕获目标信号的同时, 也允许程序自动处理该信号.

脚本

脚本的意义

  • 调试过程中很有可能会出现很多重复性的工作. 例如在某个断点处, 我们希望每次都显示所有局部变量的值. 每次运行到断点都去输入 i locals 比较麻烦, 因此脚本的存在很有必要. 根据脚本支持的命令是不是 gdb 内置的命令, 可以将脚本分为内置的 DSL 型脚本, 以及拓展的外部脚本语言, gdb 支持的是 python. 当然也可以通过执行 shell 命令的形式执行其他类型语言的脚本或者程序.

此部分整理源于博客.

编写 GDB DSL 脚本

例子代码: 带 bug 的二分查找实现.

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
#include <iostream>
using std::cout;
using std::endl;

int binary_search(int *ary, unsigned int ceiling, int target)
{
unsigned int floor = 0;
while (ceiling > floor) {
unsigned int pivot = (ceiling + floor) / 2;
if (ary[pivot] < target)
floor = pivot + 1;
else if (ary[pivot] > target)
ceiling = pivot - 1;
else
return pivot;
}
return -1;
}

int main()
{
int a[] = {1, 2, 4, 5, 6};
cout << binary_search(a, 5, 7) << endl; // -1
cout << binary_search(a, 5, 6) << endl; // 4
cout << binary_search(a, 5, 5) << endl; // 期望3, 实际运行结果是-1
return 0;
}
  1. 断点自动触发一系列操作: commands 机制
1
2
3
4
5
b binary_search if target == 5
comm
i locals
i args
end

每次触发断点都会自动执行两个 info 命令.

  1. 将一系列操作归成自定义命令: define 命令
1
2
3
4
5
6
7
8
(gdb) define br_info
Type commands for definition of "br_info".
End with a line saying just "end".
>b $arg0
>comm
>i locals
>i args
>end

使用自定义命令:

1
(gdb) br_info binary_search if target == 5
  1. 将自定义命令写入到脚本文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# gdb_macro
define mv
if $argc == 2
delete $arg0
# 注意新创建的断点编号和被删除断点的编号不同
break $arg1
else
print "输入参数数目不对, help mv以获得用法"
end
end

# (gdb) help mv 会输出以下帮助文档
document mv
Move breakpoint.
Usage: mv old_breakpoint_num new_breakpoint
Example:
(gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search`

end

使用示例:

1
2
3
4
5
6
7
8
9
10
11
(gdb) b binary_search
Breakpoint 1 at 0x40083b: file binary_search.cpp, line 7.
(gdb) source ~/gdb_macro
(gdb) help mv
Move breakpoint.
Usage: mv old_breakpoint_num new_breakpoint
Example:
(gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search`

(gdb) mv 1 binary_search.cpp:18
Breakpoint 2 at 0x4008ab: file binary_search.cpp, line 18.
  1. 自动加载.
    1. 放入 gdb 配置文件 ~/.gdbinit 中, gdb 启动时自己加载. 当 gdb 启动时, 会读取 HOME 目录和当前目录下的的配置文件, 执行里面的命令. 这个文件通常为 .gdbinit. 例子:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      # 打印STL容器中的内容
      python
      import sys
      sys.path.insert(0, "/home/xmj/project/gcc-trunk/libstdc++-v3/python")
      from libstdcxx.v6.printers import register_libstdcxx_printers
      register_libstdcxx_printers (None)
      end

      # 保存历史命令
      set history filename ~/.gdb_history
      set history save on

      # 退出时不显示提示信息
      set confirm off

      # 按照派生类型打印对象
      set print object on

      # 打印数组的索引下标
      set print array-indexes on

      # 每行打印一个结构体成员
      set print pretty on
    1. 通过 -x 参数选项指定加载特定脚本. sudo gdb -q -p $(pidof $your_project) -x ./gdb_marco

编写 GDB Python 脚本

  1. gdb 中可以直接执行 python(稍微高一点的 gdb 版本默认支持 python3).
    1
    2
    python [command]
    py [command]
    例子
    1
    (gdb) python print 23

交互式的, 使用 EOF 字符或者 Ctrl + D 结束交互:

1
2
python-interactive [command]
pi [command]

也可以设置成断点出发的命令:

1
2
3
4
(gdb) python
>print 23
>end
23
  1. 使用 gdb python 库提供的接口编写独立的脚本文件 .py.

例子, 实现上面的 mv 命令:

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
# move.py
# 1. 导入gdb模块来访问gdb提供的python接口
import gdb


# 2. 用户自定义命令需要继承自gdb.Command类
class Move(gdb.Command):

# 3. docstring里面的文本是不是很眼熟?gdb会提取该类的__doc__属性作为对应命令的文档
"""Move breakpoint
Usage: mv old_breakpoint_num new_breakpoint
Example:
(gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search`
"""

def __init__(self):
# 4. 在构造函数中注册该命令的名字
super(self.__class__, self).__init__("mv", gdb.COMMAND_USER)

# 5. 在invoke方法中实现该自定义命令具体的功能
# args表示该命令后面所衔接的参数, 这里通过string_to_argv转换成数组
def invoke(self, args, from_tty):
argv = gdb.string_to_argv(args)
if len(argv) != 2:
raise gdb.GdbError('输入参数数目不对, help mv以获得用法')
# 6. 使用gdb.execute来执行具体的命令
gdb.execute('delete ' + argv[0])
gdb.execute('break ' + argv[1])

# 7. 向gdb会话注册该自定义命令
Move()

使用: (gdb) so ~/move.py.

按何种方式解析脚本文件

gdb 支持的脚本文件分为两种

  1. 只包含 gdb 自身命令的脚本, 例如 .gdbinit 文件.
  2. 其它一些语言写的脚本文件(比如 python).

set script-extension VAL 命令: 决定按何种格式来解析脚本文件.

  • off: 所有的脚本文件都解析成 gdb 的命令脚本.
  • soft: 根据脚本文件扩展名决定如何解析脚本. 如果 gdb 支持解析这种脚本语言(比如python), 就按这种语言解析, 否则就按命令脚本解析.
  • strict: 根据脚本文件扩展名决定如何解析脚本. 如果 gdb 支持解析这种脚本语言(比如python), 就按这种语言解析, 否则不解析.

参考链接:
https://www.gitbook.com/book/wizardforcel/100-gdb-tips

GDB 入门--反汇编, 反向调试, 脚本, 杂项

https://www.chuxin911.com/GDB_note_3_20220731/

作者

cx

发布于

2022-07-31

更新于

2022-07-31

许可协议