一、从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()被调用时:
call ftrace_caller- 进入trampoline,保存寄存器
- 遍历ops链表,依次调用每个ops->func()
- 先调用function trace回调:记录trace信息,返回(不修改IP)
- 再调用
klp_ftrace_handler():修改regs->ip为new_func地址 - 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。实际执行路径:
- 用户空间
read()→ 内核调用cmdline_proc_show() - 函数入口触发ftrace
- function trace先记录 :"
cmdline_proc_show被调用了" - livepatch劫持 :IP改为
patch_cmdline_proc_show - 执行补丁函数,输出修改后的内容
trace日志里记录的函数名可能还是cmdline_proc_show(取决于trace实现是否读取修改后的IP),但实际执行已是补丁函数。
五、总结
两者能同时生效,本质是ftrace的插件化架构:
- 共享基础设施 :都依赖
-mfentry和动态代码补丁 - 职责分离:function trace是"观察者"(只读),livepatch是"劫持者"(改写IP)
- 标志位区分 :
FTRACE_OPS_FL_IPMODIFY管理执行流修改权限 - func_stack管理:支持补丁叠加,也支持原子替换
这种设计体现了Linux内核"提供机制,而非策略"的哲学。常规场景下它们"和平共处",但混用kretprobe等也使用IPMODIFY的工具时可能冲突,需要留意边界情况。