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工作流程如下:

  1. 操作系统启动gdb进程,gdb进程fork出一个子进程

  2. 子进程调用系统函数ptrace(PTRACE_TRACEME , …)

  3. 子进程调用execve加载,执行目标程序test
    示例如下:
    首先使用gdb ./pwn,建立追踪关系,接着使用pstree -p | grep 'pwn',可以看到pwn程序的父进程为gdb。
    gdb_pwn.jpg

    使用attach调试已运行程序

    当在终端输入命令gdb attach -p $pid时,gdb调试一个已经在运行中的程序,此时gdb的工作流程为:

  4. gdb成为test进程的父进程

  5. test进程进入TASK_TRACED,表示当前进程正在被追踪,此时test进程会停下来,等待gdb的命令,在TASK_TRACED状态下的进程只接受ptace指定的PTRACE_DETACH和PTRACE_CONT请求,从而唤醒进程执行操作

  6. 发送给test进程的信号会被转发给父进程,除了SIGKILL

  7. 父进程收到信号后对子进程进行修改,实现对子进程的调试

示例如下:

  • 首先运行目标程序:./pwn
  • 接着使用ps -a查看pwn的进程号,如下图所示,为6161
  • 使用gdb调试已经运行的pwn程序gdb attach -p 6161,(此时可能有报错ptrace: Operation not permitted,使用root即可解决)
  • 查看pwn对应的父进程ps -ef | grep 6161,可以看到gdb为pwn进程的父进程

pwn_pid.jpg
attach_pid.jpg
pid.jpg

ptrace

ptrace是Linux内核提供的一个用于进程追踪的系统调用函数,其接口声明如下,通过ptrace,gdb可以读写目标进程test的指令空间、数据空间、堆栈和寄存器的值,并且gdb进程接管了test进程所有信号,操作系统发送给目标进程的信号都会被gdb截获。

1
2
3
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);

ptrace的参数非常重要,这里参考大佬博客

request PTRACE_TRACEME、PTRACE_ATTACH PTRACE_TRACEME表示被追踪进程调用,让父进程来追踪自己。通常是gdb调试新进程时使用。
PTRACE_ATTACH父进程attach到正在运行的子进程上,这种追踪方式会检查权限,普通用户无法追踪root用户下的进程
PTRACE_PEEKTEXT、PTRACE_PEEKDATAPTRACE_PEEKUSERPTRACE_GETREGS 表示读取子进程内存,寄存器等内容
PTRACE_POKETEXT,PTRACE_POKEDATA,PTRACE_POKEUSR 表示修改子进程的内存,寄存器的内容
PTRACE_CONTPTRACE_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实现如下:

  1. gdb将断点处指令修改为INT 3,同时保存断点信息以及修改前的指令

  2. 当子进程(test)执行到断点时,出发INT 3终端,产生SIGTRAP信号

  3. ptrace将子进程的SIGTRAP信号发送给父进程(gdb),父进程与保存的断点信息对比,通过确认INT 3指令的未知,来确认当前信号是否因为断点产生

  4. 若是,将断点处的INT 3指令替换为原指令,将PC指针回退一步,等待用户输入命令,否则继续执行代码

    GDB单步调试实现

    当使用gdb单步调试一个程序时,gdb实现如下

  5. gdb与test建立追踪关系,此时ptrace系统调用的参数为PTRACE_ATTACH

  6. 获取test进程的EIP和ESP的值,其中EIP存放CPU将要执行的下一条指令,ESP中存放当前栈帧的地址

  7. 通过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三种调试方式