热补丁与ftrace的兼容性浅析

一、从ftrace的架构说起

ftrace框架的核心是一个"钩子链"机制。内核编译时开启-pg-mfentry后,每个函数入口会被插入call __fentry__。内核启动时这些调用被动态替换成NOP;当某个trace功能启用时,再替换为call ftrace_caller

关键点在于:ftrace_caller并非直接调用你的trace函数,而是通过trampoline遍历所有注册到该函数的ftrace_ops链表 。每个ftrace_ops代表一个ftrace使用者,包含回调函数和标志位。函数被调用时,框架依次执行所有注册的ops回调。

二、livepatch和function trace的ops有何不同

Function Trace的ops :只设置FTRACE_OPS_FL_RECURSION_SAFE等标志。回调函数(如function_trace_call读取寄存器信息并记录到ring buffer ,然后返回,不会修改pt_regs中的指令指针(IP),原函数继续正常执行。

Livepatch的ops :在klp_patch_func()中创建klp_ops,设置关键标志:

c 复制代码
ops->fops.func = klp_ftrace_handler;
ops->fops.flags = FTRACE_OPS_FL_DYNAMIC |
                  FTRACE_OPS_FL_IPMODIFY |   // 关键!允许修改IP
                  FTRACE_OPS_FL_SAVE_REGS;

FTRACE_OPS_FL_IPMODIFY告诉ftrace:我的回调可能修改pt_regs->ip,请允许我劫持执行流程klp_ftrace_handler()根据当前任务的patch状态,将regs->ip改为新函数地址。trampoline执行RET时,CPU直接跳转到新函数,原函数体被"跳过"。

三、为什么它们不会冲突?

1. 调用链的层级关系

当两者同时启用,ftrace维护ops链表。假设function trace先注册,livepatch后注册:

复制代码
ftrace_ops_list: [function_trace_ops] -> [klp_ops]

cmdline_proc_show()被调用时:

  1. call ftrace_caller
  2. 进入trampoline,保存寄存器
  3. 遍历ops链表,依次调用每个ops->func()
  4. 先调用function trace回调:记录trace信息,返回(不修改IP)
  5. 再调用klp_ftrace_handler()修改regs->ip为new_func地址
  6. trampoline恢复寄存器(IP已被修改),RET跳转到new_func

2. IPMODIFY的独占性约束

FTRACE_OPS_FL_IPMODIFY有重要限制:同一函数上同时只能有一个ops设置该标志 。这意味着不能对同一函数打两个livepatch,但可以同时有function trace + livepatch(后者不设置IPMODIFY)。若kretprobe等工具先注册到该函数,livepatch会返回-EEXIST失败。

3. Livepatch的func_stack机制

同一函数可被打多次补丁,livepatch通过func_stack管理。数据结构关系:

c 复制代码
struct klp_ops {
    struct list_head node;        // 全局链表节点
    struct list_head func_stack;  // 函数补丁栈
    struct ftrace_ops fops;       // ftrace操作结构
};

每个原始函数对应一个klp_ops(只注册一次ftrace handler),但func_stack可保存多个klp_func。新补丁通过list_add_rcu()头插到链表,形成LIFO栈

复制代码
func_stack: [patch_v3] → [patch_v2] → [patch_v1] → (原函数)
                ↑
              栈顶(最新生效)

klp_ftrace_handler()list_first_or_null_rcu()取栈顶作为当前生效函数。卸载时从栈中移除对应节点,若栈空则注销ftrace。若卸载中间补丁,系统自动回退到下一层,保证依赖关系。

不过官方强烈推荐Cumulative Patches 配合atomic replace

c 复制代码
static struct klp_patch patch = {
    .mod = THIS_MODULE,
    .objs = objs,
    .replace = true,  // 清空旧补丁,只保留当前
};

func_stack支持叠加补丁(开发调试),replace=true则避免"补丁套娃"的维护噩梦。

四、实验验证的再思考

插入livepatch后启用function trace,再cat /proc/cmdline。实际执行路径:

  1. 用户空间read() → 内核调用cmdline_proc_show()
  2. 函数入口触发ftrace
  3. function trace先记录 :"cmdline_proc_show被调用了"
  4. livepatch劫持 :IP改为patch_cmdline_proc_show
  5. 执行补丁函数,输出修改后的内容

trace日志里记录的函数名可能还是cmdline_proc_show(取决于trace实现是否读取修改后的IP),但实际执行已是补丁函数。

五、总结

两者能同时生效,本质是ftrace的插件化架构

  1. 共享基础设施 :都依赖-mfentry和动态代码补丁
  2. 职责分离:function trace是"观察者"(只读),livepatch是"劫持者"(改写IP)
  3. 标志位区分FTRACE_OPS_FL_IPMODIFY管理执行流修改权限
  4. func_stack管理:支持补丁叠加,也支持原子替换

这种设计体现了Linux内核"提供机制,而非策略"的哲学。常规场景下它们"和平共处",但混用kretprobe等也使用IPMODIFY的工具时可能冲突,需要留意边界情况。