实战能力|一文看懂GDB调试底层实现
在程序出现bug的时候,最好的解决办法就是通过 GDB
调试程序,然后找到程序出现问题的地方。比如程序出现 段错误
(内存地址不合法)时,就可以通过 GDB
找到程序哪里访问了不合法的内存地址而导致的。
本文不是介绍 GDB 的使用方式,而是大概介绍 GDB 的实现原理,当然 GDB 是一个庞大而复杂的项目,不可能只通过一篇文章就能解释清楚,所以本文主要是介绍 GDB 使用的核心的技术 - ptrace
。
ptrace系统调用
ptrace()
系统调用是 Linux 提供的一个调试进程的工具,ptrace()
系统调用非常强大,它提供非常多的调试方式让我们去调试某一个进程,下面是 ptrace()
系统调用的定义:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
下面解释一下 ptrace()
各个参数的作用:
request
:指定调试的指令,指令的类型很多,如:PTRACE_TRACEME
、PTRACE_PEEKUSER
、PTRACE_CONT
、PTRACE_GETREGS
等等,下面会介绍不同指令的作用。pid
:进程的ID(这个不用解释了)。addr
:进程的某个地址空间,可以通过这个参数对进程的某个地址进行读或写操作。data
:根据不同的指令,有不同的用途,下面会介绍。
ptrace()
系统调用详细的介绍可以参考以下链接:https://man7.org/linux/man-pages/man2/ptrace.2.html
ptrace使用示例
下面通过一个简单例子来说明 ptrace()
系统调用的使用,这个例子主要介绍怎么使用 ptrace()
系统调用获取当前被调试(追踪)进程的各个寄存器的值,代码如下(ptrace.c):
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <stdio.h>int main()
{ pid_t child;struct user_regs_struct regs;child = fork(); // 创建一个子进程if(child == 0) { // 子进程ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 表示当前进程进入被追踪状态execl("/bin/ls", "ls", NULL); // 执行 `/bin/ls` 程序} else { // 父进程wait(NULL); // 等待子进程发送一个 SIGCHLD 信号ptrace(PTRACE_GETREGS, child, NULL, ®s); // 获取子进程的各个寄存器的值printf("Register: rdi[%ld], rsi[%ld], rdx[%ld], rax[%ld], orig_rax[%ld]\n",regs.rdi, regs.rsi, regs.rdx,regs.rax, regs.orig_rax); // 打印寄存器的值ptrace(PTRACE_CONT, child, NULL, NULL); // 继续运行子进程sleep(1);}return 0;
}
通过命令 gcc ptrace.c -o ptrace
编译并运行上面的程序会输出如下结果:
Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
ptrace ptrace.c
上面结果的第一行是由父进程输出的,主要是打印了子进程执行 /bin/ls
程序后各个寄存器的值。而第二行是由子进程输出的,主要是打印了执行 /bin/ls
程序后输出的结果。
下面解释一下上面程序的执行流程:
主进程调用
fork()
系统调用创建一个子进程。子进程调用
ptrace(PTRACE_TRACEME,...)
把自己设置为被追踪状态,并且调用execl()
执行/bin/ls
程序。被设置为追踪(TRACE)状态的子进程执行
execl()
的程序后,会向父进程发送SIGCHLD
信号,并且暂停自身的执行。父进程通过调用
wait()
接收子进程发送过来的信号,并且开始追踪子进程。父进程通过调用
ptrace(PTRACE_GETREGS, child, ...)
来获取到子进程各个寄存器的值,并且打印寄存器的值。父进程通过调用
ptrace(PTRACE_CONT, child, ...)
让子进程继续执行下去。
从上面的例子可以知道,通过向 ptrace()
函数的 request
参数传入不同的值时,就有不同的效果。比如传入 PTRACE_TRACEME
就可以让进程进入被追踪状态,而传入 PTRACE_GETREGS
时,就可以获取被追踪的子进程各个寄存器的值等。
本来我想使用 ptrace
实现一个简单的调试工具,但在网上找到了一位 Google 的大神 Eli Bendersky
写了类似的系列文章,所以我就不再重复工作了,在这里贴一下文章的链接:
https://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1/
https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
https://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information
但由于 Eli Bendersky
大神的文章只是介绍使用 ptrace
实现一个简单的进程调试工具,而没有介绍 ptrace
的原理和实现,所以这里为了填补这个空缺,下面就详细介绍一下 ptrace
的原理与实现。
ptrace实现原理
本文使用的 Linux 2.4.16 版本的内核
看懂本文需要的基础:进程调度,内存管理和信号处理相关知识。
调用 ptrace()
系统函数时会触发调用内核的 sys_ptrace()
函数,由于不同的 CPU 架构有着不同的调试方式,所以 Linux 为每种不同的 CPU 架构实现了不同的 sys_ptrace()
函数,而本文主要介绍的是 X86 CPU
的调试方式,所以 sys_ptrace()
函数所在文件是 linux-2.4.16/arch/i386/kernel/ptrace.c
。
sys_ptrace()
函数的主体是一个 switch
语句,会传入的 request
参数不同进行不同的操作,如下:
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{struct task_struct *child;struct user *dummy = NULL;int i, ret;...read_lock(&tasklist_lock);child = find_task_by_pid(pid); // 获取 pid 对应的进程 task_struct 对象if (child)get_task_struct(child);read_unlock(&tasklist_lock);if (!child)goto out;if (request == PTRACE_ATTACH) {ret = ptrace_attach(child);goto out_tsk;}...switch (request) {case PTRACE_PEEKTEXT:case PTRACE_PEEKDATA:...case PTRACE_PEEKUSR:...case PTRACE_POKETEXT:case PTRACE_POKEDATA:...case PTRACE_POKEUSR:...case PTRACE_SYSCALL:case PTRACE_CONT:...case PTRACE_KILL: ...case PTRACE_SINGLESTEP:...case PTRACE_DETACH:...}
out_tsk:free_task_struct(child);
out:unlock_kernel();return ret;
}
从上面的代码可以看出,sys_ptrace()
函数首先根据进程的 pid
获取到进程的 task_struct
对象。然后根据传入不同的 request
参数在 switch
语句中进行不同的操作。
ptrace()
支持的所有 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
由于 ptrace()
提供的操作比较多,所以本文只会挑选一些比较有代表性的操作进行解说,比如 PTRACE_TRACEME
、PTRACE_SINGLESTEP
、PTRACE_PEEKTEXT
、PTRACE_PEEKDATA
和 PTRACE_CONT
等,而其他的操作,有兴趣的朋友可以自己去分析其实现原理。
进入被追踪模式(PTRACE_TRACEME操作)
当要调试一个进程时,需要使进程进入被追踪模式,怎么使进程进入被追踪模式呢?有两个方法:
被调试的进程调用
ptrace(PTRACE_TRACEME, ...)
来使自己进入被追踪模式。调试进程(如GDB)调用
ptrace(PTRACE_ATTACH, pid, ...)
来使指定的进程进入被追踪模式。
第一种方式是进程自己主动进入被追踪模式,而第二种是进程被动进入被追踪模式。
被调试的进程必须进入被追踪模式才能进行调试,因为 Linux 会对被追踪的进程进行一些特殊的处理。下面我们主要介绍第一种进入被追踪模式的实现,就是 PTRACE_TRACEME
的操作过程,代码如下:
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{...if (request == PTRACE_TRACEME) {if (current->ptrace & PT_PTRACED)goto out;current->ptrace |= PT_PTRACED; // 标志 PTRACE 状态ret = 0;goto out;}...
}
从上面的代码可以发现,ptrace()
对 PTRACE_TRACEME
的处理就是把当前进程标志为 PTRACE
状态。
当然事情不会这么简单,因为当一个进程被标记为 PTRACE
状态后,当调用 exec()
函数去执行一个外部程序时,将会暂停当前进程的运行,并且发送一个 SIGCHLD
给父进程。父进程接收到 SIGCHLD
信号后就可以对被调试的进程进行调试。
我们来看看 exec()
函数是怎样实现上述功能的,exec()
函数的执行过程为 sys_execve() -> do_execve() -> load_elf_binary()
:
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{...if (current->ptrace & PT_PTRACED)send_sig(SIGTRAP, current, 0);...
}
从上面代码可以看出,当进程被标记为 PTRACE
状态时,执行 exec()
函数后便会发送一个 SIGTRAP
的信号给当前进程。
我们再来看看,进程是怎么处理 SIGTRAP
信号的。信号是通过 do_signal()
函数进行处理的,而对 SIGTRAP
信号的处理逻辑如下:
int do_signal(struct pt_regs *regs, sigset_t *oldset)
{for (;;) {unsigned long signr;spin_lock_irq(¤t->sigmask_lock);signr = dequeue_signal(¤t->blocked, &info);spin_unlock_irq(¤t->sigmask_lock);// 如果进程被标记为 PTRACE 状态if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {/* 让调试器运行 */current->exit_code = signr;current->state = TASK_STOPPED; // 让自己进入停止运行状态notify_parent(current, SIGCHLD); // 发送 SIGCHLD 信号给父进程schedule(); // 让出CPU的执行权限...}}
}
上面的代码主要做了3件事:
如果当前进程被标记为 PTRACE 状态,那么就使自己进入停止运行状态。
发送 SIGCHLD 信号给父进程。
让出 CPU 的执行权限,使 CPU 执行其他进程。
执行以上过程后,被追踪进程便进入了调试模式,过程如下图:
当父进程(调试进程)接收到 SIGCHLD
信号后,表示被调试进程已经标记为被追踪状态并且停止运行,那么调试进程就可以开始进行调试了。
获取被调试进程的内存数据(PTRACE_PEEKTEXT / PTRACE_PEEKDATA)
调试进程(如GDB)可以通过调用 ptrace(PTRACE_PEEKDATA, pid, addr, data)
来获取被调试进程 addr
处虚拟内存地址的数据,但每次只能读取一个大小为 4字节的数据。
我们来看看 ptrace()
对 PTRACE_PEEKDATA
操作的处理过程,代码如下:
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{...switch (request) {case PTRACE_PEEKTEXT:case PTRACE_PEEKDATA: {unsigned long tmp;int copied;copied = access_process_vm(child, addr, &tmp, sizeof(tmp), 0);ret = -EIO;if (copied != sizeof(tmp))break;ret = put_user(tmp, (unsigned long *)data);break;}...
}
从上面代码可以看出,对 PTRACE_PEEKTEXT
和 PTRACE_PEEKDATA
的处理是相同的,主要是通过调用 access_process_vm()
函数来读取被调试进程 addr
处的虚拟内存地址的数据。
access_process_vm()
函数的实现主要涉及到 内存管理
相关的知识,可以参考我以前对内存管理分析的文章,这里主要大概说明一下 access_process_vm()
的原理。
我们知道每个进程都有个 mm_struct
的内存管理对象,而 mm_struct
对象有个表示虚拟内存与物理内存映射关系的页目录的指针 pgd
。如下:
struct mm_struct {...pgd_t *pgd; /* 页目录指针 */...
}
而 access_process_vm()
函数就是通过进程的页目录来找到 addr
虚拟内存地址映射的物理内存地址,然后把此物理内存地址处的数据复制到 data
变量中。如下图所示:
access_process_vm()
函数的实现这里就不分析了,有兴趣的读者可以参考我之前对内存管理分析的文章自行进行分析。
单步调试模式(PTRACE_SINGLESTEP)
单步调试是一个比较有趣的功能,当把被调试进程设置为单步调试模式后,被调试进程没执行一条CPU指令都会停止执行,并且向父进程(调试进程)发送一个 SIGCHLD 信号。
我们来看看 ptrace()
函数对 PTRACE_SINGLESTEP
操作的处理过程,代码如下:
asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{...switch (request) {case PTRACE_SINGLESTEP: { /* set the trap flag. */long tmp;...tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;put_stack_long(child, EFL_OFFSET, tmp);child->exit_code = data;/* give it a chance to run. */wake_up_process(child);ret = 0;break;}...
}
要把被调试的进程设置为单步调试模式,英特尔的 X86 CPU 提供了一个硬件的机制,就是通过把 eflags
寄存器的 Trap Flag
设置为1即可。
当把 eflags
寄存器的 Trap Flag
设置为1后,CPU 每执行一条指令便会产生一个异常,然后会触发 Linux 的异常处理,Linux 便会发送一个 SIGTRAP
信号给被调试的进程。eflags
寄存器的各个标志如下图:
从上图可知,eflags
寄存器的第8位就是单步调试模式的标志。
所以 ptrace()
函数的以下2行代码就是设置 eflags
进程的单步调试标志:
tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
put_stack_long(child, EFL_OFFSET, tmp);
而 get_stack_long(proccess, offset)
函数用于获取进程栈 offset
处的值,而 EFL_OFFSET
偏移量就是 eflags
寄存器的值。所以上面两行代码的意思就是:
获取进程的
eflags
寄存器的值,并且设置Trap Flag
标志。把新的值设置到进程的
eflags
寄存器中。
设置完 eflags
寄存器的值后,就调用 wake_up_process()
函数把被调试的进程唤醒,让其进入运行状态。单步调试过程如下图:
处于单步调试模式时,被调试进程每执行一条指令都会触发一次 SIGTRAP
信号,而被调试进程处理 SIGTRAP
信号时会发送一个 SIGCHLD
信号给父进程(调试进程),并且让自己停止执行。
而父进程(调试进程)接收到 SIGCHLD
后,就可以对被调试的进程进行各种操作,比如读取被调试进程内存的数据和寄存器的数据,或者通过调用 ptrace(PTRACE_CONT, child,...)
来让被调试进程进行运行等。
小结
由于 ptrace()
的功能十分强大,所以本文只能抛砖引玉,没能对其所有功能进行分析。另外断点功能并不是通过 ptrace()
函数实现的,而是通过 int3
指令来实现的,在 Eli Bendersky
大神的文章有介绍。而对于 ptrace()
的所有功能,只能读者自己慢慢看代码来体会了。
- END -
看完一键三连在看,转发,点赞
是对文章最大的赞赏,极客重生感谢你
推荐阅读
劫起|再谈Linux epoll惊群问题的原因和解决方案
网络排障全景指南手册v1.0精简版pdf
一个奇葩的网络问题
实战能力|一文看懂GDB调试底层实现相关推荐
- 实战能力|一文看懂GDB调试上层实现
一.前言 这篇文章来聊聊大名鼎鼎的GDB,它的豪门背景咱就不提了,和它的兄弟GCC一样是含着金钥匙出生的,在GNU的家族中的地位不可撼动.相信每位嵌入式开发工程师都使用过gdb来调试程序,如果你说没有 ...
- 【Opencv实战】一文看懂车牌识别系统全部内容,未来市场前景看好(很赞)
前言 车辆号牌是车辆唯一身份证,它的特殊性与重要性决定车牌识别系统成为城市智能交通管理系统 中不可或缺的重要组成部分. 未来,随着我国城市化进程发展的提速,交通压力将更加严峻,因此智能化交通管理将是今 ...
- 一文看懂大数据生态圈完整知识体系【大数据技术及架构图解实战派】
一文看懂大数据生态圈完整知识体系 徐葳 随着大数据行业的发展,大数据生态圈中相关的技术也在一直迭代进步,作者有幸亲身经历了国内大数据行业从零到一的发展历程,通过本文希望能够帮助大家快速构建大数据生态圈 ...
- 一文看懂“声纹识别VPR” | AI产品经理需要了解的AI技术概念_团员分享_@cony
前言:声纹识别是AI领域中一个看似很小.但其实有机会在近期落地,且比较有意思的细分方向:本文作者是"AI产品经理大本营"团员@cony ,她总结了AI产品经理"最必要& ...
- 一文看懂“业务定制智能客服”的产品设计_团员分享_@苍狼剑歌
前言:本文作者是"AI产品经理大本营"团员@苍狼剑歌,现任某一线大厂AI产品经理.另外,文末还有2个"hanniman读者专属福利",1)优惠券 for 三节课 ...
- 一文看懂“语音识别ASR” | AI产品经理需要了解的AI技术概念
原标题:一文看懂"语音识别ASR" | AI产品经理需要了解的AI技术概念 温馨提示:文末有[重大福利]:优惠券(金额很大) for 三节课<产品经理P2(进阶)系列课程&g ...
- 【燃】是时候展现真正的实力了!一文看懂2022华为开发者大赛技术亮点
摘要:2022华为开发者大赛备赛攻略,超详细的开发技术要点介绍来了. 本文分享自华为云社区<[燃]是时候展现真正的实力了!一文看懂2022华为开发者大赛技术亮点>,作者:华为云社区精选 . ...
- 【深度学习理论】一文看懂卷积神经网络
[深度学习理论]一文看懂卷积神经网络 https://mp.weixin.qq.com/s/wzpMtMFkVDDH6scVcAdhlA 选自Medium 作者: Pranjal Yadav 经机器之 ...
- python读取excelsheet-一文看懂用Python读取Excel数据
原标题:一文看懂用Python读取Excel数据 导读:现有的Excel分为两种格式:xls(Excel 97-2003)和xlsx(Excel 2007及以上). Python处理Excel文件主要 ...
最新文章
- 【 c#】实现form窗口中,通过按钮实现tabPage的切换
- gitlable iphone_iPhone 12首批用户成“小白鼠”,“信号故障”躺枪者无数
- 大数据处理黑科技:揭秘PB级数仓GaussDB(DWS) 并行计算技术
- 港大计算机科学奖学金,香港大学奖学金情况怎么样?
- 【转】:localStorage使用总结
- Ruby 的 FileUtils 模块
- 转换到coff期间_error LNK1123: 转换到 COFF 期间失败
- linux 桌面对比,七大顶级Linux桌面比较
- ping命令显示的TTL是什么意思
- 记参加 2022 Google开发者大会
- ros服务器打开网页变慢了,解决ros 在计算机没有限速情况下,有几个网站打开很慢,或者打不开。...
- Linux的拓扑结构,linux底下的makefile框架拓扑结构分析
- Vue报错Invalid handler for event “click“: got undefined的原因及解决办法
- 机器人守则悖论_第一节:机器人三定律的悖论
- 鸿蒙OS分布式任务调度,鸿蒙子系统解读-分布式任务调度篇(下)
- disallow: /api.php,dz论坛如何禁止搜索引擎抓取任何内容?
- 雷赛 DMC1000B c# 获取轴(电机)状态
- HUST软测1504班第4周小组作业成绩:WordCount优化
- 唐诗宋词 v4.0软件下载 真人朗读版 唐诗宋词三百首 唐诗三百首 宋词三百首 唐诗三百首下载 宋词三百首下载...
- 电大计算无纸化测试软件,电大无纸化考试试题
热门文章
- 书生阅读器打开gd文件出错_CAJ论文怎么打开?
- webfunny前端监控
- =在php中,PHPExcel在PHP7中,save(‘php://output’)出现ERR_INVALID_RESPONSE错误的解决方法...
- 基于 DataLakeAnalytics 做跨地域的数据分析 1
- 【重温基础】2.流程控制和错误处理
- BugkuCTF web2
- docker-ce私有仓库搭建
- 数据分析系统数据库选型
- Placeholder in IE8 and older
- CodeForces - 888G Xor-MST(贪心+字典树+最小生成树)