(非常详细的livepatch解析)热补丁技术原理之---3.livepatch原理解析

livepatch是什么?

livepatch是linux内核引入的一个用于支持内核函数热修复的内核特性。通过kpatch-build工具制作的hotfix是可以使用livepatch特性的。通过livepatch机制,可以运行内核在运行过程中动态替换运行的函数,减少因修复系统漏洞而导致的停机时间,从而达到修复问题函数的能力,提高系统的安全性的稳定性。

livepatch与kpatch有什么关系?

其实livepatch与kpatch我认为是一脉相承的。最简单的我们可以看到livepatch的maintainer与kpatch-build的maintainer其实是同一拨人。原本使用的是kpatch.ko的模块加载的方式,但是livepatch提供了一套完整的内核机制,是一个内核特性,与内核兼容性更好,实现的功能更多,性能更好以及安全性有更好的保障。
为什么要使用livepatch?
首先,kpatch core module在linux 5.7+以后就无法编译了。livepatch作为内核特性,这个问题当然就是不存在的。
其次,使用kpatch core module的话,如果修复补丁使用了jump labels或者一些特殊section得话,在模块初始化阶段的顺序问题可能会导致一些很细微的bug。而且,使用livepatch内核特性可以修复更多的内核函数(虽然目前我还没有遇到过,但是这是我跟社区的同行们讨论的结果),一些内核函数基于livepatch特性可以进行修改,但是基于kpatch core module则无法完成。
从性能层面,使用livepatch重定向内核函数要比kpatch.ko要快,如果针对一些小函数的修改,这个对性能的影响可能就会非常的明显。
而一个非常重要的原因就是,kpatch.ko使用了内核的stop_machine机制,而livepatch使用的是modern upstream’s consistency model(这个我还要再研究一下)。对于前者而言,使用的stop_machine是存在明显的延迟的,但是livepatch则是通过一个一个进程来修改修复状态,这不仅仅解决了性能上的延迟问题,而且还是安全的。livepatch的做法对低延迟要求的机器更友好。

livepatch与kpatch在代码中的关系体现

上面我们介绍了livepatch与kpatch其实是一脉相承的,他们其实是同源的,这个我们从代码上也能看出点蛛丝马迹。在kpatch-build的项目中,kmod文件夹下有core以及patch两个文件夹(这两个文件夹是干啥的可以看我上一篇文章)。patch里面有个livepatch-patch-hook.c以及kpatch-patch-hook.c,这两个文件分别是在livepatch以及kpatch场景下用于构建patch使用的。对比两个文件,可以看出,所有的klp_*开头的结构体都是为livepatch服务的,而kpatch_patch_*开头的都是为kpatch_core_module服务的。lfunc是对livepatch服务的,kfunc则是为了kpatch core module服务的。
而在本地的一些数据结构或者函数,往往直接采用patch_*以及直接的函数名(func)。这样的前缀描述的结构体或者没有前缀的函数名就是本地数据结构。
所以,根据变量名以及函数名,我们可以直观地看出来这个函数或者变量是为那种机制服务的了。

livepatch由什么组成

针对livepatch由什么组成的这个问题,我们可以从内核的livepatch的Makefile看起:

# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_LIVEPATCH) += livepatch.o

livepatch-objs := core.o patch.o shadow.o state.o transition.o

上面这个是内核livepatch模块下的Makefile文件,第一行我们看到obj-$(CONFIG_LIVEPATCH)。这个涉及到Kbuild的预发。$(CONFIG_LIVEPATCH)是从内核config file中读取的值,是对livepatch的配置。当CONFIG_LIVEPATCH=y的时候,这里是obj-y,也就是说livepatch会被编译进内核映像。obj-yobj-mobj-nobj-等是kbuild系统用来决定是否编译某个对象文件的变量。obj-y表示总是编译,obj-m表示作为模块编译,obj-nobj-表示不编译。obj-m是一个变量,它用来指定当前的对象需要按照模块来编译。
obj-*
针对obj-n以及obj-,我们没啥好说的,就是不编译。但是obj-y以及obj-m是需要讲讲的。obj-y表示总是编译,需要把当前对象编译进内核映像中,当一个对象被编译进内核映像以后,它会成为内核二进制文件中的一部分也就是说,它在系统启动时就被加载。这意味着这部分代码不能被卸载,除非重新编译和安装内核。这种方式适合那些必须在系统启动时就加载的代码,例如文件系统、网络协议栈等。
对于obj-m的对象文件,这部分对象将会按照模块的方式编译,当一个对象文件被编译成内核模块时,他会产生一个ko文件这个文件可以在系统运行时动态地加载和卸载。这种方式提供了更大的灵活性,因为你可以在不重启系统的情况下添加或删除功能。这种方式适合那些不需要常驻内存,或者只在需要时才加载的代码,例如设备驱动、文件系统等。
所以,当前文件需要按照那种方式编译,可以根据这个对象使用的方法以及使用的场景时机来进行设置。
livepatch包含什么?
从上面的makefile中我们可以看到,livepatch-objs 是由core.o patch.o shadow.o state.o transition.o这五个对象链接而成的,说明了livepatch包含了这些对象所提供的功能。根据以前的历史经验,我们不妨猜测一下这几个对象文件都提供些什么功能:
core:提供的是livepatch的核心流程
patch:提供hotfix加载卸载过程中与patch相关的流程机制功能
shadow:提供的是shadow变量的使用代码
state:提供livepatch使用过程中的状态代码
transition:提供livepatch运行过程中各个事务迁移的方式的相关功能
结合这些猜想,接下来我们将一个个去揭开livepatch的神秘面纱!
这里主要包含了livepatch的几个核心的过程:1、核心流程;2、livepatch状态改变机制;3、livepatch同步机制;4、livepatch的补丁应用机制。但是本文目前会先只介绍livepatch的流程。
为了维护整体的完成性,本文的内容将会很多,请系好你的安全带,我们马上出发!!

klp hotfix插入流程

在逐个文件揭开livepatch的神秘面纱之前,我们不如先从大局上看看,当我们插入一个klp hotfix的时候,是怎么一个流程的呢?
liveatpch hotfix 插入流程

第一步,创建辅助对象函数与重定向结构

第一步的开始,livepatch构建的函数在ELF文件中存储的数据是按照结构体kpatch_patch_func来组织的。
kpatch_patch_func结构体用于描述一个函数在kpatch中的数据结构,里面包含了这个函数符号对应的符号、新旧函数地址等信息,可以认为是kpatch用于管理函数的单位。在这一步中,livepatch-patch-hook需要把这个数据结构转成patch_func结构体。patch_func的定义为:

/**
 1. struct patch_func - scaffolding structure for kpatch_patch_func
 2. @list:	list of patch_func (threaded onto patch_object.funcs)
 3. @kfunc:	array of kpatch_patch_func
 */
struct patch_func {
	struct list_head list;
	struct kpatch_patch_func *kfunc;
};

这个其实可以认为是一个链表节点,patch_func是Node,链接起来以后,里面包含的就是kpatch_patch_func的结构体。结合之前的描述,我们可以知道,这么做的其中一个原因应该是kpatch的设计,patch_开头的,说明是这个文件中引用的变量。因为这里设计到了kpatch与livepatch的数据结构之间的转换,所以用到了这种方式。

klp如何获得每一个修复函数的地址呢?
for (kfunc = __kpatch_funcs;
	     kfunc != __kpatch_funcs_end;
	     kfunc++) {
		ret = patch_add_func_to_object(kfunc);
		if (ret)
			goto out;
	}

__kpatch_funcs是kpatch_patch_func的结构体数组。
extern struct kpatch_patch_func __kpatch_funcs[], __kpatch_funcs_end[];
kfunc是kpatch_patch_func的结构体类型。这个__kpatch_funcs的地址是在kpatch.lds.S文件中通过读取.kpatch.funcs这个section的起始地址可以获得。而这个section的结束地址就是__kpatch_funcs_end。这个获取地址的方式跟kpatch core module中的地址获取的方式是一致的。

把当前处理函数链接到对应的对象上

上一步中,我们获得了遍历当前elf文件中所有被修改的函数的地址。此时,kpatch需要做的事情是把每一个被修改的函数链接到对应的对象上。这个操作在patch_add_func_to_object中完成。
首先,创建patch_func结构体并为其分配内存。现在我们的工作是要把当前函数的地址转化为patch_的过渡变量来操作,所以,这里创建一个patch_func结构体。我们知道,patch_func其实也是用于描述一个函数的,所以,我们只需要把这个结构体的kfunc指向“新函数”(就是elf中发生修改的函数)即可。
寻找这个函数所属的对象
对于kfunc对象,其内存本身就存储了该函数所属的对象名,我们只需要重patch_objects这个链表中找出这个object即可。这里的操作跟kpatch.ko中的操作比较类似,也是当当前的obejct不存在patch_objects这个链表中的时候,认为这个就是一个新修改的对象,进而为此在patch_objects中分配一个位置。
这一步大体的过程我总结如下:
寻找函数所属对象
总之,要理解这一步,就是要明白这一步最终要的点就是构建过渡变量;然后根据一个object中会有多个func的关系,构建一个被修改的对象链表。

把回调函数链接到对象上

从kpatch.ko过来的同学们应该知道回调函数这个东西。在打patch的时候,用户可以在patch中通过定义KPATCH_POST_PATCH_CALLBACK这样的宏定义来定义需要调用的回调函数。这个回调函数是在用户打patch的时候,patch中定义的。例如:

#include "kpatch-macros.h"
static int pre_patch_function_selfdef(patch_object *obj)
{
  printk("pre patch!");
}
KPATCH_PRE_PATCH_CALLBACK(pre_patch_function_selfdef)

这样,kpatch进行宏定义展开的时候,就会把这个函数给放到.kpatch.callbacks.pre_patch的section中。一个object可以有一个过程的回调函数(pre_patchpost_patchpre_unpatchpost_unpatch是四个过程)。所以,一个.kpatch.callbacks.pre_patch可以有多个object的回调函数,我把我的理解画成了图:
回调函数的组织形式
然后,livepatch就会从patch_objects中根据回调函数的给出的object name找出对应的patch_object,并把这个函数给挂到这个patch_object的(pre/post)_(un)patch函数指针类型的结构体成员上。
通过这种方法,把post_patch_callback\pre_patch_callback\pre_unpatch_callback\post_unptch_callback给挂到对应的成员上。
每个hotfix都会这么操作的吗?
这个可不一定,如果patch中没有定义对应的回调函数,那就不会调用了。

第二步,创建klp_patch数据结构以及其友元结构

结合前面的解析,或许不少同学以及知道为什么需要创建这个临时数据结构了。在kpatch中,为了向前兼容,在kpatch中构建的使用的数据结构还是kpatch的数据结构。但是在livepatch中有klp自己的数据结构,数据结构之间存在差异。所以,在livepatch-patch-hook中,需要做kpatch到livepatch的数据结构的转换,不然这个hotfix做出来了以后,无法在livepatch的系统上加载的。所以,这一步就要开始创建klp_patch的数据结构了。

创建klp_patch与klp_object

关于klp相关数据结构的定义,在<linux/livepatch.h>中。
klp_patch的解析:

struct klp_patch {
	/* external */
	struct module *mod; // 用于描述当前这个hotfix的模块
	struct klp_object *objs; // 这是用于描述对象的
	struct klp_state *states; // 这个是用于指示系统被livepatch更改的状态,包含了修改的版本信息
	bool replace; // 一个标志,用于指示是否需要取代所有激活的patches

	/* internal */
	struct list_head list; // 这个是全局激活的patch的链表节点
	struct kobject kobj; // 这个是挂在/sys/kernel/livepatch下的kobj节点
	struct list_head obj_list; // 这个是当前patch的所属object的入口
	bool enabled; // 指示当前patch是否使用了
	bool forced; // 这是一个只写属性,用于强制推进klp状态的
	struct work_struct free_work; //从工作队列上下文中清除的patch
	struct completion finish; // 这个是用于等待,等待到当前模块能够被安全卸载
};

klp_patch中,同学们可以这么去理解,一个patch中我们可以修改多个.c文件对吧,既然一个.c文件对应一个object,那是不是一个patch就是可以对应多个object呢?所以,我们从这个klp_patch中可以看到,这个patch结构体本身下面有一个包含的objects链表,用于记录属于这个patch修改的对象的。一个系统中也可以打多个patch,所以,klp_patch本身也是一个链表。而klp_patch中internal后面的内容,这里就先不展开了,后面大家会慢慢体会到的。
基于这种树状结构的想法,我们也不难理解klp_object的结构体定义。

struct klp_object {
	/* external */
	const char *name; // 这个对象的名称,一般是这个object或者是vmlinux的,如果是vmlinux的话,
	这个是NULL
	struct klp_func *funcs; // 当前object需要修复的新函数的链表
	struct klp_callbacks callbacks; // 这个对象的回调函数结构体,里面包含了所有的回调函数

	/* internal */
	struct kobject kobj; // 挂载在sysfs livepatch所属patch下的节点
	struct list_head func_list; // 这个就是需要打的函数的链表
	struct list_head node; // 这个是上面klp_patch的obj_list的节点
	struct module *mod; // 这个对象所属的模块,如果对象属于内核内部的vmlinux的,就是NULL
	bool dynamic; // nop函数的临时对象;动态分配的
	bool patched; // 这个object是否成功添加到了klp_ops链表上
};

创建好了这两个数据结构以后,livepatch就会遍历patch_objects中的每一个对象,把其中的每一个对象中的数据转到klp_object对象中的数据。把里面每一个object的函数的信息、每一个重定向信息全部转移到新的klp_object对象中,完成对klp数据结构的转换。

第三步,删除辅助数据结构

首先,经过前面的介绍,大家是不是都知道了这个辅助数据结构是干啥用的呢?是的,之所以会出现这个数据结构的转换,是因为在kpatch中,做了向前兼容的工作。kpatch在早起使用的是kpatch core module,也就是kpatch.ko。但是使用kpatch构建热补丁的时候,需要支持livepatch的构建,构建出来的热补丁是需要按照livepatch的数据结构体来组织的,但是kpatch默认又是用kpatch的格式组织的。所以,maintainer们就想出了这个方法,通过辅助数据结构,把kpatch的数据结构转化成livepatch可以识别的数据结构。
在这一步中,操作不是很多,每一个对象开始,删除这个对象下面的每一个函数。把这些数据结构从链表中删除并释放其内存。后面,针对kernel livepatch,我除了会以livepatch来描述以外,还会以klp来进行称呼。

klp_enable_patch

klp_enable_patch是开始使能一个patch的开始。它的传入参数是klp_patch的结构体。在hotfix插入的时候,会调用patch_init函数,然后这个模块插入的过程中会把kpatch的数据结构转换成klp的数据结构,在进行klp_enable_patch的时候,以及把当前的kpatch转为klp_patch了。
klp_enbale_patch是一个导出符号。这个函数的作用是初始化与这个patch相关联的数据结构,创建sysfs接口,生成所需的符号查找表以及代码的重定向,并且把涉及修复的函数向ftrace中注册。正是因为这个函数是一个导出符号,其他的模块在编写的时候如果想接入到livepatch机制当中的话,也可以通过在module_init函数里面使用klp_enable_patch这个函数。
klp_enable_patch前面需要检查一下livepatch是否被初始化了。这个做法非常的简单,就是判断一下livepatch的kobject是否已经存在,如果不存在,说明了livepatch还没有初始化,此时一起跟livepatch有关的操作都是非法的。
在正式执行enable patch的操作之前,需要先获取一个Mutex锁klp_mutex。
klp_mutex
klp_mutex是一个互斥锁,是一把大锁。它是用于对klp数据的顺序访问控制的锁。所有需要访问klp相关的变量或者结构体都需要在这个互斥锁的保护下访问。除了在klp_ftrace_handler();klp_update_patch_state();__klp_sched_try_switch()这些函数中。

检查当前patch是否与当前系统兼容

在前面的代码中,我们看到了klp_patch结构体中有一个klp_state的结构体,这个是用来指示系统的klp状态的。klp_state的描述如下:

struct klp_state {
    unsigned long id;  //这是系统状态的id
    unsigned int version; // 当前改变的版本
    void *data; // 自定义的数据
};

要检查当前的patch与当前系统是否兼容,需要遍历klp中的每一个激活的patch,根据patch中的state的id,可以获得唯一的一个状态。
在检查当前patch是否与当前系统状态兼容,需要获得每一个patch中的状态,如果旧的patch里面存在一个状态state跟当前patch的状态是一样的,说明这两个patch的状态上出现了冲突。但是出现了冲突不代表马上就是失败。如果当前patch的版本比旧patch的版本大,说明这个patch更新,即使状态一样,也可以进行下去替换。如果状态出现了冲突,新patch的版本反而更小那就不行了。
此外,如果没有一样的状态呢?没有一样的状态的话,那就要看看这个patch是否设置了replace。replace就是类似于之前在kpatch.ko中介绍的那样,如果开启了replace,会把之前所有的active的patch给disable掉,再把自己enable上去,就像自己把他们都替换了一样。如果没有一样的状态的话,那就需要这个patch这个patch没有开启replace选项,因为一个开启了replace的patch需要对所有已经处于修改状态的patch负责。如果你开启了replace,但是找不到对应的state,这是一种非法状态的。

初始化该patch相关的数据结构

我们知道,现在需要处理的是在数据结构已经是klp的数据结构了。这个patch转换了以后,很多klp相关的数据还没有关联上,所以在正式进入klp的过程之前,需要对这个数据结构里面的一些数据进行初始化。首先,在klp_patch中,需要先初始化这个patch中的链表节点以及用于描述这个patch所属的object的链表。
然后,把这个需要把livepatch中需要的sysfs节点的操作对象初始化到这个结构体下的kobj对象中。初始化该patch的free_work指针到klp_free_patch_work_fn函数上

klp_free_patch_work_fn

在初始化patch相关的数据结构的时候,会调用INIT_WORK来初始化patch->free_work。free_work是一个work_struct的结构体。INIT_WORK是内核中的一个宏,用于初始化工作队列中的工作项。在livepach中,INIT_WORK的方式为:

INIT_WORK(&patch->free_work, klp_free_patch_work_fn);

其中,&patch->free_work是工作项,klp_free_patch_work_fn是当工作队列被调度时需要调用的函数。
第一个参数是一个指向工作队列项的指针,因为在新版本的kernel中,INIT_WORK被优化得只剩下两个参数了,所以需要把工作项塞到数据对象当中。当运行这行代码的时候,INIT_WORK会初始化patch->free_work工作队列项,并设置其对应的处理函数为klp_free_patch_work_fn函数。
这个处理函数会把当前的patch从sysfs节点上摘掉。然后调用wait_for_completion函数阻塞直到patch的状态变为finish。
最后初始化这个patch下关联的所有的object链表以及func链表

初始化这个patch

前面一步指示初始化这个patch的数据结构,我们可以只是在做一些前摇工作,成为“初始化的前摇”。在前摇结束了以后,就正式初始化这个patch了。初始化这个patch所需要的工作就是把这个patch正式添加到klp流程当中。这块的代码做的操作主要是一下几个操作:
在sysfs中patch、object、function的关系

  1. 把该patch创建sysfs节点并挂进livepatch节点中;
  2. 为涉及改变的func以及object逐级创建sysfs节点并挂载在所属对象下;
  3. 把当前patch挂在klp_patches链表表尾。

这里需要注意有一个技术点,就是针对klp_replace的这种情况需要特殊处理。
在klp_replace下,需要把旧的函数设置为nop。具体是通过遍历已经存在的patch,找出已经存在的patch中是否存在修改过的函数在新的patch里面也是存在的,如果存在一个新patch中且在旧patch中的函数,那么就要把这个旧函数的名字记录下来的同时,在新函数中设置nop成员,标志这个旧的函数是需要设置nop的。
完成了patch的初始化以后,就要开始进行该patch的使能了。

__klp_enable_patch

首先,在开始介绍livepatch是如何使能一个patch的时候,我们需要先了解一个变量:klp_transition_patch。
klp_transition_patch是一个klp_patch类型的结构体。这个是livepatch用于记录处于transition过程中的一个重要的结构体,用于存储当前正在处于操作当中的patch。根据这个klp_transition_patch,可以实现livepatch的流程状态的检查以及同步等操作。这个机制我们也会在后续介绍。
所以,在正式enable这个patch之前,需要先初始化livepatch的事物状态,因为klp transition是livepatch用于处理同步的流程逻辑,在正式使能一个patch之前需要先初始化livepatch的状态。
livepatch初始化状态
从上面的抽象出来的这个图中,初始化livepatch的事务状态可以总结成两句话:1、初始化每个进程的初始化状态;2、设置当前patch中每个待修补函数的事务状态。
看过前面kpatch.ko的同学们应该会有印象,就是在我们正式把patch应用之前,都需要先调用我们的回调函数(如果有定义的话)。livepatch中也是一样的,在livepatch-patch-hook中,制作热补丁的时候,我们就把每一个对象中定义的callback函数写到了对应的section中。在加载这个patch的时候,需要对每一个object直接调用其回调函数。
当这个object的回调函数成功执行以后,livepatch就会进入正式patch这个对象的流程。

livepatch是如何patch一个对象的?

其实这个问题的核心并不是如何patch一个对象,因为我们知道,livepatch中patch、object以及func是一个树状结构。patch一个对象不是核心,其核心在于patch对象中的func。
patch对象的方法就是:遍历每一个patch下的object,把object中的每一个func给应用上。

klp_patch_func

klp_func这个结构体中包含了livepatch对函数进行操作过程中需要的所有信息。
old_name:这个是保存了需要修复的那个函数的名字;
new_func:是一个指针类型的函数,这个指针指向我们新的函数地址;
old_sympos:是一个long类型的成员,这个是一个可选项。这里这个成员的目的在与解决在livepatch中可能出现重复的object名的问题。如果这个值为0,说明了这个函数符号在符号表中是唯一的,如果这个值为0,但是在符号表中又不是唯一的,说明打patch失败了;如果这个值大于0,则需要使用所属object在kallsyms中提供的符号;
old_func:这是一个函数指针,指向准备被patch的那个函数的地址;
kobj:是一个kobject类型的成员,这个是用于挂载在object在sysfs中的接口下的node;
node:这个容易更下面的成员stack_node弄混。他们都是链表中的节点,但是node是在klp_object中func_list的节点,是用于记录函数的链表节点;但是stack_node则是klp_ops中的节点,klp_ops链表维护的是不同函数ftrace ops的节点;
(old)new_size:分别代表了(旧)新函数的大小;
nop:bool成员,指示是否临时使用旧函数逻辑(原始代码)
patched:bool成员,这个标记着当前这个函数是不是已经打上了,并挂载在klp_op链表上了
transition:指示当前这个函数是否在事务当中。什么是事务中的状态?就是当前函数正在进行操作,或者在被enable的过程中;或者是在disable的过程中。
下面看看一个函数成员指示其变化吧:
成员状态的指示
上面的这个图就告诉了你们patch以及transition两个值结合起来代表当前函数处于什么状态,以及其中间状态的含义
那么在patch一个函数的时候,当前函数的klp_func中存储了旧函数的信息。我们根据旧函数的信息,查找旧函数在klp_ops链表上的节点。如果找到了,说明了这个函数之前是打过hotfix的。在这种情况下,只需要把当前的当前func结构体的stack挂到klp_ops的rcu链表上即可(这里有什么用呢?我们后面展开)。
如果找不到这个节点呢?说明这个函数之前是没有打过patch的,这一次是首次修改。那么这个时候就需要为这个函数创建一个新的ops节点。首先,我们会获取这个func的old_func地址,这个地址认为是指向旧函数地址的。然后调用ftrace_location获取这个旧函数的ftrace点。
获得旧函数的ftrace点以后,我们就可以为ops结构体申请内核内存。在介绍下面的工作之前,我们需要先了解一下klp_ops结构体的成员:

struct klp_ops {
    struct list_head node;
    struct list_head func_stack;
    struct ftrace_ops fops;
};

其中的node是全局klp_ops链表的节点;func_stack可以理解这个是一个栈,这个节点则是头部,这个栈放着的是klp_func的栈,在这个栈的栈顶函数才是被激活的函数;fops则是用于注册ftrace操作的ftrace_ops结构体。
ftrace_ops包含了func指针,这个是目标函数调用时调用的函数;在创建的klp_ops中fops中的func指针式非常重要的,这个涉及到了函数的替换操作;其次就是klp_ops中fops->flags,这个flags标注用于设置ftrace的操作。
设置好了klp_ops中的操作以后,这个创建的ops就会被挂载上klp_ops链表中。挂载上去以后,livepatch就能正式感知到这个函数了。
挂载上去以后,就要用ftrace_set_filter_ip设置这个函数的ftrace点开启过滤并调用register_ftrace_function将这个新函数的ops注册用于ftrace的函数分析。

klp_ftrace_handler

上面的过程基本覆盖了livepatch中patch一个函数的过程中主要的工作。但是不知道你们是否注意到了一个点,那就是klp_ftrace_handler。前面说这个函数很重要,到底哪里重要了呢,以及有什么特殊的呢?
首先,这个函数被设置为了ftrace_ops中的func,就是当内核中有人调用了我们注册的函数的时候,ftrace会先调用func指向的函数进行处理。也就是说,我们现在对FuncA打补丁,那么这个时候,当内核中有人调用FuncA这个函数的时候,ftrace会劫持它,并直接调用func进行处理。这里相当于把函数“拐跑了”。
klp_ftrace_handler必须声明为notrace,因为设定为ftrace_ops->func的函数必须是notrace,否则在ftrace中会导致循环引用的问题。
总的来说,klp_ftrace_handler做的工作就是:通过内存屏障机制,获取最新版本函数,并进行跳转。
klp_ftrace_handler里面有些有趣的操作,现在我们就打开来解析它:

  1. 获取生效的函数:前面我们介绍了,在klp_ops结构体中,有一个func_stack是模拟一个栈的结构,只有在栈顶的那个节点才是当前生效的那个函数。所以,这个函数里面,为了确保函数切换过程顺利,这里会调用ftrace_test_recursion_trylock函数来禁止抢占。禁止抢占以后,就会从当前函数的klp_ops中获取func_stack中的第一个节点作为生效函数的节点。
  2. 设置内存读屏障。这里其实有两次内存读屏障的设置。第一次的内存读屏障是用来保障klp_ops->func_stack和func->transition的读顺序的。为什么在乎这个顺序呢?因为klp_ops->func_stack的顶部节点是当前生效的函数,如果func->transition的状态发生了修改,说明了这个func_stack的栈顶可能是不可靠的,函数还在处理当中。一般来说,处于栈顶节点的函数的func->transition是不为true的,如果发现栈顶元素为true的话,那就要判断一下当前这个栈顶节点是准备做什么操作了,这里就移步到3操作。否则,直接跳到4步。
  3. 走到这一步说明了栈顶节点函数的transition==true。说明当前函数正在被处理。此时我们需要获取current进程,查看一下current进程的patch_state。如果current进程的patch_state是KLP_UNPATCHED,说明当前的这个函数是需要被卸载的,既然是一个需要被卸载的函数,那此时需要跳转的函数就不是栈顶的函数了,而是需要换成栈顶下的那个函数;如果这个栈只有一个节点呢?那就需要把跳转的函数给换成原始的函数了。
  4. 拿到了函数节点以后,ftrace_regs_set_instruction_pointer函数就会修改跳转函数的地址为需要跳转的函数地址,完成函数的跳转。并在完成了寄存器的值的设置以后开启抢占,恢复保护的场景。

那么设置函数跳转的过程我们可以总结成下面这样:
在这里插入图片描述
的,那现在我们回到前面,前面我们提到过,如果发现这个函数是打过hotfix的,只需要把当前的当前func结构体的stack挂到klp_ops的rcu链表上即可,这是为什么呢?
现在应该知道原因了吧。因为在调用这个函数符号的时候,ftrace会先调用klp_ftrace_handler进行处理。然后检查这个函数的klp_ops节点是否存在,如果存在,那就从这个klp_ops中的func_stack函数栈上找出栈顶的那个函数来实际执行。所以,如果这个函数是打过hotfix的,那么这个klp_ops会存在,直接把这个函数挂载func_stack的栈顶,那么klp_ftrace_handler就会默认栈顶是最新的,拿出来实际执行。

开始进程切换事务

上面的过程就是把每个object中的每个函数生成对应的klp_ops结构体并挂载到livepatch全局ops链表klp_ops上,此时livepatch已经对这些新打上去的函数已经可以感知了。来到这一步以后,就是把livepatch的事务状态转成对应的状态,这样一来,每个进程就可以开始进行切换了。
需要注意的一个点是,内核中的task_struct是用来描述进程的结构体。在这个结构体中,有一个成员叫patch_state,这个就是用来指示当前进程在livepatch中的状态。
在这一步中,首先对进程链表进行保护,获取全局进程链表的保护锁tasklist_lock。然后,设置每一个线程的标注位为TIF_PATCH_PENDING,表示当前线程的状态是等待livepatch更新的状态。
这里指示对每一个进程以及线程进行了设置,但是为了完全更新,livepatch还需要获取每一个CPU的idle_task进程,把每一个idle_task进程的状态设置更新。
基本上klp_start_transition以后,我们的这个patch就可以设置为enabled了,因为接下来的操作就很只有等各个进程和线程完成其切换即可。

进程切换 klp_try_complete_transition

在尝试完成klp的事务状态的时候,需要对每一个进程进行操作,尝试去切换每一个进程的状态。
在尝试去切换进程的状态的时候,我们先判断一下当前处理的进程是不是current进程,如果当前处理的进程是current进程且当前进程正在CPU上运行的话,那么我们就不能处理它,否则会搞出严重的后果的。

task == current

如果没有问题,那么调用stack_trace_save_tsk_reliable保存当前函数的栈,然后遍历我们的hotfix中的每一个object下的每一个function,检查我们的function是否落在函数的执行栈上面。
但是我们检查当前进程是否切换安全也不是只需要判断我们的function是否有落在函数栈就ok的。我们也是需要根据情况进行处理。livepatch需要根据当前的处理的patch的状态来选定需要判断的函数地址。
看下图:
在这里插入图片描述
如果目标状态是KLP_UNPATCHED的话,说明当前的任务是要卸载热补丁。那么我们要关心的就是当前处理的patch中的函数是否有在被调用。我们需要理解的是,func->new_func是我们替代以后,新的函数地址,也就是hotfix打上了以后,函数调用的地址;而func->old_func则是我们要替代的目标函数的地址。所以,我们需要检查这个函数的new_func是否落在了当前进程的运行栈里面。如果在,说明正在被当前进程call,不能被卸载。

如果目标状态是KLP_PATCHED的话,说明当前的任务就是要加载这个热补丁。既然要加载这个热补丁,我们当然就是要看func_stack中的第二个节点的new_func地址是否在call_stack上了。为什么呢?别忘了,到这一步的时候,我们已经把我们当前的函数节点挂载在了func_stack上了。如果现在func_stack上只有一个节点,说明了这个系统中运行的函数是原始的函数版本,因为这个func_stack就是我们现在挂上去的这个函数;如果这个func_stack上有多个节点,说明了这个函数被打了多次修复,那么此时我们需要拿出栈顶第二个节点,因为第一个节点是我们自己刚刚挂上去的,对于系统而言,可见的运行中的“栈顶”节点现在变成了第二个节点。然后再判断这个节点的new_addr是否落在了task的call_stack对应的区间上判断这个函数在当前task上是否被调用。

如果都是不存在的话,那么这个进程的flag的标志为就可以被清除而且这个task的patch_state可以设置为target_patch_state的结束状态了。因为如果当前函数没有call过这个目标函数的话,后面这个task如果要call这个函数的时候,就会被ftrace拐跑到新的patch上面去了。相当于已经生效了。

task != current

当task不为current的时候,其实校验的过程跟上面的还是一样的,唯一不同的是对task需要固定和保护。
因为我们需要获得当前这个task的运行栈,既然当前的task并不是在运行中的,我们就要确保我们在检验的过程中她不会跑起来。我们不能排除这个进程在上面判断的时候不是运行的,但是我们进入了分支处理的时候却被调度执行了。所以,我们需要对task再一次判断,对于RUNNING或者WAKING中的task,我们需要获取这个进程的rq lock。
对进程上锁了以后,没有执行的进程可能有下面的四种状态的其中一种:

1、阻塞;2、唤醒;3、调度;4、运行中。

但是我们把它的这些状态全部推迟了。直到我们做完校验以后才会释放。
类似的,针对CPU中的idle_task执行类似的校验。

如果校验不通过怎么办?

其实很多情况下,特别是针对一些热点函数的修改,存在某个task的call_stack上是一个概率很大的时期。所以,当检验不通过的时候,livepatch会向所有的task处于PATCH_PENDING的进程发送假的信号,尝试去唤醒这个进程。然后,调用函数schedule_delayed_work函数设置一段时间以后,再一次调度执行klp_transition_work这个函数。klp_transition_work会判断当前livepatch状态是否是处于事务当中,如果发现livepatch的状态处于事务当中,说明现在还有进程task的状态没有完成切换,进而调用klp_try_complete_transition函数来对没有改变状态的进程尝试改变。

所以,如果我们往klp_try_transition函数中加入打印,如果有进程一直没有完成状态切换,我们可以在demsg中看到内核会定期调用这个函数,就是定期检查看看是否能够把剩余的进程的状态完成切换。

结束事务状态 klp_complete_transition

前面如果出现了校验不通过的情况,livepatch会在设置了delayed_work以后返回这个函数,拖出try_complete_transition的过程。如果我们patch得不是一个非常热补丁的函数,很顺利的所有的进程都没有call到这个函数,所有校验都通过了,那么这个时候livepatch就会结束当前的事务状态了。

如果我们打的这个hotfix设置了livepatch的replace标志的话,说明了这个patch自己是包含了前面所有的patch的(自己以为罢了)。此时livepatch对它会非常的信任,直接一股脑的把livepatch上剩下的所有的old_patch涉及的object全部给unpatch掉了。

然后,livepatch就会设置每一个func的transition的状态为false,表示已经不是在transition的过程中了。同时,别忘了前面我们给所有的进程,包括CPU的idle_taks的patch_state都设置了klp_target_state的状态。现在我们的事务状态要结束了,是不是要恢复每一个进程的原始状态呢?当然是了,所以需要设置每一个进程的状态为KLP_UNDEFINED。

最后,既然所有的事情已经做完了,我们patch得过程就结束了。所以,这个时候还有最后的一项工作需要做的,那就是给我们的这个patch中的每一个object调用post回调函数。执行完回调函数,这个过程才是真正的结束了。恢复klp_target_state为初始状态KLP_UNDEFINED以及设置klp_transition_patch为NULL,表示现在没有patch在事务当中。

如果在事务处理过程中出现了异常,livepatch怎么处理?

果在事务处理过程中出现了异常,例如每个object打patch之前调用pre_patch_callback出现了异常或者object在打patch过程中,出现了异常,那么这个时候livepatch就会会退我们先前的操作,设置我们的klp_target_state为KLP_UNPATCHED的卸载状态,然后调用klp_complete_transition函数终结事务处理过程。

好的,接下来,说了这么多,我们来总结一下livepatch的在enable一个patch的过程吧:

livepatch加载过程

klp hotfix卸载流程

既然有hotfix的加载,自然就有hotfix的卸载。hotfix的卸载相当于是hotfix加载的逆过程。这块我们仍然从一个完整的过程开始讲解。但是跟加载一个hotfix稍微有点不同的是,我们在卸载一个hotfix的时候,往往不是直接rmmod livepatch.ko就完事的,这个时候这个ko往往是在被占用的,rmmod是rmove不掉的,需要先对其disable然后在rmmod。

disable一个livepatch

disable一个hotfix的方法是echo 0 > /sys/kernel/livepatch/hotfix***/enabled完成的。我们来看看这条命令输入以后,执行的什么操作。
在我们echo 0 到了enabled以后,这个是一个sysfs的接口操作,此时系统会调用enabled_store函数进行处理。
在内核中有个非常好用的函数:container_of。这个函数可以通过成员获取其结构体。
由于kobject是klp_patch中的成员,我们是通过sysfs接口来操作的,这样可以直接获得其kobject。然后利用container_of可以直接获得这个kobject对应的klp_patch结构体得到这个patch的完整描述。
这里逻辑操作其实挺有趣的,当你触发了enabled的修改的时候,必须要求你的输入状态是相反的状态,不然你就是同状态修复,这当然是不被允许的。
但是livepatch这里却不允许你将一个disabled的livepatch给重新enabled。livepatch为什么要这样做呢?

livepatch为什么不允许一个disabled的livepatch hotfix给重新enable呢?
livepatch曾经引入了一个特性,把klp_register_patch的注册过程给删除了,简化了公共API,同时klp_replace让一个patch可以作为累积patch把前面的其他patch都替换掉了。在这种情况下,我们是不清楚如果对其中前面的一个disabled的patch给重新enable的话是否是安全的。而且livepatch简化了API,把klp_register_patch的操作给搬到了klp_enable_patch里面去了。对于一个disabled的patch来说,它的函数不可能在栈顶上了,也是没有保留的必要了,所以,当我们在echo 0 到enabled的时候,不仅仅会disable掉这个hotfix,还会直接把这个patch的sysfs节点摘除,这样用户就无法对其再次enable了。

__klp_disable_patch

当开始对一个patch卸载的时候,我们首先要判断一下klp_transition_patch是否为空的。如果klp_transition_patch是空的,说明了livepatch当前情况下是空闲的;如果klp_transition_patch不为空,说明了当前有patch在事务当中,无法承接第二个patch进行处理,所以此时livepatch是不允许改操作的。

如果在进行patch卸载的是否,livepatch没有处于在事务当中,卸载过程则可以继续下去。所以,livepatch会使用操作状态KLP_UNPATCH来初始化livepatch的事务。前面提到了初始化livepatch事务的主要功能是设置指示当前处理中的patch对象klp_transition_patch以及对各个进程的patch_state状态进行设置。

初始化事务状态了以后,对每个对象调用所属的pre_unpatch_callback函数用于前卸载函数的调用。然后,livepatch就会开始事务的进行。

经过了我们前面的介绍,事务的进行我们可以很简单的概括成:设置每个进程的进程flag为排队中的标志位。
每个进程经过了这一步以后,基本是都可以设置为排队中的状态的,因为使用标识位标记还没有涉及到太多的安全性检查。此时patch的状态就可以认为是disabled的。

到了这一步了也是最后一步了,那就是对每一个进程尝试结束进程的事务状态。也是跟enabled过程类似,在尝试disable的过程中,需要对每一个进程进行操作,需要检查每一个进程的call_stack判断是否安全,针对不是正在运行中的进程,需要对齐上锁保护,固定调用栈以便获得可靠的调用栈结果。针对安全的进程,直接更新进程patch_state;针对不安全的进程,等待一段时间,重新唤醒更新函数,尝试检查并变更没有成功发生改变的函数状态,直到最后所有的进程状态成功发生改变。

klp_reverse_transition

klp_reverse_transition函数也是在enabled_store中调用了。这个函数可能很好的处理取消已经存在的处于加载或者卸载过程中的状态,特别是被一些进程卡住导致状态无法继续推进的场景下。

那么klp_reverse_transition是如何取消事务状态的呢?

首先,对于每一个进程,设置进程flag为进程等待状态。之所以要这么设置,是因为既然我们现在是要取消事务状态,那说明我们当前的patch_state是已经进入了某种状态,而且我们的进程或多或少已经被进行了设置的。所以,我们需要对所有的进程回退操作,因此,需要对所有的进程状态进行设置。

强制事务流程

在一些特殊的情况下,某些进程可能会一直被stuck住,卡住不动了。这个时候我们的livepatch事务也是会停止在一个地方不断重试完成transition。livepatch考虑到了这种情况,kobject节点中,为当前的patch添加了一个force接口。我们只需要echo 1 > force,即可强迫livepatch完成其事务状态。

当我们echo 1 > force的时候,livepatch会把剩下的所有处于等待过程中的task的等待状态全部清除,并把那些处于等待状态的进程全部设置为目标的patch状态。这个patch中有一个force成员,是用来指示当前patch是否是强制执行事务流程的。

如果这个patch被设置了强制事务流程,而且这个patch是一个replace的patch,说明了这个patch后面需要对其他剩余patch可能都会有操作。这种情况下,livepatch就会给其他的patch的force状态设置为true。
当其他patch的force状态被设置为true的,当其他事务处理到了这些patch时,自然也会强制这些patch执行剩余事务,从而达到replace功能的一致性。

总结

结合上面的解析,相信大家对livepatch是如何工作的原理比较清晰了。livepatch中,打在系统上的函数由一个全局链表klp_ops维护,每一个klp_ops中的节点就是一个函数,里面的func_stack是函数栈,可以存放多个该函数的不同版本,但只有栈顶的那个版本才是生效的。
livepatch中引入了transition的概念,结合transition,实现了livepatch中函数的替换达到进程级别,对不同进程逐个更新,与内核中的一致性模型保持一致,也解决了kpatch core module中的stop_machine带来的性能抖动问题,更适合在低延迟高性能机器上使用。
livepatch中实现的patch_state用于指示在livepatch中加载的热补丁的处理状态,可以更好处理patch运行中的异常情况。总的说来,livepatch作为一个内核特性,很大程度上克服了kpatch core module的缺陷。这也是为什么社区打算全部往livepatch上迁移的原因。

  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
原始的ucm-0822-10.bag数据集是一个采集自传感器的ROS bag文件。ROS(机器人操作系统)是一个灵活的、开源的机器人开发平台,通过使用ROS bag文件,可以记录和回放传感器数据。 要解析这个数据集,首先需要安装ROS,并使用ROS提供的工具包进行处理。下面是一个解析raw-ucm-0822-10.bag数据集的步骤: 1. 安装ROS:根据自己的操作系统版本,下载并安装ROS。可以选择ROS Kinetic、ROS Melodic等稳定版本。 2. 创建ROS工作空间:在终端中运行以下命令创建和设置ROS工作空间: ``` mkdir -p ~/catkin_ws/src cd ~/catkin_ws/ catkin_make ``` 3. 将raw-ucm-0822-10.bag文件复制到ROS的数据目录下: ``` cp <path_to_raw-ucm-0822-10.bag> ~/catkin_ws/src ``` 4. 运行ROS节点:在终端中运行以下命令,启动ROS节点,并解析raw-ucm-0822-10.bag数据集: ``` roscore ``` 打开新的终端窗口,运行以下命令,解析数据集: ``` rosbag play ~/catkin_ws/src/raw-ucm-0822-10.bag ``` ROS节点将开始解析数据集,并将传感器数据发布到相应的主题(topics)上。 5. 监听话题(topics):可以使用`rostopic list`命令查看当前发布的主题,然后使用`rostopic echo <topic_name>`命令监听特定主题的数据。 ``` rostopic list ``` ``` rostopic echo <topic_name> ``` 通过监听主题,可以获取到传感器数据的详细信息,如激光雷达数据、相机图像等。 通过以上步骤,我们可以解析raw-ucm-0822-10.bag数据集,并获取传感器数据的相关信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值