linux内核热补丁KLP原理浅析 本文代码基于linux内核4.19.195.KLP是linux内核中提供打热补丁能力的一个组件。一般我们通过用户态工具kpatch-build来制作热补丁ko该ko会利用KLP子系统的能力完成打热补丁的过程。本文侧重点在于内核侧的实现。kpatch-build的工作原理大致如下图所示就不详细展开了。┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ 原始内核源码 │ │ 修改后的源码 │ │ 运行中的内核 │ │(vanilla)│ │(patched)│ │(vmlinux)│ └────────┬────────┘ └────────┬─────────┘ └─────────────────┘ │ │ └───────────┬───────────┘ ▼ ┌─────────────────┐ │ kpatch-build │ │(差异分析与构建)│ └────────┬────────┘ ▼ ┌─────────────────┐ │ 热补丁模块 │ │(kpatch-*.ko)│ └─────────────────┘kpatch-build做的工作太复杂还是拿内核的例子来讲会简单点。下面代码摘自samples/livepatch/livepatch-sample.cstaticintlivepatch_cmdline_proc_show(structseq_file*m,void*v){seq_printf(m,%s\n,this has been live patched);return0;}staticstructklp_funcfuncs[]{{.old_namecmdline_proc_show,.new_funclivepatch_cmdline_proc_show,},{}};staticstructklp_objectobjs[]{{/* name being NULL means vmlinux */.funcsfuncs,},{}};staticstructklp_patchpatch{.modTHIS_MODULE,.objsobjs,};staticintlivepatch_init(void){intret;retklp_register_patch(patch);if(ret)returnret;retklp_enable_patch(patch);if(ret){WARN_ON(klp_unregister_patch(patch));returnret;}return0;}关于struct klp_patch、static struct klp_object、struct klp_func几个结构体就不做过多的展开非本文重点。可以看到要打热补丁就两件事情把新的函数代码写好调用klp_register_patch()以及klp_enable_patch()讲热补丁打进去重点来了新的函数就按需写即可那klp_register_patch()以及klp_enable_patch()这两个函数是怎么实现的呢/** * klp_register_patch() - registers a patch * patch: Patch to be registered * * Initializes the data structure associated with the patch and * creates the sysfs interface. * * There is no need to take the reference on the patch module here. It is done * later when the patch is enabled. * * Return: 0 on success, otherwise error */intklp_register_patch(structklp_patch*patch){if(!patch||!patch-mod)return-EINVAL;if(!is_livepatch_module(patch-mod)){pr_err(module %s is not marked as a livepatch module\n,patch-mod-name);return-EINVAL;}if(!klp_initialized())return-ENODEV;if(!klp_have_reliable_stack()){pr_err(This architecture doesnt have support for the livepatch consistency model.\n);return-ENOSYS;}returnklp_init_patch(patch);}EXPORT_SYMBOL_GPL(klp_register_patch);klp_have_reliable_stack()比较有意思当前X86架构是支持的但是ARM64架构不支持听说是ARM64业界对于have_reliable_stack这个还没达成一个共识亦或者还没有完成所有必要的工具的开发。主要的工作最终都交给了klp_init_patch()去完成staticintklp_init_patch(structklp_patch*patch){structklp_object*obj;intret;if(!patch-objs)return-EINVAL;mutex_lock(klp_mutex);patch-enabledfalse;init_completion(patch-finish);retkobject_init_and_add(patch-kobj,klp_ktype_patch,klp_root_kobj,%s,patch-mod-name);if(ret){mutex_unlock(klp_mutex);returnret;}klp_for_each_object(patch,obj){retklp_init_object(patch,obj);if(ret)gotofree;}list_add_tail(patch-list,klp_patches);mutex_unlock(klp_mutex);return0;free:klp_free_objects_limited(patch,obj);mutex_unlock(klp_mutex);kobject_put(patch-kobj);wait_for_completion(patch-finish);returnret;}staticintklp_init_object(structklp_patch*patch,structklp_object*obj){structklp_func*func;intret;constchar*name;if(!obj-funcs)return-EINVAL;if(klp_is_module(obj)strlen(obj-name)MODULE_NAME_LEN)return-EINVAL;obj-patchedfalse;obj-modNULL;klp_find_object_module(obj);nameklp_is_module(obj)?obj-name:vmlinux;retkobject_init_and_add(obj-kobj,klp_ktype_object,patch-kobj,%s,name);if(ret)returnret;klp_for_each_func(obj,func){retklp_init_func(obj,func);if(ret)gotofree;}if(klp_is_object_loaded(obj)){retklp_init_object_loaded(patch,obj);if(ret)gotofree;}return0;free:klp_free_funcs_limited(obj,func);kobject_put(obj-kobj);returnret;}/* parts of the initialization that is done only when the object is loaded */staticintklp_init_object_loaded(structklp_patch*patch,structklp_object*obj){structklp_func*func;intret;mutex_lock(text_mutex);module_disable_ro(patch-mod);// 1. 暂时关闭只读保护 (Disable Read-Only),because klp_write_object_relocations 需要修改代码段里的指令retklp_write_object_relocations(patch-mod,obj);// 2. 执行重定位if(ret){// 如果失败记得把保护开回去然后解锁退出module_enable_ro(patch-mod,true);mutex_unlock(text_mutex);returnret;}// 3. 架构相关的初始化 (比如刷新指令缓存 I-Cache)arch_klp_init_object_loaded(patch,obj);module_enable_ro(patch-mod,true);// 4. 恢复只读保护 (Enable Read-Only)mutex_unlock(text_mutex);klp_for_each_func(obj,func){// 1. 查找旧函数在内存中的真实地址retklp_find_object_symbol(obj-name,func-old_name,func-old_sympos,func-old_addr);if(ret)returnret;// 2. 查找旧函数的大小 (Old Size)retkallsyms_lookup_size_offset(func-old_addr,func-old_size,NULL);if(!ret){pr_err(kallsyms size lookup failed for %s\n,func-old_name);return-ENOENT;}// 3. 查找新函数的大小 (New Size)retkallsyms_lookup_size_offset((unsignedlong)func-new_func,func-new_size,NULL);if(!ret){pr_err(kallsyms size lookup failed for %s replacement\n,func-old_name);return-ENOENT;}}return0;}重点关注klp_init_object_loaded其最关键的点在于klp_write_object_relocations()这里会把热补丁修改所引入的那些非导出符号给解析了可以参考kpatch-build中为什么需要做重定位表的相关文章结合起来一起理解然后再对热补丁涉及的结构做一些初始化。klp_register_patch()主要是做初始化klp_enable_patch()则是完成热补丁使能的关键函数。intklp_enable_patch(structklp_patch*patch){intret;mutex_lock(klp_mutex);if(!klp_is_patch_registered(patch)){ret-EINVAL;gotoerr;}ret__klp_enable_patch(patch);err:mutex_unlock(klp_mutex);returnret;}EXPORT_SYMBOL_GPL(klp_enable_patch);staticint__klp_enable_patch(structklp_patch*patch){structklp_object*obj;intret;if(klp_transition_patch)return-EBUSY;if(WARN_ON(patch-enabled))return-EINVAL;/* enforce stacking: only the first disabled patch can be enabled */if(patch-list.prev!klp_patches!list_prev_entry(patch,list)-enabled)return-EBUSY;/* * A reference is taken on the patch module to prevent it from being * unloaded. */if(!try_module_get(patch-mod))return-ENODEV;pr_notice(enabling patch %s\n,patch-mod-name);klp_init_transition(patch,KLP_PATCHED);// 初始化状态过渡/* * Enforce the order of the func-transition writes in * klp_init_transition() and the ops-func_stack writes in * klp_patch_object(), so that klp_ftrace_handler() will see the * func-transition updates before the handler is registered and the * new funcs become visible to the handler. */smp_wmb();klp_for_each_object(patch,obj){if(!klp_is_object_loaded(obj))continue;retklp_pre_patch_callback(obj);//pre callif(ret){pr_warn(pre-patch callback failed for object %s\n,klp_is_module(obj)?obj-name:vmlinux);gotoerr;}retklp_patch_object(obj);// 对每个对象应用补丁if(ret){pr_warn(failed to patch object %s\n,klp_is_module(obj)?obj-name:vmlinux);gotoerr;}}klp_start_transition();// 开始状态过渡klp_try_complete_transition();// 完成过渡patch-enabledtrue;return0;err:pr_warn(failed to enable patch %s\n,patch-mod-name);klp_cancel_transition();returnret;}使能热补丁的逻辑非常清晰初始化状态过渡调用热补丁的pre call对每个对象应用补丁开始状态过渡完成过渡做清理工作让我们一个个逻辑步骤来看这里以使能热补丁为例。第一步对于初始化状态过渡说白了就是给每个task给上KLP_UNPATCHED这个标志第二步调用热补丁的pre call这个没啥说的第三步对每个对象应用补丁这个调用了ftrace的接口通过ftrace_set_filter_ip()及register_ftrace_function()利用ftrace的机制在钩子函数里面将RIP替换掉从而实现了函数的替换当然这个钩子函数klp_ftrace_handler()里会根据进程的状态current-patch_state确定使用旧的代码还是新的代码详细就不展开了第四步开始状态过渡即klp_start_transition()函数的工作。本质就是给所有进程进程置上TIF_PATCH_PENDING的标签。第五步完成过渡也就是klp_try_complete_transition()函数的工作这块也是klp的核心工作但逻辑也不复杂。绝大部分工作由函数klp_try_switch_task()完成本质上对于非running的task就是通过检查函数栈确定函数的调用链路里面不涉及本次更新的代码详见函数klp_check_stack()就可以让该task度过“klp的过渡状态”清除该task的TIF_PATCH_PENDING并修改task-patch_state。而对于running的task如果这个task就是自己current那也可以做栈的检查但如果不是自己就只能暂时放弃了因为running的task你永远不知道他下一时刻会跑到哪个代码里。嗯那遇到running的task检查不了怎么办呢逻辑也非常简单通过schedule_delayed_work()等一会再做一次检查即可毕竟一个任务总有被CPU调度出去的时候。总之这里遵从的原则就是不能让一个进程即看到旧的代码又看到新的代码。第六步做清理工作具体由klp_complete_transition()函数完成。这里补充一下对于第五步并不单单只有klp的kworker去做检查还有好几个点比如控制流回到用户态的时候函数exit_to_usermode_loop()这里是直接这里可以思考一下如果被打热补丁的函数就是exit_to_usermode_loop()会怎么样系统进入idle的时候这个时候系统也没事情干了不如趁机检查一下能够过渡一下klp的状态同理这里也可以思考一下如果被打热补丁的函数是do_idle()会怎么样4.19.195代码里似乎没有在schedule()函数里面做检查高版本代码里有个klp_sched_try_switch()函数会在schedule()函数里对prev task做检查。看懂了打热补丁的逻辑基本就能理解热补丁的原理了。