- ptrace 系统调用
- 事件循环(Event Loop)
关于 gdb 内部实现介绍的文章非常少,本人计划通过阅读 gdb 源码,推出系列文章介绍 gdb 内部实现的机制,以窥视 gdb 内部是如何控制和调试程序的。
GDB 通过 ptrace 系统调用技术控制和调试目标程序,并通过事件循环(Event Loop)机制循环处理来自用户和目标程序的事件。
ptrance 是 linux 系统提供调试进程的系统调用,ptrace 接口提供了丰富的参数让我们能够控制和调试目标进程。ptrace 接口如下:
#includelong ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
- request:指定调试的指令,指令的类型很多,如:PTRACE_TRACEME、PTRACE_PEEKUSER、PTRACE_CONT、PTRACE_GETREGS等。
- pid:进程的ID。
- addr:进程的某个地址空间,可以通过这个参数对进程的某个地址进行读或写操作。
- data:根据不同的指令,有不同的用途,下面会介绍。
这里重点介绍几个 request:
- PTRACE_TRACEME:本进程被其父进程所跟踪。
- PTRACE_ATTACH:跟踪指定 pid 进程。pid 表示被跟踪进程。
- PTRACE_GETREGS:读取寄存器值,pid 表示被跟踪的子进程,data 为用户变量地址用于返回读到的数据。
- PTRACE_SETREGS:设置寄存器值,pid 表示被跟踪的子进程,data 为用户数据地址。
- PTRACE_SINGLESTEP:设置单步执行标志,单步执行一条指令。pid表示被跟踪的子进程。signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。当被跟踪进程单步执行完一个指令后,被跟踪进程被中止,并通知父进程。
- PTRACE_CONT:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。
- …
request 更多定义见 linux-2.4.16/include/linux/ptrace.h 文件:
#define PTRACE_TRACEME 0 #define PTRACE_PEEKTEXT 1 #define PTRACE_PEEKDATA 2 #define PTRACE_PEEKUSR 3 #define PTRACE_POKETEXT 4 #define PTRACE_POKEDATA 5 #define PTRACE_POKEUSR 6 #define PTRACE_CONT 7 #define PTRACE_KILL 8 #define PTRACE_SINGLESTEP 9 #define PTRACE_ATTACH 0x10 #define PTRACE_DETACH 0x11 #define PTRACE_SYSCALL 24 #define PTRACE_GETREGS 12 #define PTRACE_SETREGS 13 #define PTRACE_GETFPREGS 14 #define PTRACE_SETFPREGS 15 #define PTRACE_GETFPXREGS 18 #define PTRACE_SETFPXREGS 19 #define PTRACE_SETOPTIONS 21
一个典型调试过程简化如下:
- 父进程(gdb进程)调用 fork() 创建一个子进程。
- 子进程通过 ptrace(PTRACE_TRACEME, …) 接口将自己设置为被追踪模式,并通过 execl() 运行被调试程序。
- 子进程执行 execl()运行目标程序时,会给子进程发送 SIGTRAP 信号,让子进程暂停,并向父进程发送 SIGCHLD 信号。
- 父进程通过 wait() 接收到子进程发送的 SIGCHLD 信号后,可以通过 ptrace(PTRACE_GETREGS, …) 接口获取子进程相关信息,比如寄存器值。并可以通过 ptrace(PTRACE_CONT, …) 让子进程继续运行。
以上就是 gdb 使用 ptrace 系统调用控制和调试目标程序的基本原理。接下来将介绍 gdb 如何通过事件循环机制具体控制和调试目标的。
事件循环(Event Loop)介绍 gdb 事件循环机制前,需要先介绍下该机制实现的关键技术: poll / select 接口。poll 和 select 实现机制类似,这里只介绍下 poll 接口(详细说明直接查看手册:man poll/select)。
在 linux 系统中,一切 IO 设备都抽象成文件(一切皆是文件, Every thing is file!)。poll 和 select 用于监控多个文件描述符,一旦某个文件就绪(比如读就绪、写就绪),poll 将会捕捉到,进而进行相应的操作。
poll 接口申明如下:
# includestruct pollfd { int fd; short events; short revents; }; int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
参数:
- fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个 struct pollfd 结构,用于指定测试某个给定的 fd 的条件
- nfds:表示 fds 结构体数组的长度
- timeout:表示 poll 函数的超时时间,单位是毫秒
返回值:
- 返回值小于0,表示出错
- 返回值等于0,表示 poll 函数等待超时
- 返回值大于0,表示 poll 监听的文件描述符就绪返回,并且返回结果就是就绪的文件描述符的个数。
events: 指定监测 fd 的事件(输入、输出、错误),每一个事件有多个取值,常用的有以下几个:
- POLLIN:有数据可读
- POLLOUT:有数据可写
- POLLRDNORM:普通数据可读
- POLLRDBAND:优先级带数据可读
- …
下面给一个例子:使用 poll 监控标准输入。
#incude#include void main() { struct pollfd pfd; pfd.fd = 0 // 标准输入的文件描述符 pfd.event = POLLIN; while(1) { int ret = poll(&pfd, 1, 2000); if (ret < 0) { // error return; } else if (ret == 0) { // timeout printf("timeout!n"); } else { // to do something you want char buf[1024]; read(0, buf, sizeof(buf)); printf("hello!n"); } } }
以上代码可以监控标准输入,你还可以自定义需要监控的文件。
再来看 gdb 的事件循环机制。gdb 程序在完成一系列初始化操作后,就会进入事件循环(Event Loop):start_event_loop 函数循环执行 gdb_do_one_event(我这里的gdb 版本为 7.12)。
gdb_do_one_event 中使用 poll 和 select(取决于系统支持哪个函数,并由编译宏控制)监控多个文件描述符,也即事件。gdb 的事件有两种,一种是用户通过 cli 或者 tui 输入的事件,另一种是来自目标程序进程发送给 gdb 信号的事件。
几个关键数据结构和函数:
gdb_notifier: 用于描述 gdb 监控的文件事件。
typedef struct gdb_event
{
event_handler_func *proc;
event_data data;
} *gdb_event_p;
typedef struct file_handler
{
int fd;
int mask;
int ready_mask;
handler_func *proc;
gdb_client_data client_data;
int error;
struct file_handler *next_file;
}
file_handler;
static struct
{
file_handler *first_file_handler;
file_handler *next_file_handler;
#ifdef HAVE_POLL
struct pollfd *poll_fds;
int next_poll_fds_index;
int poll_timeout;
#endif
fd_set check_masks[3];
fd_set ready_masks[3];
int num_fds;
struct timeval select_timeout;
int timeout_valid;
}
gdb_notifier;
add_file_handler/create_file_handler:用于将 gdb 监控的文件描述符和相应的回调函数添加到 gdb_notifier 中。
static
void create_file_handler (int fd, int mask,
handler_func * proc,
gdb_client_data client_data);
linux_nat_event_pipe[] 和 async_file_mark 函数:
gdb 监控两种文件。一种是标准输入,用于接收用户命令,对应的 fd 固定为 0,只要用户输入命令,poll 即可监控到,进而触发相应的动作。
另一种为目标程序发出的异步信号。gdb 调试目标程序会创建一个子进程(gdb 中称为 inferior),子进程会通过 SIGCHLD 信号告知 gdb 主进程自身的状态。gdb 使用数组 linux_nat_event_pipe 来描述异步信号的事件,第一个元素用于表示文件描述符,第二个元素作为文件内容。当 gdb 捕捉到 inferior 的信号时,通过 add_file_handler 接口向 gdb_notifier 中添加该文件描述符和注册回调函数,并通过 async_file_mark 接口向 linux_nat_event_pipe 数组中写内容。gdb 通过 poll 可以监控该文件,进而进入回调处理函数。
static int linux_nat_event_pipe[2] = { -1, -1 };
static void
async_file_mark (void)
{
int ret;
async_file_flush ();
do
{
ret = write (linux_nat_event_pipe[1], "+", 1);
}
while (ret == -1 && errno == EINTR);
}
start_event_loop、gdb_do_one_event、gdb_wait_for_event:
start_event_loop -> gdb_do_one_event -> gdb_wait_for_event,gdb_wait_for_event 中使用 poll 监控 gdb_notifier 中的文件和调用相应的回调函数。
void
start_event_loop (void)
{
while (1)
{
int result = 0;
TRY
{
result = gdb_do_one_event ();
}
CATCH (ex, RETURN_MASK_ALL)
{
...
}
int gdb_do_one_event (void);
static int gdb_wait_for_event (int block);
极简的 gdb 处理流程如下:
gdb 首先进行初始化操作:包括读取可执行程序、读取符号表、通过 add_file_handler 接口将标准输入文件描述符和回调函数注册到 gdb_notifier 中,gdb 进入事件循环 。然后用户输入命令:比如设置断点,然后 run。gdb 在事件循环中监控到标准输入事件,然后执行用户命令、创建子进程等等。gdb 接收到 inferior 的 SIGCHLD 信号后,通过 add_file_handler 和 async_file_mark 等接口向 event_loop 插入事件。gdb 监控到子进程的事件,进而做出相应的处理,接着在向 event_loop 插入标准输入事件,待用户继续输入命令、resume 子程序。如此循环进行,直到用户输入退出命令。
本文介绍了 gdb 两大关键技术:ptrace 系统调用和 event_loop 机制。更多 gdb 内部实现的技术,即将推出,敬请期待。



