|
哈喽,大家好,我就是那个不喜欢在大厂搬砖,不喜欢在研究院做研究,只喜欢创业做计算机底层课程的coder,子牙老师
讲调试器底层原理的文章、书、视频极少极少,但是调试器强大的功能吸引着无数的coder想去一探究竟,我也是其中之一。
之前写过系列文章《从零手写gdb调试器》《调试器是如何让代码停下来的》《gdb单步调试底层实现》《gdb查看反汇编底层原理》,感兴趣的小伙伴可以去看看。还做了一个系统性大课《从零带你手写gdb调试器》,想学习手写调试器的小伙伴可以加班主任vx【jvm-anan】咨询
今天来聊另一个话题:gdb调试器是如何读写线程上下文之寄存器的?(瓦特?你不知道寄存器是什么?给你最高效的学习视频!关注公众号【硬核子牙】回复【汇编教程】免费领取)
这篇文章看完,你除了对gdb调试器读写寄存器的信息了如指掌,你还能写代码随意修改进程的寄存器信息,想玩黑客必备技能!
普通coder跟黑客的区别是什么?我觉得最大的区别就是:普通coder对计算机世界的各种束缚束手无策,而黑客则是了解这些束缚的原理,进而突破束缚,实现自己的一切不为人知的目的!
国际惯例,先看读写寄存器的案例吧
info r查看所有寄存器信息(x64 CPU) print查看具体某个寄存器,set可以修改寄存器 这些功能的底层是如何实现的呢?Linux内核在其中扮演了怎样的角色呢?我已深入Linux内核,解开了这些疑惑,分享给你
以下,enjoy
gdb拿到的是镜像
没研究之前,我就在想:当用户在gdb中输入info b,调试器会委托Linux内核去拿调试进程的寄存器数据?这怎么做的到呢?一看代码,原来Linux内核是把进程镜像中的寄存器数据给Linux内核了,然后Linux内核把返回给gdb
所以如果想知道gdb是如何拿到调试进程的寄存器信息的,问题就转化为:1、进程的寄存器信息,在Linux内核中是如何存储的?2、Linux内核是在何时保存这个信息的?
接着走
进程的寄存器信息存储上玩底层的顶级思维:如果这个代码让你来写,你会把寄存器信息存储在哪?
存储到task_struct中?类似这样 我写的操作系统就是这么干的。如果你想学习手写操作系统,可以咨询班主任【jvm-anan】我的课程《手写64位多核操作系统》,不难,学起来非常有趣! 但是Linux内核不是这么干的!那Linux内核存储在哪呢?stack中!你怎么证明呢?我单步调试Linux内核给你看
这个环境是我自己打造的交互式单步调试Linux内核环境,玩Linux内核非常方便!全网唯一!对应的课程是《手写Ubuntu Linux发行版》+《手写gdb调试器》,感兴趣的小伙伴咨询班主任【jvm-anan】
将我写的gdb调试器上传到我写的Linux系统中,跑起来 在Linux内核中的sys_ptrace处下断点 眼尖的小伙伴发现了:这函数不是sys_ptrace啊,忽悠人呢!这个就是sys_ptrace,只不过我为了调试方便,搞成了这样 接下来调试器中执行查看寄存器的命令:info r,就可以调试Linux内核找答案了 最终找到核心代码 看copy_regset_to_user的第二个参数,这段代码涉及到Linux内核中的一个机制:regset view机制。你要理解这个机制就需要了解CPU的运行模式,这里就不展开讲了,感兴趣的自行研究或学习我的课程《手写64位多核操作系统》 还没找到最终答案,接着走,进入函数copy_regset_to_user
代码走到哪去了呢?你了解了regset view机制就知道,是这个函数
核心代码就在函数genregs_get中 __put_user就是Linux内核将数据写回用户态的一个宏,那第一个参数毫无疑问,是内存地址。看这个内存地址是否是stack的地址,就可以证明了
info r查看的是通用寄存器,走的是最后那个函数,第一个参数是寄存器信息所在的内存地址 答案找到了!task_stack_page! 这里面的公式是什么意思?因为栈是自高地址向低地址用,所以需要加THREAD_SIZE,就到了栈顶。再减去栈顶的padding,才是真正在用的栈顶。最后为什么-1?因为需要把指针拨到寄存器信息开始的地址 寄存器有那么多,在栈中是按什么顺序存储的呢? 你会发现,这个结构,跟gdb调试器获取寄存器信息传入的参数顺序是一样的! 至此,谜底揭开!进程的寄存器信息存储在stack中。注意!这个stack是进程的内核态栈!
那寄存器的信息是CPU写入的还是Linux内核写入的?又是何时写入的呢?
who在when写入
直接说答案!通用寄存器的信息是Linux内核写入的,不可能是CPU写入的
此时你是不是想问:你为什么这么笃定的说这句话?因为我写过操作系统,我了解CPU,知道CPU会做什么不会做什么。我在写操作系统的时候,寄存器的数据就是我自己写代码保存的 保存寄存器信息就是为了给寄存器去读的吗?当然不是!你应该知道,你的程序运行,是在很多个CPU时间片下完成的吧。就像你看一本书,不可能一口气读完吧,此时有人抬杠了:我可以!给你个白眼
你看一本书,每天看一点,你看后面的得回想起前面的内容吧,这叫记忆的保存与恢复
程序也是一样的,在第二个CPU时间片执行代码,得知道第一个CPU时间片,程序执行到哪了,执行出了哪些结果。所以第一个CPU时间片执行完了,需要保存寄存器信息,给第二个CPU时间片恢复记忆用。从程序的角度,这个叫线程上下文的保存与恢复
调试器,只是恰好用得上这个记忆!就像你书还没看完,别人跟你聊起,你能从记忆中把信息调取出来一样
其实就算计的世界,跟人的世界,道理是相通的!
Linux内核写入寄存器信息Linux内核这部分代码在哪呢?得调动我写操作系统的经验了
第一个CPU时间片结束,保存上下文。为什么会结束?中断来了,线程调度要开始了!所以朦胧的知道,代码大概率在线程切换的位置。Linux内核线程切换的入口在哪呢?这里 找到答案了! 注释的意思是:切换新进程的内存空间及寄存器信息,相对的就是,保存老线程的寄存器信息。继续找核心代码 功夫不负有心人,找到了! cool!你已经知道线程的调试器信息保存在哪了,你自己写代码把stack中的寄存器数据改掉,线程恢复运行的时候,是不是就将你修改的寄存器数据恢复到真正的CPU寄存器中了,不就达到了修改寄存器的目的!
所以我一直说,你不自己写一个操作系统就去读Linux内核,是不可能完全玩明白的。就像你都没读过书,连字都不认识,就想写一篇这么硬核的文章,是不可能做到的,一样的道理
同样,你脑子里没货,AI发展的再强大,它对你的价值,也只是那么大。因为你问不出你认知以外的问题,你也看不懂你认知以外的答案!
|