gdb调试器底层实现原理

我,子牙老师,一个手写过操作系统、编程语言、Java虚拟机、docker、Ubuntu系统,玩透Windows内核、Linux内核的…硬核男人

前几天写了两篇调试器相关的文章《从零手写gdb调试器》《调试器是如何让代码断下来的》,在多个学员群里引发了大家的讨论。没想到大家对调试器这么有热情!今天再来把大家讨论比较多的话题写成文章分享:gdb调试器的底层实现原理

gdb的底层实现,一半是基于函数ptrace实现的,一半是调试器本身的业务实现。比如下断点,是基于ptrace函数实现的,但是支持多断点、条件断点,是调试器自身业务实现。这句话如果你不太理解,看上面列出的两篇文章打个底

搞明白gdb的底层实现,其实就是搞明白ptrace函数的底层实现,更准确的说,是ptrace在Linux内核中干了些什么
在这里插入图片描述

通过看前面的文章,大家应该知道,ptrace具体要做什么,是靠第一个参数request控制的
在这里插入图片描述

本篇文章就分析前五个request吧,其他的,后面再写文章分享。关注公众号【硬核子牙】,看Linux内核、调试器、编程语言、docker等底层硬核文章

以下,enjoy

变身可调试进程

Linux平台的任何调试器,如果是基于ptrace实现,一定是fork+exec+waitpid结构。看代码
在这里插入图片描述

通过fork创建子进程,子进程调用ptrace,request=PTRACE_TRACEME,变成可调试进程,就是让Linux内核允许调试器调试自己。这里的调试器就是父进程

我们要研究的第一个点就是:ptrace进入内核后做了什么让进程变成了可调试进程呢?父进程作为调试进程,与child被调试进程是如何建立联系的呢?

来,看ptrace进入内核后对应的系统调用sys_ptrace代码
在这里插入图片描述

核心逻辑在ptrace_traceme中,看代码
在这里插入图片描述

529行,给调试进程的task_struct.ptrace赋值PT_PTRACED

530行,调用ptrace_link。这个函数干了什么呢?看代码
在这里插入图片描述

75行,将child(调试进程)的task_struct.ptrace_entry与parent(调试进程)的task_struct.ptraced关联。

这里注意一下,调试进程与被调试进程是一对多的关系,即一个调试器可以调试多个进程,但是一个进程只能被一个调试器调试

76行,将child(调试进程)的task_struct.parent赋值为new_parent,即调试器进程

BTW,进程结构体task_struct中有两个parent,它们之间的区别如图
在这里插入图片描述

77行,配置调试证书,用于后面调试器操作的权限验证,比如是否有权限读写内存、读写寄存器等

来张总结图吧
在这里插入图片描述

大师,你悟了吗?

下断点

下断点应该是咱们使用调试器用的最多的功能吧,来看看gdb是如何实现的。看简版代码
在这里插入图片描述

看懂这里的代码需要一点基础,来补下。这里下的是软件断点,硬件断点是直接写DR寄存器。对软件断点与硬件断点不了的,看文章《调试器是如何让代码停下来的》

软件断点的本质是在对应的内存位置写入0xCC,翻译成汇编就是int3,对应的是3号软中断

比如想在main函数上下断点,就是把main函数开头位置的机器码0x55改成0xCC
在这里插入图片描述

如果直接改行不行呢?可以!但是会有问题!怎么理解呢?0x55对应的汇编指令是push rbp,就是把rbp寄存器的值存入栈顶,与后面的pop rbp是对称的。如果直接改,就不会做push rbp这件事,但是后面依然会执行pop rbp,就会导致程序执行出错

所以应该怎么做呢?先把0x55读出来,然后保存起来,当执行到断点的时候,写回去。24行代码,就是读机器码,默认是读8字节,执行完后data=0x400609bfe5894855。30行代码,就是把0x55取出来,保存起来。31行代码,就是把0x55改成0xCC,33行代码,将改成0xCC的data写入,完成下断点

那request=PTRACE_PEEKTEXT或request=PTRACE_POKETEXT,Linux内核层是怎么做的呢?

读写代码区跟读写数据区,走的函数是一样的,我就放在一起讲了

读写内存

直接看Linux内核中的代码
在这里插入图片描述

其实本质都是读写内存,只是传的参数不一样
在这里插入图片描述
在这里插入图片描述

核心函数是__access_remote_vm。这个函数是干嘛的呢?通过GUP机制跨进程读写内存!你要读懂这个函数的逻辑你就要了解4-level paging机制及Linux内核内存布局、伙伴系统。

我直接说答案吧,这个函数的底层,就是通过调试进程的vma找到addr对应的page对象,进而找到物理页,然后调用kmap将调试进程的物理页映射到调试器进程的虚拟内存中,进而完成读写

至此,调试器下断点的秘密,调试器读写内存的秘密,你已全部知晓!

其他的request的底层实现原理,我会陆续出文章。对调试器底层实现感兴趣的小伙伴,可以关注公众号【硬核子牙】看我写的硬核文章

最后再打个小广告,如果你想学习手写操作系统及实战Linux内核,如果你对调试器底层实现感兴趣,同时想写一个自己可以百分百控制的调试器,调试你自己写的操作系统,欢迎看我【个人简介】详细了解我的课程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值