栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

探寻 GDB 内部实现系列文章一:ptrace 系统调用和事件循环(Event Loop)

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

探寻 GDB 内部实现系列文章一:ptrace 系统调用和事件循环(Event Loop)

探寻 GDB 内部实现系列文章一:ptrace 系统调用和事件循环(Event Loop)
  • ptrace 系统调用
  • 事件循环(Event Loop)

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

GDB 通过 ptrace 系统调用技术控制和调试目标程序,并通过事件循环(Event Loop)机制循环处理来自用户和目标程序的事件。

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

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/858951.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号