GDB调试原理初探
背景
在学习二进制时,GDB是必不可少的工具。我经常使用GDB来动态调试一个ELF文件,但是却不能准确、完整地说出GDB调试的原理,所以在这里总结一下。
GDB能attach 到一个正在运行的进程中,以实时获取进程中的内存数据、增加断点、查看当前运行状态下函数变量值、修改寄存器的值。其基本的调试功能是通过系统调用ptrace实现的。GDB有本地调试和远程调试两种模式:
本地调试:GDB与目标程序在同一台电脑中
远程调试:GDB与目标程序不在同一台电脑,此时需要GdbServer,GdbServer与目标程序在同一个设备中,GDB可以通过串口或者RSP协议(GDB Remote Serial Protocol协议)通信
gdb工作流程
直接调试目标程序
当在终端输入命令
gdb ./test
时,此时gdb启动并从头开始调试test,gdb工作流程如下:
操作系统启动gdb进程,gdb进程fork出一个子进程
子进程调用系统函数ptrace(PTRACE_TRACEME , …)
子进程调用execve加载,执行目标程序test
示例如下:
首先使用gdb ./pwn
,建立追踪关系,接着使用pstree -p | grep 'pwn'
,可以看到pwn程序的父进程为gdb。使用attach调试已运行程序
当在终端输入命令
gdb attach -p $pid
时,gdb调试一个已经在运行中的程序,此时gdb的工作流程为:gdb成为test进程的父进程
test进程进入TASK_TRACED,表示当前进程正在被追踪,此时test进程会停下来,等待gdb的命令,在TASK_TRACED状态下的进程只接受ptace指定的PTRACE_DETACH和PTRACE_CONT请求,从而唤醒进程执行操作
发送给test进程的信号会被转发给父进程,除了SIGKILL
父进程收到信号后对子进程进行修改,实现对子进程的调试
示例如下:
- 首先运行目标程序:
./pwn
- 接着使用
ps -a
查看pwn的进程号,如下图所示,为6161 - 使用gdb调试已经运行的pwn程序
gdb attach -p 6161
,(此时可能有报错ptrace: Operation not permitted
,使用root即可解决) - 查看pwn对应的父进程
ps -ef | grep 6161
,可以看到gdb为pwn进程的父进程
ptrace
ptrace是Linux内核提供的一个用于进程追踪的系统调用函数,其接口声明如下,通过ptrace,gdb可以读写目标进程test的指令空间、数据空间、堆栈和寄存器的值,并且gdb进程接管了test进程所有信号,操作系统发送给目标进程的信号都会被gdb截获。
1 | #include <sys/ptrace.h> |
ptrace的参数非常重要,这里参考大佬博客:
request | PTRACE_TRACEME、PTRACE_ATTACH | PTRACE_TRACEME 表示被追踪进程调用,让父进程来追踪自己。通常是gdb调试新进程时使用。 |
---|---|---|
PTRACE_ATTACH 父进程attach到正在运行的子进程上,这种追踪方式会检查权限,普通用户无法追踪root用户下的进程 |
||
PTRACE_PEEKTEXT、PTRACE_PEEKDATA 、PTRACE_PEEKUSER 、PTRACE_GETREGS |
表示读取子进程内存,寄存器等内容 | |
PTRACE_POKETEXT ,PTRACE_POKEDATA ,PTRACE_POKEUSR |
表示修改子进程的内存,寄存器的内容 | |
PTRACE_CONT ,PTRACE_SYSCALL , PTRACE_SINGLESTEP |
PTRACE_CONT 表示重新启动被追踪进程 |
|
PTRACE_SYSCALL 每次进入或者退出系统调用时都会触发一次SIGTRAP (Trace/breakpoint trap),strace的追踪系统调用就是通过该配置进行追踪的,进入时获取参数,退出时获取系统调用返回值 |
||
PTRACE_SINGLESTEP 每执行完一次指令之后会触发一次sigtrap,支持获取当前进程的内存/寄存器状态。gdb的next指令通过该选项实现 |
||
PTRACE_DETACH, PTRACE_KILL | 解除父子进程之间的追踪关系 | |
如果父进程在在子进程前结束,则会自动解除追踪关系。 | ||
pid | ||
表示要跟踪的进程pid | ||
addr | ||
表示要跟踪的进程pid | ||
data | 根据前面设置的requet选项而变化,比如要开始追踪时则设置request= PTRACE_CONT ,同时将data 设置为对应signal 数字(SIGTRAP – 5)。 |
GDB实现原理
GDB断点实现
当使用gdb为程序设置断点时,gdb实现如下:
gdb将断点处指令修改为INT 3,同时保存断点信息以及修改前的指令
当子进程(test)执行到断点时,出发INT 3终端,产生SIGTRAP信号
ptrace将子进程的SIGTRAP信号发送给父进程(gdb),父进程与保存的断点信息对比,通过确认INT 3指令的未知,来确认当前信号是否因为断点产生
若是,将断点处的INT 3指令替换为原指令,将PC指针回退一步,等待用户输入命令,否则继续执行代码
GDB单步调试实现
当使用gdb单步调试一个程序时,gdb实现如下
gdb与test建立追踪关系,此时ptrace系统调用的参数为PTRACE_ATTACH
获取test进程的EIP和ESP的值,其中EIP存放CPU将要执行的下一条指令,ESP中存放当前栈帧的地址
通过ptrace的PTRACE_SINGLESTEP参数不断将EIP和ESP向下移动,每执行一条指令,寄存器指针移动一次,直到指针达到栈尾,结束调试
参考链接
https://www.cnblogs.com/sewain/articles/14131927.html //原来gdb的底层调试原理这么简单
https://blog.csdn.net/mrhesongze/article/details/81980397 //GDB调试二进制和符号表symbol分开的程序
https://blog.csdn.net/Z_Stand/article/details/108395906 //一文带你看透GDB实现原理
https://blog.csdn.net/wohu1104/article/details/124934068 //gdb三种调试方式