GDB 调试 setarch 启动的程序?3种方法搞定!
2025-04-07 17:57:36
GDB 调试 setarch
启动的程序?搞得定!
遇到的麻烦
想用 GDB 调试一个比较特殊的程序?具体来说,这个程序得通过 setarch
启动,并且加上 --addr-no-randomize
和 --read-implies-exec
参数。目标是在一个基于 Docker 的挑战环境里部署这玩意儿。
命令大概长这样:
setarch "$(uname -m)" --addr-no-randomize --read-implies-exec ./my_program my_args
这命令的作用是给 ./my_program
这个进程设定一个特定的“个性”(personality),主要是关掉地址空间布局随机化(ASLR)和允许数据段可执行。这对于某些特定的开发或安全研究场景(比如复现或分析某些漏洞利用)挺有用的。
问题来了:直接用 GDB 搞不定。
试了第一种法子,用 setarch
来启动 GDB,想着 GDB 启动的子进程(也就是我们要调试的 ./my_program
)能继承这个“个性”:
setarch "$(uname -m)" --addr-no-randomize --read-implies-exec gdb ./my_program -ex "set args my_args" ...
结果发现,子进程该崩溃还是崩溃(比如尝试在数据段执行代码时收到 SIGSEGV 信号),说明 setarch
的效果没作用到目标子进程上。这条路不通。
那换个思路,让 GDB 去启动 setarch
,再由 setarch
去启动目标程序:
gdb setarch -ex "set args x86_64 --addr-no-randomize --read-implies-exec ./my_program my_args" ...
用这种方式启动,之前那个依赖特殊环境的程序(比如在数据段执行代码的 exploit)确实能跑起来了,说明 setarch
的参数生效了!但麻烦的是,GDB 现在调试的是 setarch
这个程序,而不是我们真正关心的 ./my_program
。因为我们没有 setarch
的源码和调试符号(而且假设不能自己编译 setarch
),并且也没法直接让 GDB 关注到 setarch
启动的那个 ./my_program
子进程。在 GDB 里给 ./my_program
的函数下断点?根本停不下来,因为 GDB 根本没在调试它。
假设 GDB 没有啥内建的、专门用来控制进程“个性”的功能,那问题可以更普遍地成:
我们能用 GDB 调试一个有源码、有调试符号的程序(./my_program
),但这个程序必须通过另一个我们无法控制(没有源码)的启动器(比如 setarch
)来间接启动吗?
为啥会这样?
这得从 GDB 和进程如何工作说起。
-
setarch ... gdb ./my_program
为啥不行?
当你运行setarch ... gdb ...
时,setarch
命令会创建一个新进程,设置好它要求的环境(比如关闭 ASLR),然后执行gdb
。现在,GDB 进程本身是运行在setarch
创建的环境里的。但 GDB 在启动要调试的程序(inferior process)时,通常会fork()
一个子进程,然后子进程再execve()
目标程序 (./my_program
)。关键在于,GDB 启动子进程的这个过程,并不会自动或者默认地将 GDB 自身 所处的特殊环境(由setarch
设置的)完全传递给 子进程。子进程的执行环境更多是由 GDB 和操作系统默认行为决定的。结果就是,./my_program
并没有得到setarch
的“照顾”。 -
gdb setarch ... ./my_program
为啥也别扭?
当你运行gdb setarch ...
时,GDB 正常启动,并将setarch
作为它的直接调试目标。GDB 内部执行run
命令时,会fork()
一个子进程,这个子进程execve()
执行了setarch x86_64 --addr-no-randomize --read-implies-exec ./my_program my_args
这条命令。
这个setarch
进程接收到参数后,它会先按照要求设置好进程环境(关闭 ASLR 等),然后,它 自己 再execve()
执行./my_program my_args
。
问题就在这里:GDB 只知道它直接fork
和execve
的那个进程是setarch
。当setarch
进程内部再execve
成./my_program
时,从 GDB 的视角看,它调试的那个进程(PID 没变)虽然“变身”了,但 GDB 最初加载的是setarch
的符号(如果 GDB 能找到的话,这里假设找不到),而不是./my_program
的符号。GDB 的调试上下文、符号表、断点等都是针对setarch
的。所以你给./my_program
里的函数下断点,自然没用。GDB 没“看见”那个最终跑起来的./my_program
的内部结构。
总结一下:我们需要一种方法,让 ./my_program
确实在 setarch
配置好的环境下启动,并且 GDB 还能准确地附加上去,加载正确的符号,让我们能够调试 ./my_program
本身。
怎么办?几种搞定它的法子
好消息是,有好几种办法可以绕过这个限制,让 GDB 成功调试由 setarch
启动的目标程序。
法子一:先跑起来,再 GDB 挂上去 (Attach)
这是最直接的想法:既然 GDB 直接启动不好使,那就让 setarch
先把 ./my_program
启动起来,让它在正确的环境下运行。一旦它跑起来了,它就是一个普通的进程,我们再用 GDB 的附加(attach)功能挂到这个已经运行的进程上。
怎么玩:
-
后台启动目标程序: 在终端里,用
setarch
启动你的程序,并把它放到后台运行(用&
)。$ setarch x86_64 --addr-no-randomize --read-implies-exec ./my_program my_args & [1] 12345 # 这里的 12345 是示例 PID
-
找到目标进程的 PID: 程序启动后,需要知道它的进程 ID (PID)。有几种方法:
- 如果你用
&
启动,很多 shell 会直接打印出后台任务的 Job ID 和 PID(如上例中的12345
)。 - 如果错过了或者不确定,可以用
pidof
或者pgrep
命令来查找:$ pidof my_program 12345 # 或者 $ pgrep -f my_program # -f 参数可以匹配完整命令行,更精确 12345
- 小技巧: 如果你的程序启动后很快就做事然后退出,可能来不及附加。可以在
./my_program
的代码早期(比如main
函数开头)加一个短暂的sleep()
或者一个等待输入的getchar()
,给自己留出附加 GDB 的时间。
- 如果你用
-
用 GDB 附加: 知道了 PID,就可以启动 GDB 并附加到该进程了。
# 方式一:先启动 GDB,再附加 $ gdb (gdb) attach 12345 (gdb) symbol-file ./my_program # 手动加载符号 ... # 方式二:启动 GDB 时直接指定 PID 和程序文件(推荐,能自动加载符号) $ gdb ./my_program -p 12345 # 或者 $ gdb -p 12345 ./my_program
加载符号后,你就可以像平时一样设置断点、查看变量、单步执行了。程序会在你附加后暂停,你可以用
continue
(或c
) 命令让它继续执行。
为啥行: 这个方法绕过了 GDB 启动子进程的复杂性。setarch
确保了 ./my_program
在正确的环境下运行。GDB 只是连接到一个已经存在的、环境配置好的进程上,然后接管它的调试控制。
注意点:
- 权限: 附加到进程通常需要足够的权限。你要么是 root 用户,要么 GDB 和目标进程由同一个用户运行。在某些系统上,可能需要特定的权能(capabilities)或配置(比如
ptrace_scope
sysctl 设置)。在 Docker 容器里,如果以非 root 用户运行,需要确保容器启动时有SYS_PTRACE
能力 (比如docker run --cap-add=SYS_PTRACE ...
)。 - 时机: 如果程序启动非常快就执行到你想调试的关键代码区域,可能在你找到 PID 并附加 GDB 之前,那段代码就已经执行完了。上面提到的加
sleep
或getchar
是应对策略。 - 僵尸进程: 如果目标程序退出了,GDB 会报告进程消失。
法子二:给 GDB 配个 '启动器' (set exec-wrapper
)
GDB 其实提供了一个更优雅的、内建的方式来处理这类“需要用包装器启动程序”的场景,那就是 set exec-wrapper
命令。你可以告诉 GDB:“别直接运行目标程序,用我指定的这个包装命令来启动它。”
怎么玩:
-
正常启动 GDB,指定你的目标程序文件(为了加载符号):
$ gdb ./my_program
-
设置执行包装器: 在 GDB 提示符下,使用
set exec-wrapper
命令,后面跟上你的包装命令,包括setarch
和它的参数。注意,不需要在这里写目标程序名 (./my_program
) 或它的参数 (my_args
)。(gdb) set exec-wrapper setarch x86_64 --addr-no-randomize --read-implies-exec
-
设置目标程序的参数: 如果
./my_program
需要参数,用set args
命令设置。(gdb) set args my_args # 如果有多个参数,像平时一样写: set args arg1 arg2 "arg with spaces"
-
运行: 使用
run
(或r
) 命令启动调试。(gdb) run
GDB 内部会执行类似
setarch x86_64 --addr-no-randomize --read-implies-exec ./my_program my_args
的命令来启动子进程,但 GDB 会正确地跟踪到最终的./my_program
进程,并对其进行调试。
为啥行: exec-wrapper
的设计初衷就是为了处理这类场景。GDB 在 run
的时候,知道需要通过这个 wrapper 来启动。它会执行 wrapper,并把目标程序路径和 set args
设置的参数传递给 wrapper。GDB 的内部机制能确保它正确地“捕获”并调试由 wrapper 最终 execve
的那个目标程序进程,而不是 wrapper 本身。
优点:
- 比手动附加更方便,流程更顺畅。
- 不容易有时序问题(race condition)。
- 是 GDB 标准功能,可移植性好。
进阶使用:
- 可以将
set exec-wrapper
和set args
命令写到 GDB 的初始化文件(比如项目目录下的.gdbinit
或家目录的~/.gdbinit
)里,这样每次启动 GDB 调试该程序时就不用手动敲了。# Example .gdbinit set exec-wrapper setarch x86_64 --addr-no-randomize --read-implies-exec set args my_args b main # 可以再加上其他常用设置或断点
法子三:利用 gdbserver
(远程调试风格)
gdbserver
是一个轻量级的程序,它可以在目标机器上启动你的程序,并监听一个网络端口,等待 GDB 连接过来进行远程调试。即使 GDB 和 gdbserver
在同一台机器上,这种方式也很有用,特别是在容器环境或者权限受限的环境里。
怎么玩:
-
用
setarch
启动gdbserver
,让它再启动你的程序:# 在目标环境(比如 Docker 容器内)运行: # 格式: setarch ... gdbserver <监听地址:端口> <目标程序> [目标程序参数] $ setarch x86_64 --addr-no-randomize --read-implies-exec gdbserver :1234 ./my_program my_args Process ./my_program created; pid = 12346 Listening on port 1234
这会在本地(
:
表示所有网络接口,也可以用localhost:1234
)监听 TCP 端口1234
。gdbserver
会启动./my_program
,并且使其暂停,等待 GDB 连接。 -
启动 GDB 并连接: 在同一台机器或另一台可以访问端口
1234
的机器上启动 GDB。$ gdb ./my_program # 加载符号
-
在 GDB 中连接到
gdbserver
:(gdb) target remote localhost:1234 # 如果 gdbserver 在另一台机器,用它的 IP 地址 Remote debugging using localhost:1234 0x00007f... in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) # 现在可以正常调试了,比如下断点,continue (gdb) b main Breakpoint 1 at 0x...: file my_program.c, line 10. (gdb) c Continuing. ...
为啥行: gdbserver
进程是由 setarch
在正确的环境下启动的。然后 gdbserver
负责启动 ./my_program
子进程,这个子进程自然继承了 gdbserver
(也就是 setarch
设置好)的环境。GDB 通过网络连接上来时,gdbserver
充当了 GDB 和目标进程之间的桥梁。GDB 发送调试命令(如设置断点、继续执行),gdbserver
在目标进程上执行这些操作,并将结果(如寄存器值、内存内容)返回给 GDB。
优点:
- 将程序启动和环境设置(由
setarch
和gdbserver
处理)与 GDB 调试器的运行清晰地分离开。 - 非常适合 Docker 等容器环境,或者当 GDB 无法直接运行在目标环境(比如嵌入式系统)的情况。
- 解决了权限问题,因为启动
gdbserver
的用户只需要有执行setarch
和./my_program
的权限即可,GDB 本身运行在别处。
注意点:
- 确保 GDB 能够访问
gdbserver
监听的 IP 地址和端口。防火墙可能会阻止连接。 - 选择一个未被占用的端口号。
总的来说,想要用 GDB 调试一个必须通过像 setarch
这样的启动器来间接启动的程序,完全是可行的。set exec-wrapper
是 GDB 内建的最方便的方案。如果喜欢手动控制或者脚本化,先启动再附加进程也是个简单粗暴有效的办法。而 gdbserver
则提供了一个更灵活、尤其适合容器和远程场景的强大选项。根据你的具体场景和偏好,选择一个合适的法子开干吧!