我们直接来到最核心的部分,现在被 attach 的进程 (我喜欢称之为 debuggee,但 gdb 用的是 inferior) 已经被 ptrace 暂停在了断点,rip 指向断点的位置,我们执行 call printf("233\n")。以下只关注 x64。
第一个核心细节,gdb call 并不调用 call 指令。如果熟悉 bpf fentry 的实现就会知道内核是用 call trampoline 来做了一层“装饰器”,但 gdb call 并非如此,而是直接修改 rip 指向 &printf,让 cpu 硬跳转过去。https://github.com/MIPS/binutils-gdb/blob/37cb3cd8f896b8d0aa95ced818c6c7b1fb9ddc99/gdb/infcall.c#L1478
we don't execute a call instruction to call the function
第二个核心细节,构造 LR(return address)。rip 直接指向 &printf 确实可以跳过去,但是 printf 运行结束后的 ret 弹出 LR 到 rip 决定了 call 结束后的执行流,gdb 构造了一个特别的 LR 压到栈上,让 printf 结束后跳到↓。这个地址在 gdb 里叫做 bp_addr,根据系统架构 gdb 会自动选择 ON_STACK / AT_ENTRY_POINT。
第三个核心细节,LR 指向用户栈某地址,where 写着 int3 指令。这真的很特别,特别在它有两层,第一层你以为它构造了一个 int3 让 call printf 结束后跳 int3,进程进入 trap stop 被 gdb 接管做一些收尾工作,然而这是不对的;真实情况是在第二层,int3 写在用户栈上,但用户栈内存没有 executable permission,不能执行写在这里的指令,所以 pop 这个栈地址到 rip 之后内核会抛出 SIGSEGV,然后 gdb 捕捉了这个预期的信号,把它当做 SIGTRAP 处理。这一步真的非常邪门,一开始 codex 读代码也读错,gdb 的注释也不太匹配,我亲自做了很多实验和检查,和 codex 吵了很多轮,确定了这确实是 x64 gdb 的逻辑: https://github.com/MIPS/binutils-gdb/blob/37cb3cd8f896b8d0aa95ced818c6c7b1fb9ddc99/gdb/infrun.c#L6327
[...] We do
something similar for SIGSEGV, since a SIGSEGV will be generated
when we're trying to execute a breakpoint instruction on a
non-executable stack. This happens for call dummy breakpoints
for architectures like SPARC that place call dummies on the
stack.
第四个核心细节,call 使用的栈直接跳过 x64 红灯区。精确来说,断点时刻 rsp 指向的是 debuggee 栈,但 call 用的是 rsp-128,这 128 字节在 gdb 里叫做 x64 red zone。上面说的栈上 int3 也是写在这里(bp_addr),rsp-128-1(int3 指令只有一个字节);然后 rsp-128-16 作为 call 开始使用的栈,先把参数压栈,然后是执行栈。
第五次核心细节,堆上内存优先用 malloc 分配。我确实也很震惊,因为并非所有 debuggee 都有 malloc,怎么 gdb 会 call libc 函数。比如 call printf("233\n") 就会用堆上内存,把 "233\n" 写在堆上,然后 rdi 指向这个堆地址作为参数传递。我本来以为 gdb 会用 ptrace 注入 mmap syscall 给自己分配一小块内存,没想到竟然是 malloc。使用 gdb 命令 set debug infcall on 可以看到这是真的:
(gdb) set debug infcall on
(gdb) call printf("233\n")
[infcall] call_function_by_hand_dummy: enter
[infcall] call_function_by_hand_dummy: calling __printf
[infcall] call_function_by_hand_dummy: enter
[infcall] call_function_by_hand_dummy: calling __GI___libc_malloc让 gpt 花了一张示意图,费劲。
其实 gdb resume from breakpoint 也很魔法,下一期再见