突然回想起了往事,那是2007年的冬天的一个周五,我在看我的老湿调试Linux协议栈的IP层,只见他修改了路由查找的逻辑,然后直接make install了一下就即时生效了,当时我只知道的是,修改了这个逻辑需要重新编译内核,而他并没有重新编译,好像只是编译了一个文件...编译内核这个耗时又无聊的工作阻碍了我对Linux内核的探索进度,直到今天,我依然对编译内核有相当的恐惧,不怕出错,而是怕磁盘空间不够,initrd的组装拆解之类,太繁琐了。我之所以知道2007年的那天是周五,是因为第二天我要加班,没有谁逼我,我自愿的,因为我想知道师父是怎么做到不重新编译内核就能改变非模块的内核代码处理逻辑的,第二天的收获很多,不但知道了他使用了“镜像协议栈”,还额外赚了一天的加班费,我还记得周六加完班我和老婆去吃了一家叫做石工坊的羊排火锅,人家赠送了一只绿色的兔子玩偶。现在那个玩偶还在,我家小小特别喜欢,就是这么一堆看似无关却又巧合的事,让我在这个周末觉得必须写下一点什么。
好吧,从kprobe开始吧。如果我面试一个搞Linux内核的人,问他怎么调试内核,他回答先加入printk然后重新编译最后载入新内核运行,看dmesg,我会让他先等上几分钟,然后人事就会告诉他让他回去等通知。幸运的是,我没有碰到这样的人让我面试来展现我五十步笑百步的半瓶子晃荡作风,也从来没有碰到过如此不仁慈的面试者,我曾经在一次找工作的时候真的就是这么说的,人家也真的让我去等通知,然而我真的就等到了通知,通知入职的时间以及体检事宜...说这些的目的是想展示一个调试内核的利器,kprobe。它可以动态修改内核地址空间代码的二进制指令,然后执行任意你想让它执行的代码段,这也许应该可以称为二进制动态编程!多么黑的技术,完全无视源代码的逻辑,完全无视编译器的苦功,直接就这么把二进制机器码给改了。
kprobe的工作原理很简单,比如你有一个函数func,你可以在func被调用前和调用后各插入一段代码,我们假设func指令是
begin
go
end
kprobe要做的就是替换掉begin,将其变为:
jmp prefunc
当然在替换前还要保存原有的,以便执行完我们的钩子函数prefunc还能跳回原来的逻辑,至于复杂的jmp细节(长短跳,相对绝对跳之类的)以及Intel的INT 3调试模式单步模式本文不再赘述,赘这个字用得好,因为所有这些细节都是累赘,你换个非Intel平台的话,你就知道这些是多么累赘了,不过对一辈子不换平台的那些人来讲,理解这些细节就成了资本,因此想了解这些,还是去看雪吧,找级别高态度好的问,或者潜水也行,我觉得看雪的信息量已经够大了,基本上都能找到现成的。
虽然我不提倡在本文讲Intel的细节,但是有一个除外,那就是prefunc钩子函数的参数问题,比如我想钩住vfs_write函数,它的声明如下:
如果这个prefunc钩子函数的参数和vfs_write的一样那多好啊,整个逻辑就成了:
但是不幸的是,kprobe做不到。因为它是基于INT 3异常/中断来处理的,而Intel的异常/中断的处理有一套特定的规程,即保存所有的上下文环境,因此它的参数就只有struct pt_regs *regs一个,即所有的寄存器信息。要想还原vfs_write的参数,你必须针对这个regs参数做一个“深度解析”才行,而这又一次将你引入了平台相关的地狱,如果你在X86平台,你就不得不对它的寄存器使用规约做一番详细的了解才能还原被钩函数的参数,对于X86来讲,参数保存在栈中(也可以通过寄存器传参),要想还原被钩函数的参数现场,你要分析的就是regs->sp,下面我就不说了。
说了上述不幸,来点幸运的,那就是Linux内核提供了一种kprobe之上的机制,帮你实现了上面说的那些本应该由你自己完成的工作,这就是jprobe。总的来讲,jprobe的要点在于它实际上就是一个kprobe的prefunc,它的prefunc是这么实现的:
好吧,从kprobe开始吧。如果我面试一个搞Linux内核的人,问他怎么调试内核,他回答先加入printk然后重新编译最后载入新内核运行,看dmesg,我会让他先等上几分钟,然后人事就会告诉他让他回去等通知。幸运的是,我没有碰到这样的人让我面试来展现我五十步笑百步的半瓶子晃荡作风,也从来没有碰到过如此不仁慈的面试者,我曾经在一次找工作的时候真的就是这么说的,人家也真的让我去等通知,然而我真的就等到了通知,通知入职的时间以及体检事宜...说这些的目的是想展示一个调试内核的利器,kprobe。它可以动态修改内核地址空间代码的二进制指令,然后执行任意你想让它执行的代码段,这也许应该可以称为二进制动态编程!多么黑的技术,完全无视源代码的逻辑,完全无视编译器的苦功,直接就这么把二进制机器码给改了。
kprobe的工作原理很简单,比如你有一个函数func,你可以在func被调用前和调用后各插入一段代码,我们假设func指令是
begin
go
end
kprobe要做的就是替换掉begin,将其变为:
jmp prefunc
当然在替换前还要保存原有的,以便执行完我们的钩子函数prefunc还能跳回原来的逻辑,至于复杂的jmp细节(长短跳,相对绝对跳之类的)以及Intel的INT 3调试模式单步模式本文不再赘述,赘这个字用得好,因为所有这些细节都是累赘,你换个非Intel平台的话,你就知道这些是多么累赘了,不过对一辈子不换平台的那些人来讲,理解这些细节就成了资本,因此想了解这些,还是去看雪吧,找级别高态度好的问,或者潜水也行,我觉得看雪的信息量已经够大了,基本上都能找到现成的。
虽然我不提倡在本文讲Intel的细节,但是有一个除外,那就是prefunc钩子函数的参数问题,比如我想钩住vfs_write函数,它的声明如下:
- ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos);
- ssize_t prefunc(struct file *file, const char __user *buf, size_t count, loff_t *pos)
- {
- todo_something(.....);
- return vfs_write(file, buf, count, pos);
- }
说了上述不幸,来点幸运的,那就是Linux内核提供了一种kprobe之上的机制,帮你实现了上面说的那些本应该由你自己完成的工作,这就是jprobe。总的来讲,jprobe的要点在于它实际上就是一个kprobe的prefunc,它的prefunc是这么实现的:
- prefunc(kprobe, regs)
- {
- 保存regs寄存器现场
- 保存栈的内容 //因为jprobe使用和被钩函数相同的栈,可能会改变栈的内容
- 替换regs里面ip指针为jprobe钩子的指针
- 返回
- }