GDB 入门--反汇编, 反向调试, 脚本, 杂项
[TOC]
本文介绍 GDB 中的反汇编, 反向调试, 脚本, 杂项相关操作.
汇编命令
设置汇编命令格式
在 Intel x86 处理器上, gdb 默认显示汇编命令格式是 AT&T 格式.
目前 set disassembly-flavor
命令只能用在 Intel x86 处理器上, 并且取值只有 intel
和 att
.
查看反汇编命令: disassemble
(disas
)
- 反汇编一个函数
1
disass func_name
- 反汇编一段内存地址, 第1个参数是起始地址, 第2个是终止地址不带参数的话, 默认的反汇编范围是所选择帧的
1
disass 0×0 0×10
pc
附近的函数.
可以使用info line
命令来映射一个源码行到程序地址, 然后使用命令disassemble
显示一个地址范围的机器命令. 例子如下:
1 | (gdb) info line main |
查看反汇编命令的另一个方法: x
x/3i $pc
: 显示 pc
开始的 3 条命令.
自动反汇编后面要执行的代码
- 在任意情况下反汇编后面要执行的代码
1
(gdb) set disassemble-next-line on
- 在后面的代码没有源码的情况下才反汇编后面要执行的代码
1
(gdb) set disassemble-next-line auto
将源程序和汇编命令映射起来
例子代码
1 |
|
用 disas /m fun
将函数代码和汇编命令映射起来, 显示代码每行对应的汇编命令.
1 | (gdb) disas /m main |
查看某一行对应的汇编命令
行所对应的地址范围
1 | (gdb) i line 13 |
disassemble [Start],[End]
命令反汇编
1 | (gdb) disassemble 0x4004e9, 0x40050c |
显示将要执行的汇编命令
display n/i $pc
命令显示当程序停止时, 将要执行的 n 条汇编命令.
取消显示可以用 undisplay
命令.
显示程序原始机器码
例子代码
1 |
|
disassemble /r
命令可以用 16 进制形式显示程序的原始机器码.
源码操作
edit
命令
作用: 编辑源文件
1 | (gdb) edit location |
默认使用的是 ex 编译器. export EDITOR=/usr/bin/vim
: 修改默认编辑器.
search
命令
作用: 搜索源码, 使用正则表达式. 可以实现正向或者反向搜索.
1 | 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 ).
- 将 command 命令作用于指定编号 id 的线程. command 指 GDB 命令(包括自定义命令), 如
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 | (gdb) record |
反向运行程序, 直到遇到使程序中断的事件, 比如断点或者已经退回到 record
命令开启时程序执行到的位置.
1 | (gdb) reverse-continue |
反向执行一行代码, 并在上一行代码的开头处暂停. 和 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 | b -> break |
多个字母
1 | aw -> awatch |
如果直接按回车键, 会重复执行上一次的命令.
保存历史命令
在 gdb 中, 默认是不保存历史命令的. 设置成保存历史命令:
1 | (gdb) set history save on |
默认保存在了当前目录下的 .gdb_history
文件中. 设置要保存的文件名和路径:
1 | (gdb) set history filename fname |
可以放到 .gdbinit
文件中, 不用每次都重新输入.
进入不带调试信息的函数
默认情况下, gdb 不会进入不带调试信息的函数.
代码例子
1 |
|
由于 printf
函数不带调试信息, 所以 s
命令(step
)无法进入 printf
函数. set step-mode on
: gdb 不会跳过没有调试信息的函数.
1 | (gdb) set step-mode on |
信号处理
handle
命令
查看当前处理策略: info handles
. 查看 GDB 可以处理的信号种类, 以及各个信号的具体处理方式.
1 | (gdb) info signals |
- 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 |
|
- 断点自动触发一系列操作:
commands
机制
1 | b binary_search if target == 5 |
每次触发断点都会自动执行两个 info
命令.
- 将一系列操作归成自定义命令:
define
命令
1 | (gdb) define br_info |
使用自定义命令:
1 | (gdb) br_info binary_search if target == 5 |
- 将自定义命令写入到脚本文件中
1 | # gdb_macro |
使用示例:
1 | (gdb) b binary_search |
- 自动加载.
- 放入 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
- 放入 gdb 配置文件
- 通过
-x
参数选项指定加载特定脚本.sudo gdb -q -p $(pidof $your_project) -x ./gdb_marco
- 通过
编写 GDB Python 脚本
- gdb 中可以直接执行 python(稍微高一点的 gdb 版本默认支持 python3).例子
1
2python [command]
py [command]1
(gdb) python print 23
交互式的, 使用 EOF 字符或者 Ctrl + D 结束交互:
1 | python-interactive [command] |
也可以设置成断点出发的命令:
1 | (gdb) python |
- 使用
gdb
python 库提供的接口编写独立的脚本文件.py
.
例子, 实现上面的 mv
命令:
1 | # move.py |
使用: (gdb) so ~/move.py
.
按何种方式解析脚本文件
gdb 支持的脚本文件分为两种
- 只包含 gdb 自身命令的脚本, 例如
.gdbinit
文件. - 其它一些语言写的脚本文件(比如 python).
set script-extension VAL
命令: 决定按何种格式来解析脚本文件.
off
: 所有的脚本文件都解析成 gdb 的命令脚本.soft
: 根据脚本文件扩展名决定如何解析脚本. 如果 gdb 支持解析这种脚本语言(比如python), 就按这种语言解析, 否则就按命令脚本解析.strict
: 根据脚本文件扩展名决定如何解析脚本. 如果 gdb 支持解析这种脚本语言(比如python), 就按这种语言解析, 否则不解析.
GDB 入门--反汇编, 反向调试, 脚本, 杂项