假设一个内核函数,test,如下:
void test()
{
condition = update_something();
if (condition == 1) {
M1();
} else {
return;
}
}
现在需要统计在test中进入M1函数的次数,怎么办?
很简单,使用kpatch技术即可,或者,使用kprobe技术,亦可。但这些成熟的技术没有可玩性,它们属于工程的范畴,而不属于手艺。说白了,就是对于手艺人而言,kpatch,kprobe这种技术,没意思。
这里介绍一种有意思的方法,当然,这只是对于工人而不是对于经理来说的。
要点如下:
- 单独分配一段足够大且可执行的内存p,将test函数整体拷贝过去。
- 将原始test的开头5个字节替换成 32位相对跳转指令 ,即 0xe9 $rel_offset 。
- 在p中找到需要插入统计计数的位置,增加下面的7个字节指令 asm (“incl 0xffffffff81977890” ::: );
0xffffffff81977890这个地址是内核中panic_on_oops变量的地址。
如果真的试着这样做了,发现绝大多数情况下,这是不行的,哪里错了呢?这里有两个问题需要注意:
- 如何分配一段足够的内存来承载拷贝过来的test函数的指令?
- 处理拷贝后的函数内相对偏移变更。
这两点其实很好理解。
首先,我们要明白,为了修改原始函数的前5个字节为相对跳转,新内存必须和原始函数test之间的间隔在s32类型可以表示的范围以内,也就是前后2G总共4G的范围。这一点,我们以do_fork为例:
0xffffffff81073840 <do_fork>: nopl 0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffff81073845 <do_fork+5>: push %rbp
可以发现,只有5个字节的空间。
所以,我们的新内存的位置必须确保在test前后2G的范围内。
显然,kmalloc很难满足这个需求,我们不妨换个思路,直接借用一个模块中的stub函数。但是考虑到模块中的stub函数可能大小并不够,所以我们采用属性定义其按照足够的大小对齐:
void test_stub1(void) __attribute__ ((aligned (1024)));
void test_stub2(void) __attribute__ ((aligned (1024)));
void test_stub1(void)
{
printk("yes\n");
}
void test_stub2(void)
{
printk("yes yes\n");
}
这样,test_stub1就有1024个字节可用了,一般的函数,足够了。我们只需要用test来覆盖这个test_stub1就可以了。通常情况下,模块中的函数是在0xffffffff00000000以上的,这个和内核函数处在相对跳转可以够得到的范围内。
此外,当我们在原始函数内部插入了inc指令后,会改变拷贝后的函数的相对偏移,甚至将原始函数指令拷贝到新的地方,也会改变原有的相对偏移,这个不得不察。
所以,必须扫描所有的拷贝后指令,修改其相对跳转偏移:
- 函数内的相对偏移需改改变7个指令的增量。
- 函数外的相对偏移在7个指令增量的前提额外需要改变p - test的增量。
采用这个方法插入一些统计计数,比如系统中半连接数量的统计计数,对于工人们来讲,还是很有意思的。
浙江温州皮鞋湿,下雨进水不会胖。