系列文章:
GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)
GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解
GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例
GDB 源码分析系列文章五:动态库延迟断点实现机制
关于 gdb 内部实现介绍的文章非常少,本人计划通过阅读 gdb 源码,推出系列文章介绍 gdb 内部实现的机制,以窥视 gdb 内部是如何控制和调试程序的。
GDB 通过 ptrace 系统调用技术控制和调试目标程序,并通过事件循环(Event Loop)机制循环处理来自用户和目标程序的事件。
ptrance 是 linux 系统提供调试进程的系统调用,ptrace 接口提供了丰富的参数让我们能够控制和调试目标进程。ptrace 接口如下:
#include
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
这里重点介绍几个 request:
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 使用 ptrace 系统调用控制和调试目标程序的基本原理。接下来将介绍 gdb 如何通过事件循环机制具体控制和调试目标的。
介绍 gdb 事件循环机制前,需要先介绍下该机制实现的关键技术: poll / select 接口。poll 和 select 实现机制类似,这里只介绍下 poll 接口(详细说明直接查看手册:man poll/select)。
在 linux 系统中,一切 IO 设备都抽象成文件(一切皆是文件, Every thing is file!)。poll 和 select 用于监控多个文件描述符,一旦某个文件就绪(比如读就绪、写就绪),poll 将会捕捉到,进而进行相应的操作。
poll 接口申明如下:
# include
struct pollfd {
int fd; /*文件描述符*/
short events; /*监控的事件*/
short revents; /*监控事件中满足条件返回的事件*/
};
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
参数:
返回值:
events: 指定监测 fd 的事件(输入、输出、错误),每一个事件有多个取值,常用的有以下几个:
下面给一个例子:使用 poll 监控标准输入。
#incude <stdio.h>
#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
{
/* Procedure to call to service this event. */
event_handler_func *proc;
/* Data to pass to the event handler. */
event_data data;
} *gdb_event_p;
/* Information about each file descriptor we register with the event
loop. */
typedef struct file_handler
{
int fd; /* File descriptor. */
int mask; /* Events we want to monitor: POLLIN, etc. */
int ready_mask; /* Events that have been seen since
the last time. */
handler_func *proc; /* Procedure to call when fd is ready. */
gdb_client_data client_data; /* Argument to pass to proc. */
int error; /* Was an error detected on this fd? */
struct file_handler *next_file; /* Next registered file descriptor. */
}
file_handler;
static struct
{
/* Ptr to head of file handler list. */
file_handler *first_file_handler;
/* Next file handler to handle, for the select variant. To level
the fairness across event sources, we serve file handlers in a
round-robin-like fashion. The number and order of the polled
file handlers may change between invocations, but this is good
enough. */
file_handler *next_file_handler;
#ifdef HAVE_POLL
/* Ptr to array of pollfd structures. */
struct pollfd *poll_fds;
/* Next file descriptor to handle, for the poll variant. To level
the fairness across event sources, we poll the file descriptors
in a round-robin-like fashion. The number and order of the
polled file descriptors may change between invocations, but
this is good enough. */
int next_poll_fds_index;
/* Timeout in milliseconds for calls to poll(). */
int poll_timeout;
#endif
/* Masks to be used in the next call to select.
Bits are set in response to calls to create_file_handler. */
fd_set check_masks[3];
/* What file descriptors were found ready by select. */
fd_set ready_masks[3];
/* Number of file descriptors to monitor (for poll). */
/* Number of valid bits (highest fd value + 1) (for select). */
int num_fds;
/* Time structure for calls to select(). */
struct timeval select_timeout;
/* Flag to tell whether the timeout should be used. */
int timeout_valid;
}
gdb_notifier;
add_file_handler/create_file_handler:用于将 gdb 监控的文件描述符和相应的回调函数添加到 gdb_notifier 中。
/* Add a file handler/descriptor to the list of descriptors we are
interested in.
FD is the file descriptor for the file/stream to be listened to.
For the poll case, MASK is a combination (OR) of POLLIN,
POLLRDNORM, POLLRDBAND, POLLPRI, POLLOUT, POLLWRNORM, POLLWRBAND:
these are the events we are interested in. If any of them occurs,
proc should be called.
For the select case, MASK is a combination of READABLE, WRITABLE,
EXCEPTION. PROC is the procedure that will be called when an event
occurs for FD. CLIENT_DATA is the argument to pass to PROC. */
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 可以监控该文件,进而进入回调处理函数。
/* Async mode support. */
/* The read/write ends of the pipe registered as waitable file in the
event loop. */
static int linux_nat_event_pipe[2] = { -1, -1 };
static void
async_file_mark (void)
{
int ret;
/* It doesn't really matter what the pipe contains, as long we end
up with something in it. Might as well flush the previous
left-overs. */
async_file_flush ();
do
{
ret = write (linux_nat_event_pipe[1], "+", 1);
}
while (ret == -1 && errno == EINTR);
/* Ignore EAGAIN. If the pipe is full, the event loop will already
be awakened anyway. */
}
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 中的文件和调用相应的回调函数。
/* Start up the event loop. This is the entry point to the event loop
from the command loop. */
void
start_event_loop (void)
{
/* Loop until there is nothing to do. This is the entry point to
the event loop engine. gdb_do_one_event will process one event
for each invocation. It blocks waiting for an event and then
processes it. */
while (1)
{
int result = 0;
TRY
{
result = gdb_do_one_event ();
}
CATCH (ex, RETURN_MASK_ALL)
{
...
}
/* Process one high level event. If nothing is ready at this time,
wait for something to happen (via gdb_wait_for_event), then process
it. Returns >0 if something was done otherwise returns <0 (this
can happen if there are no event sources to wait for). */
int gdb_do_one_event (void);
/* Wait for new events on the monitored file descriptors. Run the
event handler if the first descriptor that is detected by the poll.
If BLOCK and if there are no events, this function will block in
the call to poll. Return 1 if an event was handled. Return -1 if
there are no file descriptors to monitor. Return 1 if an event was
handled, otherwise returns 0. */
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 内部实现的技术,即将推出,敬请期待。