汇编工具

GDB 是逆向工程不可或缺的工具,熟练使用 GDB 命令也是下一次作业必备的技能。

学习目标

  • 使用 objdump 进行反汇编

  • 使用 gdb 追踪汇编指令

objdump 反汇编

作为编译过程的一部分,汇编器(assembler)接收汇编代码并将其编码为机器指令形式。反汇编(disassembly)是一个相反的过程,将机器指令转换回便于阅读的汇编指令。objdump 就是这样的一个工具,它可以从目标文件(包含机器指令)中挖掘出各种信息,但更常见的用途是用作反汇编程序。一起来尝试一下吧!

  • 调用 objdump -d 命令可以从目标文件中提取信息,并输出机器指令序列以及等效的汇编指令。从目标文件中提取出的指令称为“死亡列表”(deadlist),这里的“死亡”(dead)形象地描述了这个过程不是针对执行时“活动”(live)的代码进行研究的。使用 make 构建 code.c 程序,然后使用 objdump -d code 获取一份“死亡列表”。

  • 服务器提供了一个自定义命令 countops,可以统计给定目标文件中最常用的汇编指令。尝试执行 countops code,该操作将按操作码统计指令,并报告最多的前 10 个指令。再尝试下 countops /usr/bin/gcc 或其他可执行程序。汇编指令的组合会因程序的不同而有较大差异吗?

提示

可以使用 objdump -d code > myfile.txt 命令,保存输出结果。

汇编中的所有文字值都是十六进制的,这在阅读汇编代码时需要注意。

GDB 汇编调试

下面介绍一些用于在汇编级别进行调试的 gdb 命令。

显示汇编指令

不带参数执行命令 disassemble 将打印当前正在执行的函数的反汇编指令。也可以提供可选参数,例如函数名称或代码地址。

(gdb) disassemble myfn
Dump of assembler code for function myfn:
   0x000000000040051b <+0>: movl   $0x1,-0x20(%rsp)
   0x0000000000400523 <+8>: movl   $0x2,-0x1c(%rsp)
...

最左列中的十六进制数字是该指令在内存中的地址,尖括号中的值是该指令相对于函数开头的偏移量

设置断点

你可以指定地址 b *address 或函数 b *myfn+8 的偏移量,在特定的汇编指令处设置断点。

(gdb) b *0x400570      break at specified address
(gdb) b *myfn+8        break at instruction 8 bytes into myfn()

请注意,后者表示断点位置设置在 main 函数偏移量为 8 的指令处,如下所示:

(gdb) disassemble myfn
Dump of assembler code for function myfn:
   0x000000000040051b <+0>: movl   $0x1,-0x20(%rsp)
=> 0x0000000000400523 <+8>: movl   $0x2,-0x1c(%rsp)

如果你不使用 * 前缀设置函数断点,则 gdb 会将其解释为函数名称,而不是特定的汇编指令的地址,所以要特别注意!

单步指令执行

命令 stepinexti 允许你单步执行汇编指令,类比源代码级的 stepnext 命令,可以缩写为 sini

(gdb) stepi            executes next single instruction
(gdb) nexti            executes next instruction (step over fn calls)

查看寄存器状态

命令 info reg 将打印所有整数寄存器。你可以按名称打印或设置寄存器的值。在 gdb 中,寄存器名称以 $ 为前缀,而不是通常的 %

(gdb) info reg
rax            0x4005c1 4195777
rbx            0x0  0
....
(gdb) p $rax            show current value in %rax register
(gdb) set $rax = 9      change current value in %rax register

文本用户界面

命令 tui(文本用户界面)可以将调试界面分成多个窗口,以便同时查看 C 源代码、汇编指令以及当前寄存器的状态。命令 layout <argument> 也可以启动 tui 模式。参数可以指定为 srcasmregssplitnext

(gdb) layout split

tui 模式非常适合跟踪代码的执行过程,在调试时,可以及时观察代码/寄存器的状态。有时 tui 界面可能会显示乱码,此时可以使用命令 refresh 尝试恢复,或者使用快捷键 Ctrl-L。如果无法恢复,尝试 Ctrl-X A 退出 tui 模式,返回到普通的界面。

检查栈帧

以下是之前实验、作业和课程中介绍过的一些命令,一起复习一下:

  • 使用 backtrace 显示从当前函数到 main 的所有栈帧。使用参数 N 时,backtrace N 仅显示 N 个最内部的帧,而 backtrace -N 仅显示 N 个最外部的帧。

  • updownframe n 命令允许你更改选定的帧。这些命令不会更改程序执行的状态(执行仍保持并停止在最顶层帧中的位置),但它们允许你从另一个栈帧检查当前运行时状态。例如,更改为 main 栈帧允许打印仅在该范围内可见的变量/参数。

  • info frame 命令可以打印有关当前栈帧的一些信息。info argsinfo locals 分别提供有关参数和局部变量的信息。

  • 你可以使用 x 命令操作 $rsp,来查看栈上的数据(请注意,g 表示一个“字”,即 8 个字节,x 表示“十六进制显示”)

(gdb) x/4gx $rsp   // 4 quadwords, in hex, read from top of stack

自动显示并打印寄存器值

info reg 命令显示所有整数寄存器的当前状态。使用类似 p $rax 打印单个寄存器值。在 GDB 中,对寄存器的访问是通过 $name 格式实现的,而不是课程中讲解的 %name

寄存器被视为无类型的 8 字节值,当你要求 GDB 打印它时,它会显示十进制整数或十六进制地址。你可以通过在打印命令中添加 /[format] 或使用 C 类型转换来指定如何解释该值,例如:

(gdb) p $rax
$1 = 4196128
(gdb) p/t $rax
$2 = 10000000000011100100000
(gdb) p (char*)$rax
$3 = 0x400720 "Hello, world!\n"

display 命令可以方便地设置一个表达式,可以在单步执行时重复计算和打印结果。例如,尝试以下命令:

(gdb) display/2gx $rsp     // 2 quadwords, in hex, read from stack top
(gdb) display/3i $rip      // next 3 assembly instructions to execute

键入不带参数的 display 可以列出当前设置的所有要显示的表达式,undisplay X 可以取消显示某个表达式 X。灵活地使用 display 命令可以实现类似 tui 界面类似的效果,并且可以乱码的情况。

设置条件断点

可以将断点设置为仅在代码中的某些条件成立时触发。例如,假设你的代码中有以下循环:

1for (int i = 0; i < count; i++) {
2   ...
3}

如果你想在循环内单步执行代码,那么可能要执行许多次循环条件才能到达要检查的位置。但是,GDB 允许你添加一个可选条件(与 C 语法相同),来指定断点何时停止:

(gdb) break 2 if i == count - 1

格式为 [BREAKPOINT] if [CONDITION]。现在,第 2 行的断点只会在循环的最后一次被命中!你甚至可以在表达式中使用局部变量,如上面的 icount 所示。尝试看看你还可以使用哪些其他有用的条件。

定制断点行为

你可以设置一系列命令,以便 GDB 可以在每次到达特定断点时执行。任何有效的 GDB 命令都是可以的——设置栈帧、启用/禁用其他断点、计算 C 表达式、更改寄存器中的值等等。你甚至可以使用断点命令从调试器中临时修补有错误的代码!

假设第 192 行分配的字节数比需要的少了一个:

192s = malloc(strlen(t));         // oops, supposed to be len + 1

首先,我们在第 192 行设置断点。

(gdb) break 192
Breakpoint 1 at 0x400a8d: file program.c, line 192.

接下来,我们向该断点添加命令,插入正确的分配数量,跳过有问题的代码行并继续执行。我们可以通过 command 命令来实现这一任务,该命令会提示我们给最近添加的断点设置一些执行命令。该命令可以持续输入命令,直到我们输入 end 结束:

(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
print s = malloc(strlen(t)+1)
jump 193
continue
end

每次执行到第 192 行时,断点命令都会介入,自动插入正确的分配数,跳过有问题的语句并继续执行。这是一个非常好用的功能,在后续二进制炸弹作业中将非常有用……