我总劝人不要用 kretprobe 耍技巧,会脱手,
Linux kernel 的 kretprobe 机制和 kprobe 完全不同,本质原因在于,函数的入口地址是固定的,但函数的返回地址不固定,由于返回位置不固定,无法固定函数大小,无法事先插桩。一图以示之:
想在函数返回时插入个 hook,只有在函数被调用时确定了函数返回地址(x86 为例,压栈返回地址)后才可能。
因此,kprobe 在register 时即可将 hook 挂载,与 kprobe 不同,kretprobe 需在函数每次被调用时才能将 hook 挂载。这只是事情的轮廓,“在函数调用时挂载 hook” 才是大头。
task 将函数返回地址替换成 kretprobe 的 hook 地址之前要保存旧返回地址,由于函数可被不同task 同时调用,因此不同 task 需拥有独立的 kretprobe instance。
同一个 kretprobe hook,不同 kretprobe instance,Linux kernel 预分配了若干(与 CPU 数量相关) kretprobe instance,挂接在 kretprobe 的某 list 上,每当有 task 需要 kretprobe 特定函数时,在函数的入口处先从 kretprobe 的 list 上摘出一 kretprobe instance 供自己私用,此时函数返回地址已经压栈,替换返地址为 hook 前将其 save 在独享的 kretprobe instance 中。
很长一段时间跨越很多内核版本,多个 kretprobe instance 以 hlist node 的方式挂接在以 task地址 hash 为 key 的 hlist 上,而对 hlist node 的 CURD 必然涉及 lock,对于高频调用的热点函数,kretprobe 里的这个lock 相当于自设的路障hash 到同一个 bucket 的 task 同时调用一个函数时会被串行化,严重影响性能,甚至使系统 soft/hard lockup。这是一个知道的人很少的秘密。
为什么不用 percpu lock? 因为函数调用返回前可能发生 task 迁移
后来,那些煤炭奈儿(maintainer,妹忒讷儿估计也发现了问题,提交了一些派驰(patch),将hlist 改成了 CAS-based lock-free free list, 但人家自己也说了 Not the fastest thing in theworld under heavy contention, but simple and correct,详见源文件
上面这些话,请自行比较不同内核版本的 pre handler kretprobe 函数实现。该函数的注释是:This kprobe pre handler is registered with every kretprobe. When probe hits it will set upthe return probe.
so?
别张口就来,挂 kretprobe 的前提是你理解它的副作用,而不是只知道个词,特别是那些上了ebpf 毒瘾的。
我讨厌一些脱离一线很久却又要彰显自己懂得很多而对工人精确指点的经理,这些经理只给出一祈使句指导,代码脚本还是工人照着经理意思去写,经理彰显才华后,事了拂衣去。
经理知道 kretprobe 可以修改函数返回值,但他大概不知道 kretprobe 隐藏的秘密。知道这些秘密的经理不会指使工人用 kretprobe 修改返回值。经理就好好当经理,做好资源和人力的分配调度,别天天混进工人队伍里瞎指挥,还美其名日技术导向,这就是扯淡。
kretprobe 的理论解法是将 return address 也固定,因此理论上可以截获 ret 指令,进入一个common stub hook 中判定该函数是不是需要被 hook,如果不需要这就返回,如果需要就先调用 hook 再返回,但依然会引入别的开销,那么可以缩小替换范围,只在 kretprobe function 附近搜索并替换 ret 指令。
工人提出用 kretprobe 修改 init cwnd,经理会说这是非标的方案,kprobe/kretprobe 更多只做 debug 和可观测性,不能上线...但工人想修改 nit cwnd 却没方案时,经理会让工人用kretprobe 来改,绝口不提是否非标。经理知道 kprobe/kretprobe,但并不真懂,仅科普水平,或做过 demo,但也仅此。对技术细节非常感兴趣又亲自指挥细节的经理不是合格经理。但不知何时,不知为什么,工人们普遍觉得懂技术的经理才合格,于是经理们不得不去懂点技术,显得很 geek,这样才是真经理。很多经理都会此地无银三百两式地强调自己写过代码,懂技术,可也没人问啊。