内核热补丁和function trace的兼容性浅析
本文代码基于linux内核4.19.195.之前的文章简要讲解了内核热补丁的原理也提到了热补丁是基于ftrace框架实现的。平时我们在用ftrace时最常用的功能当属function tracer了。这天一个有趣的问题突然浮现在我的脑海里如果我对同一个函数既打了热补丁又用function trace去跟踪这两个功能能同时生效吗直接上答案是可以的。验证的话也非常方便内核里的samples/livepatch/livepatch-sample.c编译出来的ko会hack函数cmdline_proc_show()也就是我们cat /proc/cmdline的输出函数。将这个ko插入后再通过debugfs去trace一下cmdline_proc_show()然后手动触发一下cat /proc/cmdline看下输出是否为hack后的内容以及ftrace框架是否有相关function trace的打印即可。那为什么可以呢一、从ftrace的架构说起要理解为什么这两个功能能共存我们得先看看ftrace的内部设计。ftrace框架的核心是一个钩子链机制。当内核编译时开启-pg或-mfentry选项后每个函数入口处会被插入一个call __fentry__指令x86_64架构。内核启动时这些调用会被动态替换成NOP指令以消除性能开销当某个trace功能被启用时再将其替换为call ftrace_callerftrace_caller其实就是ftrace的trampline。关键点在于ftrace_caller并非直接调用trace函数而是遍历所有注册到该函数的ftrace_ops结构体链表。每个ftrace_ops代表一个ftrace使用者它包含一个回调函数func和一些标志位。当函数被调用时ftrace框架会依次执行所有注册到该函数的ops的回调。二、livepatch和function trace的ops有何不同现在来看livepatch和function trace各自的ops注册方式Function Trace的ops普通的function tracer如function或function_graph注册的ops通常只设置FTRACE_OPS_FL_RECURSION_SAFE等标志。它的回调函数如function_trace_call只是读取寄存器信息并记录到ring buffer然后返回。它不会修改pt_regs中的指令指针(IP)原函数会继续正常执行。Livepatch的opsLivepatch注册的ops则大不相同。在klp_patch_func()函数中它会创建一个klp_ops结构并设置以下关键标志ops-fops.funcklp_ftrace_handler;ops-fops.flagsFTRACE_OPS_FL_DYNAMIC|FTRACE_OPS_FL_IPMODIFY|// 关键标志FTRACE_OPS_FL_SAVE_REGS;FTRACE_OPS_FL_IPMODIFY是这里的秘密武器。这个标志告诉ftrace框架我的回调函数可能会修改pt_regs-ip指令指针请允许我劫持这个函数的执行流程。在livepatch的回调函数klp_ftrace_handler()中它会根据当前任务的patch状态将regs-ip修改为新函数的地址。当ftrace trampoline执行RET指令时CPU会直接跳转到新函数而不是回到原函数的push rbp之后。这样原函数的整个函数体都被跳过了。三、为什么它们不会冲突现在可以回答核心问题了为什么两者能同时工作1. 调用链的层级关系当function trace和livepatch同时启用时ftrace框架会维护一个ops链表。假设function trace先注册livepatch后注册链表大概是这样的ftrace_ops_list:[function_trace_ops]-[klp_ops]当cmdline_proc_show()被调用时执行流程如下call ftrace_caller函数入口处的5字节指令进入ftrace trampoline保存寄存器遍历ops链表依次调用每个ops-func()先调用function trace的回调记录trace信息返回不修改IP再调用livepatch的klp_ftrace_handler()修改regs-ip为new_func地址trampoline恢复寄存器此时IP已被修改RET指令执行跳转到new_func而非原函数2. IPMODIFY的独占性约束需要注意的是FTRACE_OPS_FL_IPMODIFY标志有一个重要限制同一个函数上同时只能有一个ops设置这个标志。这意味着你不能对同一个函数打两个livepatch但可以通过func_stack机制叠加多个补丁你可以同时有function trace livepatch因为function trace不设置IPMODIFY如果另一个也使用IPMODIFY的工具如kretprobe先注册到了该函数livepatch的注册会返回-EEXIST失败。3. Livepatch的func_stack机制同一个函数可以被打多次补丁livepatch通过func_stack函数栈来管理这些补丁。当多次patch同一个函数时不会创建新的ftrace_ops而是将新的klp_func结构体加入到已有ops的func_stack链表中。klp_ftrace_handler()总是选择栈顶的函数作为实际执行的版本LIFO原则。这保证了最新的补丁生效同时也支持一致性模型consistency model下的平滑过渡。四、代码层面的验证让我们通过内核代码来验证上述流程。在kernel/livepatch/patch.c中klp_ftrace_handler的核心逻辑如下staticvoidklp_ftrace_handler(unsignedlongip,unsignedlongparent_ip,structftrace_ops*fops,structftrace_regs*fregs){structklp_ops*ops;structklp_func*func;opscontainer_of(fops,structklp_ops,fops);/* 获取当前应该执行的函数版本 */funclist_first_or_last_entry(ops-func_stack,...);if(func-nop)return;/* NOP补丁直接执行原函数 *//* 关键修改指令指针实现函数跳转 */klp_arch_set_pc(fregs,(unsignedlong)func-new_func);}而普通的function trace回调如function_trace_call则不会调用klp_arch_set_pc只是简单地记录数据。另外我们再看一下linux内核里对同一个ip有多个hack的回调时是如何处理的staticvoidftrace_ops_list_func(unsignedlongip,unsignedlongparent_ip,structftrace_ops*op,structpt_regs*regs){__ftrace_ops_list_func(ip,parent_ip,NULL,regs);}staticnokprobe_inlinevoid__ftrace_ops_list_func(unsignedlongip,unsignedlongparent_ip,structftrace_ops*ignored,structpt_regs*regs){structftrace_ops*op;intbit;bittrace_test_and_set_recursion(TRACE_LIST_START,TRACE_LIST_MAX);if(bit0)return;/* * Some of the ops may be dynamically allocated, * they must be freed after a synchronize_sched(). */preempt_disable_notrace();do_for_each_ftrace_op(op,ftrace_ops_list){//遍历这个链表ftrace_ops_list/* * Check the following for each ops before calling their func: * if RCU flag is set, then rcu_is_watching() must be true * if PER_CPU is set, then ftrace_function_local_disable() * must be false * Otherwise test if the ip matches the ops filter * * If any of the above fails then the op-func() is not executed. */if((!(op-flagsFTRACE_OPS_FL_RCU)||rcu_is_watching())ftrace_ops_test(op,ip,regs)){//如果其中的ftrace_ops能够在本IP插桩点执行if(FTRACE_WARN_ON(!op-func)){pr_warn(op%p %pS\n,op,op);gotoout;}op-func(ip,parent_ip,op,regs);//在当前ip满足hash的情况下逐个执行ftrace_ops-func()}}while_for_each_ftrace_op(op);out:preempt_enable_notrace();trace_clear_recursion(bit);}可以看到__ftrace_ops_list_func()会逐个执行hack的回调函数而不管这些回调函数是否会修改ftrace trampline返回时需要回到的IP。五、实验验证的再思考回到文章开头的实验插入livepatch后通过debugfs启用function trace然后cat /proc/cmdline。根据上面的分析实际执行路径是用户空间调用read()系统调用内核最终调用cmdline_proc_show()函数入口处触发ftracefunction trace先记录“嘿cmdline_proc_show被调用了”livepatch劫持执行流将IP改为patch_cmdline_proc_show执行补丁函数输出被修改的内容返回用户空间所以看到的trace日志里记录的函数名可能还是cmdline_proc_show但实际执行的已经是补丁函数了。六、总结内核热补丁和function trace能同时生效本质是因为ftrace框架设计良好的插件化架构共享基础设施都依赖-mfentry编译选项和动态代码补丁机制职责分离function trace是观察者只读livepatch是劫持者改写IP标志位区分通过FTRACE_OPS_FL_IPMODIFY标志管理对执行流的修改权限链表遍历多个ops可以共存按注册顺序依次执行这种设计体现了Linux内核中常见的单一职责原则——ftrace提供了一个通用的函数钩子机制不同的功能可以根据需要选择不同的权限级别是否修改IP而框架本身负责协调这些使用者之间的兼容性。不过也要注意边界情况如果function trace设置了FTRACE_OPS_FL_SAVE_REGS_IP之类的标志某些特殊tracer或者与kretprobe等也使用IPMODIFY的工具混用可能会遇到冲突。但在常规场景下它们确实是和平共处的。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2464071.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!