[心得] write debugger 初探

楼主: descent (“雄辩是银,沉默是金”)   2022-12-24 20:39:43
对于编译器全端的训练, 剩下 debugger (好啦, linker 严格来说还没完全练过一遍),
debugger 是我觉得最难的, 我有想过目标设定在 dos debug 那种即可 (无 symbol
debugger), 但还是不知道怎么开始, 甚至去看 msdos 的 source code, 里头有 debug,
不过是用组合语言写的, 决定先撤退。
和我之前学习的主题一样, 曾经挑战多次, 但都无功而返。突然这几天又被我想到
debugger 这个主题, 再次找了一些资料, 终于找到有关 linux debugger 开发的资料,
幸运的是还有简体中文翻译 - 开发一个 Linux 调试器,
原文是: Writing a Linux Debugger。
是不是在 linux 我倒是没那么在意, debugger 都好,
但如果是 linux debugger 那更好。
这个门槛就低一点, 站在巨人的肩膀, 使用 linux/ptrace 来写 debugger,
不用和硬件 debug 暂存器搏斗。不过 ptrace 也不是那么好学就是, 我曾经挑战多次,
一样无功而返。
ptrace 和 debugger 一起学习很适合, 之前学习 ptrace 就只想做一件事情,
改变程式的变量值印出来, 这不就是 debugger 做的事情吗!
这篇算是这个教学文的学习指引, 也就是学习这系列文的学习文, 有点绕舌, 如果
Writing a Linux Debugger 有难倒你, 希望我这篇文章能帮助你学习这个主题。
先把程式编译起来, 需要额外 2 个 library, libelfin, linenoise。
descent@debian-vm:minidbg$ ls ext/
libelfin linenoise
需要自己 git clone 出来:
libelfin 需要切到 fbreg branch
origin https://github.com/TartanLlama/libelfin.git (fetch)
origin https://github.com/TartanLlama/libelfin.git (push)
descent@debian-vm:libelfin$ git branch
* fbreg
linenoise
origin https://github.com/antirez/linenoise.git (fetch)
origin https://github.com/antirez/linenoise.git (push)
编译 minidbg
cmake .
make
make VERBOSE=1 # 可以看到编译指令
编译好 minidbg 之后会再需要一个 debug 程式, 写个 hello world 吧! 编译之后,
然后执行 ./minidbg h 会发现:
list 1 error message
Hello worlddescent@debian-vm:minidbg$ ./minidbg h
terminate called after throwing an instance of 'dwarf::format_error'
what(): unknown compilation unit version 5
Aborted
这是因为该程式使用的 libelfin 只支援 DWARFv4, gcc 11 是使用 DWARFv5,
改用 gcc-gdwarf-4 h.c -o h 来编译要除错的程式即可,
这样就会使用 DWARFv4 的版本; 不过 source code 是用 -gdwarf-2 来编译测试程式。
而为了搭配设定中断点, 最后我使用
gcc h.c -no-pie -gdwarf-2 -o h 来编译要除错的程式。
minidbg 使用范例请参考 list 2。
list 2. minidbg
0 minidbg h
1 minidbg> break main
2 Set breakpoint at address 0x1148
3 minidbg> cont
4 hello, 10
5 Got signal Unknown signal -911728400
6 minidbg> cont
作者 Sy Brand 有说明 -no-pie 的影响, 并介绍了 personality()。大概原因是这样,

objdump 看到的 main 位址很有可能不是被加载执行的位址, 如果是这样,
设置的中断点就可能不是预期的位置, 而 execl 不是自己写的,
我们不知道 execl 是不是真的把 main load 到 objdump 看到的位址
(这衍生另外一个问题, execl 把程式 load 到那个位址,
有办法查得到吗?), 所以才有 -no-pie 或是 personality(),
我自己尝试过在 linux 把 elf 执行档 load 起来并执行, 但没有成功,
卡在一些地方, 我的目标是想把 elf 执行档
load 任何位址都可以执行, 但其中有部份我还没克服, 所以止步到某个步骤,
在 linux 要克服蛮多问题, 在 bare-metal 环境我是有成功,
uefi loader 加载 os kernel 就是这么做的。
Sy Brand 有提到可以看 /proc/[pid]/maps 的第一行位址来当做加载位址。
使用 gdb 测试, gdb 不管有没有 -no-pie, 都可以正确设定中断点,
gdb 找得到执行档被真正加载的位址, 我用 strace (strace -o g.txt gdb abcdef)
观察 gdb, 可惜没找出什么方向,
gdb 在执行 run 指令之后才会呼叫 ptrace。
(gdb) b main
Breakpoint 1 at 0x1172: file h.c, line 11.
gdb 设定中断点一样是显示在 0x1172,
但最后却可以正确设定在 0x555555555172 的位址。
//child
personality(ADDR_NO_RANDOMIZE);
execute_debugee(prog);
如果不用 -no-pie 编译, elf 执行档会被加载到某个未知位址,
我想了一个很特别的方式, 找出 elf 被加载到的 main 位址,
list 22 会印出 main addree, 就可以得知被加载
的真正位址, 和 objdump (list 23) 果然不同。
可以看到, 被加载到 0x555555555163,
而不是 0x1163。
流程是这样: 执行到 list 22 L12 会卡在 while(1),
使用 kill -s SIGSTOP 240757 stop list 22 的执行档,
这时候 minidbg 又可以继续执行, list 25 L6 就可以看到
main 开始的 8 byte data。
我本来一开始是没有设计 list 22 L12 while(1), cont 让程式跑完再使用
memory read 指令, 但是只读到 0xffffffffffffffff, 可能为了资安问题,
这个 process 被整个清掉了。所以才用了这么迂回的手法。
list 22 h.c
1 #include <stdio.h>
2 int abc123 = 5;
3
4 void func123()
5 {
6 int mn789 = 8;
7 printf("hello c, abc123: %d, mn789: %d\n", abc123, mn789);
8 }
9 int main(int argc, char *argv[])
10 {
11 printf("main: %p\n", main);
12 while(1);
13 func123();
14 return abc123;
15 }
list 23 objdump -d h
1 0000000000001163 <main>:
2 int main(int argc, char *argv[])
3 {
4 1163: 55 push %rbp
5 1164: 48 89 e5 mov %rsp,%rbp
6 1167: 48 83 ec 10 sub $0x10,%rsp
list 25 md
1 descent@debian64:minidbg$ ./minidbg h
2 h child pid: 240757
3 minidbg> c
4 regs.rip: 7ffff7fd5090
5 main: 0x555555555163
6 minidbg> m r 0x555555555163
7 0x55 0x48 0x89 0xe5 0x48 0x83 0xec 0x10
8 minidbg>
分析 elf 有个 readelf 工具, 类似地, 观察 dwarf 也有一个 dwarfdump 工具,
readelf 算熟悉, dwarfdump 就很陌生了, 我今天 (20220630)
才第一次安装这个工具, 和 elf 不同的是, dwarf 的资讯是压缩过的,
所以无法用 hexdump 来直接观察 dwarf, 还是借助
dwarfdump 会比较容易。对于 elf 我会使用 readelf,
hexdump 来交叉比对, dwarf 看起来就难缠多了。
fig 3 的图应该是我看过最具象化 elf 的图了。初步比较, 我觉得 dwarf 比较复杂。
[elf101-64_pages-to-jpg-0001]
fig 3. from https://github.com/corkami/pics/raw/master/binary/elf101/
elf101-64.pdf
dwarf 比我想的难很多, 之前的基础派不太上用场, 大概是重新学习的难度。
在快速扫过 10 篇文章之后, 我慢慢有了怎么开始学习的计画:
在初步阶段, 需要用 objdump 来观察到设定的位址, 然后使用 break
指令来设定中断点, 先不要把 dwarf 搞进来, 光使用 ptrace 就够复杂了。
对于 dwarf 安排了另外的学习方式, 我已经很擅长拆开整个复杂的东西来学习,
毕竟主要还是要学习 debugger, 没有 dwarf 还是可以作到的, 只要会用 ptrace 即可。
这部份就是 Writing a Linux Debugger Part 3: Registers and memory 之前的部份,
之后就会开始讲到 dwarf, 会变得很复杂, 先放一边。
测试了“设定中断点”和“继续执行”和“读暂存器”以及“读写内存位址”
这些指令。和 list 2 不同的是, 我增加了缩写, break 打 b 就可以,
cont 打 c 就可以, 类似 gdb 的指令。
测试时需要 list 3 的资讯, 才能做这个验证, main 位址在 0x401122,
所以 list 5 L3 就是把中断点设定在 main。另外我把 h 的 pid 印出来,
ps 可以看到 h 的状态是 t+, 处于停止或是被追踪的状态。
再来打 c 让 h 停在 main, 程式起来是停在 main, 要怎么验证?
我想到的方式是看 cs:rip, 所以来看一下 rip, list 5 L8 的指令,
显示的是 0x401123, 差了 1, 不知道是怎么回事?
可能是这样, main 现在被换成 0xcc (int 3), 所以 rip 指向 0x401123
是下一个要执行的位址。如果我说错, 请打我脸 (不是真的打我脸)。
好, 看起来真的停在中断点上, 再来看内存的内容, 要看哪里, 看 main 好了,
list 5 L6 的指令下了之后可以看到 20ec8348e58948cc,
有没发现和 list 3 L5 ~ L6, L7 很像,
除了 0x55 变成 0xcc, 也说明之前下的中断点真的把 0x55 改成 0xcc 了。
list 3. objdump -Sd h
1 0000000000401122 <main>:
2 #include <stdio.h>
3 int main(int argc, char *argv[])
4 {
5 401122: 55 push %rbp
6 401123: 48 89 e5 mov %rsp,%rbp
7 401126: 48 83 ec 20 sub $0x20,%rsp
8 40112a: 89 7d ec mov %edi,-0x14(%rbp)
9 40112d: 48 89 75 e0 mov %rsi,-0x20(%rbp)
10 int a;
11 a = 5;
12 401131: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp)
13 printf("hello c\n");
14 401138: 48 8d 3d c5 0e 00 00 lea 0xec5(%rip),%rdi #
402004 <_IO_stdin_used+0x4>
15 40113f: e8 ec fe ff ff callq 401030 <puts@plt>
16 return a;
17 401144: 8b 45 fc mov -0x4(%rbp),%eax
18 }
19 401147: c9 leaveq
20 401148: c3 retq
21 401149: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
list 5 minidbg 测试过程
1 descent@debian64:minidbg$ ./minidbg h
2 h child pid: 231719
3 minidbg> b 0x401122
4 Set breakpoint at address 0x401122
5 minidbg> c
6 minidbg> m r 0x401122
7 20ec8348e58948cc
8 minidbg> r r rip
9 401123
有了使用上的理解, 之后看相关的程式码, 就可以知道这是怎么办到的,
揭开除错器神秘的面纱。Writing a Linux Debugger
系列文一次解除我 debugger/ptrace 2 个疑惑。
最后在我写下这篇文章时, 我对 dwarfdump 的内容以达“略懂”,
没有一开始觉得那么难了, 也觉得 dwarf 很了不起,
为了 debug, 纪录了相当多的资讯。
ref:
‧ LCTT 是“Linux 中国”(https://linux.cn/)的翻译组,负责从国外优秀媒体翻

Linux 相关的技术、资讯、杂文等内容。
‧ DWARF, 调试信息存储格式
‧ dwarf2调试信息格式——chapter1,2
‧ Dwarf2 Exception Handler HOWTO
‧ Writing a Debugger
‧ 中文试译:How debuggers work: Part 3 – Debugging information
blog 原文:
https://descent-incoming.blogspot.com/2022/07/debugger.html
作者: hella (hellc)   2022-12-24 21:16:00
作者: NCKUchemRx (天才梦)   2022-12-25 07:08:00
看了1/2 先推个
作者: enthos (影斯作业系统)   2022-12-26 01:08:00
我是只到手刻elf header(FORTH),可以执行但格式有错.
作者: SmallHanley (小亨利)   2022-12-26 11:53:00
作者: lc85301 (pomelocandy)   2022-12-27 12:08:00
作者: eopXD (eopXD)   2022-12-27 14:18:00
推推推
作者: IhateOGC (我讨厌)   2021-01-01 00:15:00
太神了
作者: handsome616 (豆花伯爵)   2021-01-01 18:05:00
谢谢

Links booklink

Contact Us: admin [ a t ] ucptt.com