在开发过程中我们都会使用 gdb 进行调试程序,那么当我们使用 gdb 进行调试程序的时候,底层发生了什么?

今天通过分析 ptrace 系统调用来分析下 gdb 的底层实现原理。

ptrace 是什么

ptrace 是操作系统提供的一个用于跟踪进程的系统调用。通过 ptrace 系统调用可以获取被跟踪进程的进程状态。

比如我们常用的获取可执行文件执行时都进行了哪些系统调用的 strace 命令和我们常使用的调试工具 gdb 等,他们都是通过使用 ptrace 进行实现的。

在使用 gdb 进行本地跟踪某个进程时,常使用方式有 2 种:

1、gdb 可执行文件,被跟踪进程从头开始执行,也即是使用 PTRACE_TRACEME 来使自己进入被跟踪模式。

2、gdb attach 进程 id,跟踪一个已经运行的进程,也即是使用 PTRACE_ATTACH 来使指定的进程进入被跟踪模式。

**

ptrace 系统调用

**

ptrace 系统调用如下

#include <sys/ptrace.h>

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

ptrace 的各个参数如下:

request: 指定调试的指令,比如:PTRACE_PEEKDATA、PTRACE_SYSCALL、PTRACE_CONT、PTRACE_KILL、PTRACE_ATTACH 等等。

pid : 进程的 id。

addr:进程的某个地址空间,可以通过该参数对进程的某个地址进行读和写。

data:根据 request 的不同而变化,如果需要向目标进程中写入数据,data存放的是需要写入的数据;如果从目标进程中读数据,data将存放返回的数据。

ptrace 使用例子

下面通过一个例子来说明 ptrace 系统调用的使用方式。

该例子通过使用ptrace系统调用用来获取子进程执行一个可执行文件时都进行了哪些系统调用,返回系统调用的 id 号和返回值,类似于 strace 命令。

#include <stdio.h>
#include <sys/ptrace.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/reg.h>

int main(int argc, char *argv[])
{
pid_t pid = fork();
if(pid < 0)
{
printf(“fork failed\n”);
exit(-1);

}else if(pid == 0){ // 子进程//告诉内核本进程被父进程进行跟踪ptrace(PTRACE_TRACEME, 0, NULL, NULL);execve("argv[1]", NULL, NULL);} else { // 父进程int status;int flag = 1;long num;long ret;//父进程中则使用wait系统调用等待子进程的状态改变wait(&status);if(WIFEXITED(status)){return 0;}//获取子进程系统调用号num = ptrace(PTRACE_PEEKUSER, pid, ORIG_RAX * 8, NULL);printf("system call num = %ld\n", num);//PTRACE_SYSCALL 会使得每次子进程进行系统调用或系统调用退出时被暂停,内核会给父进程发信号,父进程可以获取子进程的状态变化ptrace(PTRACE_SYSCALL, pid, NULL, NULL);while(1){//等待子进程发送 SIGCHLD 信号wait(&status); if(WIFEXITED(status))return 0;if(flag ){//获取系统调用号num =  ;printf("system call num = %ld", num);flag = 0;}else {//获取系统调用返回值ret = ptrace(PTREACE_PEEKUSER, pid, RAX * 8, NULL);printf("system call return = %ld\n", ret);flag = 1;}ptrace(PTRACE_SYSCALL, pid, NULL, NULL);}}

}

上面的程序流程如下:

主进程 fork 一个子进程。

子进程通过 ptrace(PTRACE_TRACEME, 0, NULL, NULL) 把自己设置为被跟踪状态,然后子进程调用 execve() 进行加载可执行文件,子进程在运行前向父进程发送一个 SIGCHLD 信号,并暂停本进程。

父进程收到信息号后,调用 ptrace(PTRACE_PEEKUSER, pid, ORIG_RAX * 8, NULL) 获取子进程的系统调用号。然后调用 ptrace(PTRACE_SYSCALL, pid, NULL, NULL) 来跟踪子进程系统调用,也即是子进程每次进行系统调用前和调用后都会暂停本进程并通知父进程,父进程即可获取子进程的系统调用 id 和系统调用的返回值。

父进程在被子进程每次进行系统调用前和调用后唤醒后,通过调用 ptrace(PTRACE_PEEKUSER, pid, xx, NULL) 获取寄存器中的值。

ptrace 实现原理

ptrace 函数最终会调用 sys_ptrace 内核函数,而 sys_ptrace 的实现就是通过 switch 根据 request 的不同而进行不同的处理。

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{

if (request == PTRACE_TRACEME) {/* are we already being traced? */if (current->flags & PF_PTRACED)return -EPERM;/* set the ptrace bit in the process flags. */current->flags |= PF_PTRACED;return 0;
}//通过id获取对应的进程
if (!(child = get_task(pid)))return -ESRCH;switch (request) {case PTRACE_PEEKTEXT: /* read word at location addr. */ case PTRACE_PEEKDATA: {...}case PTRACE_PEEKUSR: {...}/* when I and D space are separate, this will have to be fixed. */case PTRACE_POKETEXT: /* write the word at location addr. */case PTRACE_POKEDATA:...case PTRACE_POKEUSR: /* write the word at location addr in the USER area */...case PTRACE_SYSCALL: /* continue and stop at next (return from) syscall */case PTRACE_CONT: { /* restart after signal. */...}case PTRACE_KILL: {...}case PTRACE_SINGLESTEP: {  /* set the trap flag. */...}case PTRACE_DETACH: { /* detach a process that was attached. */...}default:return -EIO;
}

}

在 sys_ptrace 中,首先通过进程 pid 来获取进程的 task_struct 内核结构。然后根据传入的参数 request 来对进程进行不同的操作。

使用 PTRACE_TRACEME 进入被跟踪模式

该该方式中 gdb 进程会 fork 一个子进程,然后子进程执行可执行文件进行运行,然后 gdb 进程对新运行的进程进行跟踪。

执行过程如下:

if(pid == 0){
//告诉内核本进程被父进程进行跟踪
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
//加载执行文件
execve(“argv[1]”, NULL, NULL);
}

子进程先设置状态为被跟踪状态,然后使用 execv 系列函数进行加载可执行文件进行运行。

ptrace 会调用 sys_ptrace 系统调用。

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
struct task_struct *child;
struct user * dummy;
int i;

dummy = NULL;if (request == PTRACE_TRACEME) {/* are we already being traced? */if (current->flags & PF_PTRACED)return -EPERM;/* set the ptrace bit in the process flags. */current->flags |= PF_PTRACED;return 0;
}...

}

当进程调用者使用 PTRACE_TRACEME 时,会把当前进程状态设置为被跟踪者 PF_PTRACED,然后退出。

接下来进程调用 execv 系列函数进行加载可执行文件进行运行,具体实现本文不再具体分析,可以查看《Linux 可执行文件程序载入和执行过程》。

execv 进行加载可执行文件时,会调用 load_elf_binary ,在该函数的最后会进行判断当前进程状态,若设置了 PF_PTRACED,则给当前进程发送一个 SIGTRAP 信号。

static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{

if (current->flags & PF_PTRACED)
send_sig(SIGTRAP, current, 0);

return 0;

}

进程收到信号后, 会调用 notify_parent 通知父进程。

asmlinkage int do_signal(unsigned long oldmask, struct pt_regs * regs)
{

while ((signr = current->signal & mask)) {...if ((current->flags & PF_PTRACED) && signr != SIGKILL) {current->exit_code = signr;//设置本进程为暂停状态current->state = TASK_STOPPED;//通知父进程notify_parent(current);//调度其他进程schedule();...}
}...

}

void notify_parent(struct task_struct * tsk)
{
if (tsk->p_pptr == task[1])
tsk->exit_signal = SIGCHLD;

//向父进程发送 SIGCHLD 信号
send_sig(tsk->exit_signal, tsk->p_pptr, 1);
wake_up_interruptible(&tsk->p_pptr->wait_chldexit);

}

到此可知,在加载完可执行文件,子进程通知完父进程后就会被暂停了,这个是时候父进程被唤醒进行运行。

唤醒的父进程就可以通过 sys_ptrace 系统调用进行获取子进程的各种状态数据了,比如调用 ptrace(PTRACE_PEEKUSER, pid, ORIG_RAX * 8, NULL) 获取子进程的系统调用号等。

获取系统调用过程 PTRACE_SYSCALL

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{

if (request == PTRACE_TRACEME) {...
}...switch (request) {...//跟踪系统调用case PTRACE_SYSCALL: /* continue and stop at next (return from) syscall */case PTRACE_CONT: { /* restart after signal. */long tmp;if ((unsigned long) data > NSIG)return -EIO;if (request == PTRACE_SYSCALL)//给子进程设置跟踪系统调用位child->flags |= PF_TRACESYS; elsechild->flags &= ~PF_TRACESYS;child->exit_code = data;child->state = TASK_RUNNING; //设置子进程运行状态/* make sure the single step bit is not set. */tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) & ~TRAP_FLAG;put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);return 0;}...//跟踪单步执行case PTRACE_SINGLESTEP: {  /* set the trap flag. */long tmp;if ((unsigned long) data > NSIG)return -EIO;child->flags &= ~PF_TRACESYS;  //取消跟踪系统调用位//设置单步跟踪位tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) | TRAP_FLAG;put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);child->state = TASK_RUNNING; 设置子进程运行状态child->exit_code = data;/* give it a chance to run. */return 0;}...default:return -EIO;
}

}

比如父进程调用 sys_ptrace 跟踪子进程获取系统调用过程,则给子进程设置跟踪系统调用bit位,设置子进程为运行状态让子进程运行,当子进程每次调用系统调用前和系统调用后,都会调用 _syscall_trace 函数。

// arch/i386/kernel/entry.S
_system_call:

.align 4
1: call _syscall_trace //系统调用前调用_syscall_trace
movl ORIG_EAX(%esp),%eax //设置系统调用号
call _sys_call_table(,%eax,4) //系统调用
movl %eax,EAX(%esp) # save the return value
movl _current,%eax
movl errno(%eax),%edx
negl %edx
je 1f
movl %edx,EAX(%esp)
orl $(CF_MASK),EFLAGS(%esp) # set carry to indicate error
1: call _syscall_trace //系统调用后调用_syscall_trace

_syscall_trace 函数的实现如下:

asmlinkage void syscall_trace(void)
{
if ((current->flags & (PF_PTRACED|PF_TRACESYS))
!= (PF_PTRACED|PF_TRACESYS))
return;

current->exit_code = SIGTRAP;
current->state = TASK_STOPPED; //暂停本进程
notify_parent(current); //通知父进程
schedule();if (current->exit_code)current->signal |= (1 << (current->exit_code - 1));
current->exit_code = 0;

}

syscall_trace 函数作用暂停本进程,然后通知父进程,这个时候父进程就可以通过 sys_ptrace 系统调用从 ORIG_EAX(32位,64位为 ORIG_RAX)寄存器中获取系统调用号。

同理,当子进程调用完系统调用后,调用 syscall_trace 通知父进程,父进程通过 sys_ptrace 系统调用 从寄存器 EAX(32位,64位为 ORIG_RAX)中获取系统调用的返回值。

单步调试 PTRACE_SINGLESTEP

通过上述分析可知,对于单步执行的实现原理跟获取系统调用的方式是同样的。

//跟踪单步执行
case PTRACE_SINGLESTEP: { /* set the trap flag. */
long tmp;

 if ((unsigned long) data > NSIG)return -EIO;child->flags &= ~PF_TRACESYS;  //取消跟踪系统调用位//设置单步跟踪位tmp = get_stack_long(child, sizeof(long)*EFL-MAGICNUMBER) | TRAP_FLAG;put_stack_long(child, sizeof(long)*EFL-MAGICNUMBER,tmp);child->state = TASK_RUNNING; 设置子进程运行状态child->exit_code = data;/* give it a chance to run. */return 0;

}

给子进程设置一个单步跟踪的 bit 位,这样cpu每次执行一个指令后便会产生一个异常,系统就会给被调试进程发送一个 SIGTRAP 信号,被调试进程的信号处理函数会发送一个 SIGCHLD 信号给调试进程(父进程),并且让自己停止。

调试进程收到 SIGCHLD 信号后,就可以对被调试进程进行各种操作,比如读取被调试进程的内存数据和各个寄存器中的值。

**

使用 PTRACE_ATTACH 使指定进程进入被跟踪模式

**

通过 ptrace 也可以跟踪调试一个已经存在运行的程序,其具体方式是通过

PTRACE_ATTACH 实现的。

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{

if (request == PTRACE_ATTACH) {...//设置被调试状态child->flags |= PF_PTRACED;//设置和父进程的关系if (child->p_pptr != current) {REMOVE_LINKS(child);child->p_pptr = current;SET_LINKS(child);}//给子进程发送 SIGSTOP 信号send_sig(SIGSTOP, child, 1);return 0;
}...

}

其实现原理就是给子进程设置一个被调试状态,给子进程发送一个 SIGSTOP 信号让子进程暂停,子进程在信号处理函数中通知父进程,然后父进程进行控制子进程获取所需信息。

小结

ptrace 的功能十分强大,通过本文的分析我们可以了解到其实现的原理过程,本文只是对部分操作进行了分析,若有想了解更多的功能,可自行查看源码分析。

原文链接
Linux GDB的实现原理

欢迎关注公众号 Linux码农,获取更多干货

Linux GDB的实现原理相关推荐

  1. 一文带你看透 GDB 的 实现原理 -- ptrace真香

    文章目录 Ptrace 的使用 GDB 的基本实现原理 Example1 通过ptrace 修改 被追踪进程的内存数据 Example2 通过ptrace 对被追踪进程进行单步调试 Ptrace的实现 ...

  2. Linux GDB常用命令一栏

    Linux GDB 常用命令如下: 1.启动和退出gdb (1)启动:gdb ***:显示一段版权说明: (*** 表示可执行程序名) (2)退出:quit.有的时候输入quit后会出现相关提示:类似 ...

  3. linux子系统gdp调试,Linux GDB调试 详述

    今天来分享下gdb的简单调试,我这里写了个例子 三个.c文件 func1.c func2.c main.c 首先生成可调试的执行文件 gcc -g func1.c func2.c main.c -o ...

  4. linux gdb 脚本,如何写gdb命令脚本

    作为UNIX/Linux下使用广泛的调试器,gdb不仅提供了丰富的命令,还引入了对脚本的支持:一种是对已存在的脚本语言支持,比如python,用户可以直接书写python脚本,由gdb调用python ...

  5. Linux 原生异步 IO 原理与使用

    目录 什么是异步 IO? Linux 原生 AIO 原理 Linux 原生 AIO 使用 什么是异步 IO? 异步 IO:当应用程序发起一个 IO 操作后,调用者不能立刻得到结果,而是在内核完成 IO ...

  6. Linux 下 TC 命令原理及详解<一>

    文章目录 1 前言 2 相关概念 3 使用TC 4 创建HTB队列 5 为根队列创建相应的类别 6 为各个类别设置过滤器 7 复杂的实例 Linux 下 TC 命令原理及详解<一> Lin ...

  7. Linux 文件系统的工作原理深度透析

    磁盘为系统提供了最基本的持久化存储. 文件系统则在磁盘的基础上,提供了一个用来管理文件的树状结构. 那么,磁盘和文件系统是怎么工作的呢?又有哪些指标可以衡量它们的性能呢? 索引节点和目录项 文件系统, ...

  8. linux从接通电源到操作系统启动,第4章-Linux引导过程及原理要点.ppt

    <第4章-Linux引导过程及原理要点.ppt>由会员分享,可在线阅读,更多相关<第4章-Linux引导过程及原理要点.ppt(98页珍藏版)>请在人人文库网上搜索. 1.Li ...

  9. linux 随机数原理,Linux随机数生成器的原理和缺陷.pdf

    第17卷.第10期 计算机技术与发展 vol.17No.10 2007年10月 COMPUTERTECHNOLOGYANDDEVELOPMENT Oct.2007 Linux随机数生成器的原理及缺陷 ...

  10. linux随机数原理,Linux随机数生成器的原理与缺陷.pdf

    第17卷.第10期 计算机技术与发展 vol.17No.10 2007年10月 COMPUTERTECHNOLOGYANDDEVELOPMENT Oct.2007 Linux随机数生成器的原理及缺陷 ...

最新文章

  1. SOPC第四课 按键中断
  2. 推荐:制作地图的网站和工具
  3. 单片机小白学步系列(五) 集成电路、封装相关知识
  4. OJ7627-鸡蛋的硬度【各种dp之4】
  5. 深度学习算法 第四期
  6. C++--第9课 - 构造与析构 - 上
  7. GIS软件的发展现状总结
  8. 如何在html中加入下划线,文档中加入下划线
  9. [GNSS] GNSS原理:多模导航卫星精密定轨理论
  10. vue 阻止输入框冒泡
  11. 一行代码实现F11的功能,即让浏览器窗口全屏
  12. IDear 创建web项目
  13. 3年Android开发工程师面试经验分享,先收藏了
  14. Perfect Office Manner for Secretary 完美文秘办公礼仪
  15. C++::namespace
  16. Joint Discriminative and Generative Learning for Person Re-identification 论文翻译
  17. idea中重新加载新的依赖方法
  18. 第五期_信息收集《Metasploit Unleashed Simplified Chinese version(Metasploit官方文档教程中文版)》
  19. Adobe XD 好用的插件我推荐这 10 个!
  20. java试题3,计算邮局汇款的汇费

热门文章

  1. N76E003 驱动 UC1705并口屏(8080)
  2. KTV信息管理系统+点歌系统(WPF)
  3. 图像分辨率之1080P与1080i
  4. sonar配置报错问题处理
  5. ESP-MESH 无线组网,让智能家居通信更方便 | ESP32轻松学(Arduino版)
  6. 给罗永浩和王自如打个分
  7. BAT齐聚阿里安全-ASRC生态大会:呼吁联合共建网络安全白色产业链
  8. 等保2.0 安全计算环境 ——Windows服务器(三级系统)
  9. 传奇服务端如何添加地图
  10. POJ-2632:Crashing Robots(C++实现详细代码)