GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)

系列文章:

GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)
GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解
GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例
GDB 源码分析系列文章五:动态库延迟断点实现机制

GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)

  • ptrace 系统调用
  • 事件循环(Event Loop)

关于 gdb 内部实现介绍的文章非常少,本人计划通过阅读 gdb 源码,推出系列文章介绍 gdb 内部实现的机制,以窥视 gdb 内部是如何控制和调试程序的。

GDB 通过 ptrace 系统调用技术控制和调试目标程序,并通过事件循环(Event Loop)机制循环处理来自用户和目标程序的事件。
GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)_第1张图片

ptrace 系统调用

ptrance 是 linux 系统提供调试进程的系统调用,ptrace 接口提供了丰富的参数让我们能够控制和调试目标进程。ptrace 接口如下:

#include 
long 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

一个典型调试过程简化如下:

  1. 父进程(gdb进程)调用 fork() 创建一个子进程。
  2. 子进程通过 ptrace(PTRACE_TRACEME, …) 接口将自己设置为被追踪模式,并通过 execl() 运行被调试程序。
  3. 子进程执行 execl()运行目标程序时,会给子进程发送 SIGTRAP 信号,让子进程暂停,并向父进程发送 SIGCHLD 信号。
  4. 父进程通过 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 接口申明如下:

# include 
struct 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 <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 内部实现的技术,即将推出,敬请期待。

你可能感兴趣的:(GDB,编译工具链,gdb,poll,事件循环,ptrace)