来啊,一起学习二进制呀
1 启动调试器
在终端中,使用"文本用户界面"运行gdb
> make puzzlebox gcc -Wall -g -c puzzlebox.c gcc -Wall -g -o puzzlebox puzzlebox.o#注意编译时使用-g选项,这会为调试器添加调试符号:非常有用#使用文本用户界面启动gdb调试程序puzzlebox> gdb -tui ./puzzlebox
1.1 TUI模式(推荐)
可以通过使用-tui
选项运行gdb
来启用_文本用户界面_(TUI)。它显示:
-
命令和历史记录在底部 -
源代码位置在顶部
屏幕偶尔会显示"混乱",可以通过按Control-L
来纠正,这将强制重绘终端屏幕。
图1:gdb -tui ./puzzlebox
,文本用户界面模式
1.2 普通模式
这与gdb
的普通模式不同,普通模式不会显示任何源代码,除非发出list
命令。大多数人发现这种模式更难使用,除非他们将其作为另一个编辑器(如emacs
)的子进程运行。
图2:gdb ./puzzlebox
,普通"安静"模式。只有在输入list
命令后才会显示源代码。
1.3 切换模式
如果处于普通模式,可以通过以下命令进入TUI模式:
tui enable
切换到普通模式可通过以下命令完成:
tui disable
2 puzzlebox
的典型启动
下面在gdb
中的示例运行说明了在调试器中运行puzzlebox
的基础知识。有关next / run / break
等命令的详细信息,请参见下一节。
> gdb -tui ./puzzlebox # 使用文本用户界面在puzzlebox上启动gdbGNU gdb (GDB) 8.1...Reading symbols from ./puzzlebox...done.(gdb) set args input.txt # 设置命令行参数为input.txt(gdb) run # 运行程序Starting program: puzzlebox input.txtUserID 'kauf0095' accepted: hash value = 1397510491 # 程序输出PHASE 1: A puzzle you say? Challenge accepted!Ah ah ah, you didnt say the magic word...Failure: Double debugger burger, order up! * Score: 0 / 50 pts *[Inferior 1 (process 27727) exited normally] # gdb指示程序已结束(gdb) break phase01 # 在函数phase01()处设置断点Breakpoint 1 at 0x55555555551a: file puzzlebox.c, line 220.(gdb) runStarting program: puzzlebox input.txtUserID 'kauf0095' accepted: hash value = 1397510491PHASE 1: A puzzle you say? Challenge accepted!Breakpoint 1, phase01 () at puzzlebox.c:220 # 命中断点220 int a = atoi(next_input()); # 在此代码行停止(gdb) step # 前进一行代码next_input () at puzzlebox.c:197 # 步入函数next_input()197 input_idx++;(gdb) step # 再次步进198 int ret = fscanf(input_fh, "%s", inputs[input_idx]);(gdb) finish # 乏味:完成此函数Run till exit from #0 next_input () at puzzlebox.c:1980x0000555555555524 in phase01 () at puzzlebox.c:220220 int a = atoi(next_input()); # 回到phase01()Value returned is $1 = 0x555555758380 <inputs+128> "1"(gdb) step # 前进221 int b = atoi(next_input());(gdb) next # 越过next_input()调用222 int c = atoi(next_input());(gdb) next # 再次越过:此阶段需要3个输入224 a += hash % 40; # 根据随机哈希值对a进行某些操作...(gdb) print a # 显示a发生了什么$2 = 12 # 现在值为12(gdb) n # 'next'的缩写225 if (a<b && b>c && a<c) { # 对a、b、c的一些检查,它们是输入(gdb) next228 failure("Double debugger burger, order up!"); # 即将失败,一定需要不同的输入(gdb) kill# 终止程序Kill the program being debugged? (y or n) y# 现在编辑input.txt以获取不同的值(gdb) run # 重新运行程序Starting program: puzzlebox input.txtUserID 'kauf0095' accepted: hash value = 1397510491PHASE 1: A puzzle you say? Challenge accepted!Breakpoint 1, phase01 () at puzzlebox.c:220 # 再次命中断点220 int a = atoi(next_input());(gdb) info break# 显示有关哪些断点处于活动状态的信息Num Type Disp Enb Address What1 breakpoint keep y 0x000055555555551a in phase01 at puzzlebox.c:220 breakpoint already hit 1 time(gdb) break puzzlebox.c:225 # 不想继续单步执行:在前面的几行设置断点Breakpoint 2 at 0x555555555583: file puzzlebox.c, line 225.(gdb) info break# 显示活动断点的信息Num Type Disp Enb Address What1 breakpoint keep y 0x000055555555551a in phase01 at puzzlebox.c:220 breakpoint already hit 1 time2 breakpoint keep y 0x0000555555555583 in phase01 at puzzlebox.c:225(gdb) continue# 继续执行直到下一个断点Continuing.Breakpoint 2, phase01 () at puzzlebox.c:225 # 命中第二个断点225 if (a<b && b>c && a<c) {(gdb) print a # a的值$3 = 21(gdb) next # 看看这是否有效228 failure("Double debugger burger, order up!"); # 不行(gdb) print b # 查看b$4 = 2 # 可能需要增大它以满足a<b
3 标准gdb
命令
与大多数调试器一样,gdb
提供了多种控制程序执行和检查值的方法。以下是总结,但gdb
自身广泛的help
提供了更多详细信息。
3.1 在gdb
中设置断点
断点
表示调试器将在程序中停止执行的位置。可以通过多种方式创建断点,并在不再需要时删除它们。
|
|
|
---|---|---|
break main |
main() 的开头停止运行 |
|
break phase01 |
|
|
break puzzlebox.c:100 |
puzzlebox.c 的第100行停止运行 |
|
break |
|
|
info breakpoint |
|
|
disable 2 |
|
|
enable 2 |
|
|
clear phase01 |
phase01() 的断点 |
|
clear puzzlebox.c:100 |
puzzlebox.c 第100行的断点 |
|
delete 2 |
|
|
break *0x1248f2 |
|
|
break *func+24 |
|
|
break *func+0x18 |
|
3.2 参数和运行
加载程序并设置一两个断点后,通常需要运行它。命令行参数通常也是必要的。
|
|
|
---|---|---|
set args hi bye 2 |
hi bye 2 |
puzzlebox
input.txt |
show args |
|
kill/run 才能生效 |
run |
|
|
kill |
|
run 程序 |
file program |
program 并开始调试 |
puzzlebox 不必要,但对正确调试有用 |
quit |
|
3.3 单步执行
当命中断点时,可以在调试器中单步向前,以跟踪执行路径。使用step
逐行进入函数,使用next
留在当前函数中越过函数调用。
|
|
|
---|---|---|
step |
|
|
step 4 |
|
step
|
next
next 4 |
|
next
|
stepi |
|
stepi
|
nexti |
|
nexti
|
finish |
|
|
continue
cont |
|
3.4 打印内存和栈中的值
检查值通常是必要的,以了解程序中发生的情况。调试器可以以各种格式显示数据,包括不符合给定变量C类型的格式。
还包括打印由寄存器(如栈指针寄存器rsp
)指向的内存位置的命令。这对于检查可能被推入栈中并被操作的值很有用。
|
|
|
---|---|---|
print a |
a 的值,必须在当前函数中 |
a |
print/x a |
a 的值打印为十六进制数 |
|
print/o a |
a 的值打印为八进制数 |
|
print/t a |
a 的值打印为二进制数(显示所有位) |
|
print/s a |
a 的值打印为字符串,即使它不是字符串 |
|
print arr[2] |
arr[2] 的值 |
|
print 0x4A25 |
0x4A25 的十进制值,即18981 |
|
x a |
a 指向的内存 |
a 是一个指针 |
x/d a |
a 指向的内存打印为十进制整数 |
|
x/s a |
a 指向的内存打印为字符串 |
|
x/s (a+4) |
a+4 指向的内存打印为字符串 |
|
x $rax |
rax 指向的内存 |
|
x $rax+8 |
rax 指向的位置上方8字节的内存 |
|
x /wx $rax |
|
|
x /gx $rax |
|
|
x /5gd $rax |
rax 指向的位置开始打印5个64位数;使用十进制格式 |
|
x /3wd $rsp |
rsp 开始)的3个32位数 |
3.5 TUI模式下的命令历史和屏幕管理
在gdb中,基本的编辑规则很有用。文本用户界面-tui
模式会改变按键,使箭头键无法正常工作。但是,TUI模式在终端的上部区域显示源代码位置,大多数人认为这很有帮助。
|
-tui 模式 |
|
---|---|---|
Ctrl-l
|
|
|
Ctrl-p
|
|
|
Ctrl-n
|
|
|
Ctrl-r
|
|
|
Ctrl-b
|
|
|
Ctrl-f
|
|
|
|
|
|
|
|
|
list |
|
|
3.6 其他资源
4 在GDB中调试汇编
4.1 概述
当包含汇编代码或只有二进制可执行文件时,GDB可以毫无困难地处理汇编代码。以下命令对此很有用。
|
-tui 模式 |
|
---|---|---|
Ctrl-l
|
|
|
layout asm |
|
|
layout reg |
|
|
winheight regs +2 |
|
|
winheight regs -1 |
|
|
info reg |
|
|
list |
|
|
disassemble
disas |
|
|
在TUI模式下使用命令layout asm
和layout reg
,可以获得一个用于调试汇编的人体工程学布局,如下所示。
图3:在一些汇编代码上运行的gdb
。顶部框架显示寄存器内容,中间显示汇编,底部显示命令。
4.2 编译和运行汇编文件(源代码可用)
使用调试标志编译汇编文件将使gdb在TUI源窗口中默认显示汇编代码,并使step
命令一次移动一条指令。
> gcc -g collatz.s # 使用调试符号编译汇编代码> gdb ./a.out # 在可执行文件上运行gdbgdb> tui enable# 启用文本用户界面模式gdb> break main # 在main处设置断点gdb> run # 运行到断点gdb> step # 步进几次以查看汇编中的位置gdb> s # step的简写gdb> layout regs # 开始显示寄存器gdb> step # 步进以查看寄存器中的变化gdb> break collatz.s:15 # 在另一个源行设置断点gdb> continue# 运行到源行gdb> step # 步进gdb> winheight regs +2 # 显示更多寄存器窗口以查看eflagsgdb> quit # 退出调试器
4.3 调试二进制文件(无源代码可用)
没有调试符号,gdb
不知道要显示什么源代码。由于二进制文件对应于汇编,可以始终使用layout asm
在TUI中让调试器显示汇编代码。此外,应使用stepi
命令一次执行一条汇编指令,因为step
可能会尝试确定要执行的C级操作,这可能是几条汇编指令。
> gcc collatz.s # 不带调试符号编译汇编代码> gdb ./a.out # 在可执行文件上运行gdbgdb> tui enable# 启用文本用户界面模式gdb> layout asm # 在源窗口中显示汇编代码gdb> break main # 在main处设置断点gdb> run # 运行到断点gdb> stepi # 步进单个汇编指令gdb> si # stepi的简写gdb> layout regs # 开始显示寄存器gdb> stepi # 步进以查看寄存器中的变化gdb> break *main + 14 # 在main后14字节的指令处设置断点gdb> continue# 运行到源行gdb> stepi # 步进gdb> winheight regs +2 # 显示更多寄存器窗口以查看eflagsgdb> quit # 退出调试器
4.4 其他TUI命令
gdb
TUI命令和配置的官方列表在这里:https://sourceware.org/gdb/current/onlinedocs/gdb/TUI.html
文档中包括:
-
调整窗口高度 -
显示其他窗口 -
循环浏览窗口,使用上/下箭头滚动 -
单键模式,按 i
或s
向前步进一条指令,让人感觉非常酷
5 二进制文件中的断点
使用C和汇编源文件设置断点可以通过文件/行号轻松完成。但是,如果没有可用的源代码,就变得有点棘手,因为必须基于其他标准设置断点。以下是一些技巧。
5.1 在函数符号/标签处断点
在汇编级别,高级语言中的函数名仍然作为"符号"存在。可以使用二进制工具如objdump -t a.out
或nm a.out
查看这些符号。标记为T
的任何内容都是"程序文本"。在这些位置设置断点很常见。
> nm bomb # 显示二进制bomb中的符号... # 用"T"标记的函数0000000000400df6 T main # 有一个main函数...0000000000400f2d T phase_1 # 有phase函数0000000000400f49 T phase_2...> gdb ./bomb # 在调试器中运行二进制...Reading symbols from bomb...done.(gdb) break main # 在到达符号"main"时设置断点Breakpoint 1 at 0x400df6: file bomb.c, line 37.(gdb) break phase_1 # 在到达符号"phase_1"时设置断点Breakpoint 2 at 0x400f2d # 显示phase_1开始的指令内存地址(gdb) run # 运行Starting program: bombBreakpoint 1, main (argc=1, argv=0x7fffffffe6a8) at bomb.c:37 # main中的第一个断点37 {(gdb) cont # 继续Continuing....Breakpoint 2, 0x0000000000400f2d in phase_1 () # phase_1中的第二个断点,注意内存地址(gdb)
5.2 在特定指令地址处断点
由于二进制文件没有行号,因此无法在特定行号设置断点。但是,二进制文件中的所有指令都有一个指令地址。可以在特定指令地址设置断点,这样当指令指针寄存器(rip
)到达这些地址时,调试器将停止执行。这种语法有点奇怪:
# 注意每种情况下使用*(gdb) break *0x1248f2 # 在地址0x1248f2的指令处设置断点(gdb) break *func+24 # 在标签func + 24字节处设置断点(gdb) break *func+0x18 # 在标签func + 24字节处设置断点(0x18 == 24)
下面是一个示例,展示了在指令地址设置断点的典型用途。请注意,disas
命令用于在gdb
中显示当前函数中的反汇编代码,它方便地提供了从最近标签到即将到来的指令地址的一些偏移量。在TUI模式下,这些信息可能已经存在,无需使用disas
。
Breakpoint 2, 0x0000000000400f2d in phase_1 ()(gdb) disas # 显示汇编代码和当前位置Dump of assembler code forfunction phase_1:=> 0x0000000000400f2d <+0>: sub $0x8,%rsp 0x0000000000400f31 <+4>: mov $0x402710,%esi 0x0000000000400f36 <+9>: callq 0x401439 <strings_not_equal> 0x0000000000400f3b <+14>: test %eax,%eax 0x0000000000400f3d <+16>: je 0x400f44 <phase_1+23> 0x0000000000400f3f <+18>: callq 0x401762 <explode_bomb> 0x0000000000400f44 <+23>: add $0x8,%rsp 0x0000000000400f48 <+27>: retqEnd of assembler dump.(gdb) break *0x400f36 # 在特定指令地址设置断点Breakpoint 3 at 0x400f36(gdb) cont # 继续执行Continuing.Breakpoint 3, 0x0000000000400f36 in phase_1 () # 注意停止地址与断点匹配(gdb) disas # 显示汇编代码和当前位置Dump of assembler code forfunction phase_1: 0x0000000000400f2d <+0>: sub $0x8,%rsp 0x0000000000400f31 <+4>: mov $0x402710,%esi=> 0x0000000000400f36 <+9>: callq 0x401439 <strings_not_equal> 0x0000000000400f3b <+14>: test %eax,%eax 0x0000000000400f3d <+16>: je 0x400f44 <phase_1+23> 0x0000000000400f3f <+18>: callq 0x401762 <explode_bomb> 0x0000000000400f44 <+23>: add $0x8,%rsp 0x0000000000400f48 <+27>: retqEnd of assembler dump.(gdb) break *phase_1+18 # 在标签偏移量处设置断点Breakpoint 4 at 0x400f3f # 注意地址(gdb) cont # 继续执行Continuing.Breakpoint 4, 0x0000000000400f3f in phase_1 () # 注意地址(gdb) disas # 显示汇编代码和当前位置Dump of assembler code forfunction phase_1: 0x0000000000400f2d <+0>: sub $0x8,%rsp 0x0000000000400f31 <+4>: mov $0x402710,%esi 0x0000000000400f36 <+9>: callq 0x401439 <strings_not_equal> 0x0000000000400f3b <+14>: test %eax,%eax 0x0000000000400f3d <+16>: je 0x400f44 <phase_1+23>=> 0x0000000000400f3f <+18>: callq 0x401762 <explode_bomb> 0x0000000000400f44 <+23>: add $0x8,%rsp 0x0000000000400f48 <+27>: retqEnd of assembler dump.
7 处理二进制文件的其他有用工具
以下程序在大多数Unix系统上可用,在处理二进制文件时可能有用。除了GDB,它们还可以提供一些关于要查找的内容的信息。
|
|
---|---|
objdump -d file.o |
|
objdump -t file.o |
|
nm file.o |
objdump -t 但省略了符号的一些细节 |
strings file.o |
|
原文始发于微信公众号(独眼情报):gdb 调试器快速指南
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论