|
哈喽,大家好,我就是那个不喜欢在大厂搬砖,不喜欢在研究院做研究,只喜欢创业做计算机底层课程的coder,子牙老师
讲调试器底层原理的文章、书、视频极少极少,但是调试器强大的功能吸引着无数的coder想去一探究竟,我也是其中之一。
之前写过系列文章《从零手写gdb调试器》《调试器是如何让代码停下来的》《gdb单步调试底层实现》《gdb查看反汇编底层原理》,感兴趣的小伙伴可以去看看。还做了一个系统性大课《从零带你手写gdb调试器》,想学习手写调试器的小伙伴可以加班主任vx【jvm-anan】咨询
今天来聊聊gdb调试器修改进程上下文中的寄存器数据是如何做到的。瓦特?你不知道寄存器是什么?看图 寄存器是CPU内部的“超高速小型存储器”,用于存放CPU正在处理的数据与地址,是整个计算机中速度最快的存储单元。通俗说就是CPU在计算1+2的时候,会把1跟2读入寄存器中进行运算,然后将运行结果写入专门用于存放函数返回结果的rax中
只有学汇编语言,才需要学习寄存器。如果你想精通汇编语言,关注公众号【硬核子牙】回复【汇编教程】免费领取我讲的汇编教程【精通汇编语言】
使用gdb调试器,你可以这样查看被调试进程的所有寄存器数据 你也可以使用print查看具体某个寄存器,set可以修改寄存器 查看寄存器的底层实现,上篇文章《gdb查看进程寄存器数据是如何做到的》已经讲过了,本篇文章是下篇,所以如果你没有基础又想看懂本篇文章,那先把上篇文章看一看
直接开始干货分享吧。以下,enjoy
是直接修改的吗
很多人会想,gdb执行set $rax=1就可以将11写入CPU中的rax寄存器是直接写的吧!当然不是!为什么呢?
因为CPU此时运行的是gdb调试器进程,被调试进程此时是Stop状态。CPU寄存器中的数据都是gdb进程的。那是怎么做到的呢?
上篇文章提过,gdb通过执行info r拿到的被调试进程的寄存器数据,是镜像数据。是调度器在执行任务切换的时候,将被调试进程的寄存器数据写入了进程的内核态栈中 那么多寄存器,按照这个顺序存储在栈顶 所以gdb在执行set $rax=11,实际上改的是栈中的!那这个11什么时候写入真正的寄存器中的呢?
当gdb调试器给被调试进程发送SIGCONT信号唤醒被调试器进程,被调试进程获得CPU时间片的时候。调度器会将栈中的寄存器数据恢复到真正的CPU寄存器中,这样进程就可以接着上次运行的位置继续运行。这个过程叫进程的上下文恢复
口说无凭,接下来咱们找证据:1、set确实是修改的栈;2、进程上下文恢复,将栈底的寄存器数据写入真正的CPU寄存器
set修改的是进程的内核态栈
此时,很多人束手无策了。来,看我操作!如果你需要这个环境学习Linux内核,可以购买我的小课《零基础+AI带你玩转Linux内核》,文末有购买方式
使用交互式单步调试Linux内核环境,执行set $rax=11,进入内核,开始调试,找核心代码! 调试器的实现,是调用ptrace函数+request=PTRACE_SETREGS实现的! 函数copy_regset_from_user,最终执行x64 regset view中的set函数 set函数是这个 核心代码就在这里
这里的实现逻辑,有点奇葩的是,我只是要修改rax,但是它会把view中的所有寄存器信息都写一遍!作为Linux内核,不应该如此啊!理性情况不应该是这样吗?
根据rax计算出它在struct pt_regs中的offset 进而得到rax在进程栈中的内存地址addr,用户设置的11在ubuf中的值,就可以实现直接写*addr=ubuf->rax
设置的是rax的值,你知道,Linux内核怎么知道呢?除非用户态传,那ptrace机制就要变动了,现在是直接传pt_regs的 这就是大工程了!
Linux内核恢复寄存器
最后一个问题,寄存器数据恢复,这个好难找,都是汇编宏,给我找麻木了
被调试进程执行到断点,进行Stop状态。软件断点,软中断,进入内核执行的函数是do_int3。恢复上下文的时机应该是CPU处理完int3软中断返回用户态的路上。这个你要写操作系统才知道 中间过程我就省略了,最终会找到这里 这是个汇编宏,展开长这样
跟这张图是不是就对上了 至此,一切谜底揭开!
|