(全网最细的kpatch流程解析)探秘热补丁技术原理之---2.kpatch.ko

kpatch.ko是什么?

在早期的内核版本中,当时没有引入livepatch的机制。然而热补丁技术当时就已经有了。既然内核当时还没有提供livepatch技术,那么如何给热补丁技术提供其运行机制呢?这个就是kpatch.ko需要做的事情了。kpatch.ko是一个模块,插入内核以后为内核提供热补丁加载、卸载等一些列操作以及安全保障机制。
kpatch.ko只是其最终形态。其实在kpatch项目中,它存放在kmod文件夹下。在kmod文件夹下,两个文件夹分别为core以及patch,其中core文件夹存放的就是kpatch.ko的源代码。而patch文件夹下存放的就是与热补丁机制相关的辅助代码,为制作出来的热补丁提供相应机制支持的地方。
下面我们将从core以及patch两个文件夹开始,介绍里面各个文件的作用以及其机制。
在正式开始分别看这两块地方的时候,我们先看看kmod下的Makfile:

include ../Makefile.inc
KPATCH_BUILD ?= /lib/modules/$(shell uname -r)/build
KERNELRELEASE := $(lastword $(subst /, , $(dir $(KPATCH_BUILD))))

all: clean
ifeq ($(BUILDMOD),yes)
    $(MAKE) -C core
endif

install:
ifeq ($(BUILDMOD),yes)
    $(INSTALL) -d $(MODULESDIR)/$(KERNELRELEASE)
    $(INSTALL) -m 644 core/kpatch.ko $(MODULESDIR)/$(KERNELRELEASE)
    $(INSTALL) -m 644 core/Module.symvers $(MODULESDIR)/$(KERNELRELEASE)
endif
    $(INSTALL) -d $(DATADIR)/patch
    $(INSTALL) -m 644 patch/* $(DATADIR)/patch

uninstall:
ifeq ($(BUILDMOD),yes)
    $(RM) -R $(MODULESDIR)
endif
    $(RM) -R $(DATADIR)

clean:
ifeq ($(BUILDMOD),yes)
    $(MAKE) -C core clean
endif

从Makefile中可以看到,如果在构建的时候,开启了BUILDMOD=yes的话,那么就会构建core。
针对make install的时候,依靠是否开启了BUILDMOD来决定在安装的时候是否需要把kpatch.ko安装台机器上。所以,根据这个逻辑,我们可以弄清楚,kpatch中对core描述的"核心"就是kpatch core module,提供kpatch热补丁机制的核心。
我们来看看kpatch项目中的kmod模块下的文件

kmod-|
	 |-> Makefile
     |-> core -> |
     |           |-> Makefile
     |           |-> core.c
     |           |-> kpatch.h
     |           |-> shadow.c
     |
     |-> patch-> |
                 |-> Makefile
                 |-> kpatch-patch-hook.c
                 |-> kpatch-syscall.h
                 |-> kpatch.lds.S
                 |-> patch-hook.c
                 |-> kpatch-macros.h
                 |-> kpatch-patch.h
                 |-> kpatch.h
                 |-> livepatch-patch-hook.c

总的来说,core提供的代码是kpatch.ko的代码,这个模块在livepatch不存在的前提下提供热补丁机制底座;patch提供的代码是制作一个热补丁ko的过程中,与热补丁机制结合过程的接口函数的定义以及在制作过程中对热补丁机制接口代码的加入。
接下来,我们将从两个模块分别来分析kpatch.ko提供热补丁的机制以及如何给一个制作出来的diff文件加入适配热补丁的"接口"

core

在core文件夹中,一共有三个文件:core.c,kpatch.h以及shadow.c
kpatch.h中定义的是kpatch流程中的关键的数据结构以及导出函数。core.c中是kpatch流程具体的实现代码。shadow.c中实现的是kpatch流程中对shadow变量的具体实现方法。

什么是shadow变量?

shadow变量允许用户对内核中的数据结构增加新的成员变量。打个比方,我们可以为一个当前存在的task_struct分配一个新的成员变量"newpid"并给它赋值1000。

struct task_struct *tsk = current;
int *newpid;
// 通过kpatch_shadow_alloc往tsk中创建newpid变量
newpid = kpatch_shadow_alloc(tsk, "newpid", sizeof(int), GFP_KERNEL);
if (newpid)
    newpid = 1000;
// 通过kpatch_shadow_get获取某个具体变量中的"newpid"成员
newpid = kpatch_shadow_get(tsk, "newpid");
if (newpid)
	printk("task newpid = %d\n", *newpid); // prints "task newpid = 1000"
// 通过kpatch_shadow_free函数释放某个具体结构中的newpid成员
kpatch_shadow_free(tsk, "newpid");

但是shadow变量不是我们本文讲述的重点内容。就在这里提一嘴,就先不展开了。

kpatch流程介绍

在我们具体去看kpatch流程的实现代码之前,我们先需要对kpatch流程有个overview。我们来看看kpatch的大概流程吧。
加载kpatch.ko模块的时候:
kpatch.ko加载流程
在卸载模块的时候:
模块加载的时候
以及在应用热补丁的过程中的主要过程:
热补丁应用过程
上面的示意图中描述的就是我们在加载以及卸载一个热补丁的过程中kpatch模块中以及系统重发生的事情。
结合这个示意图,可以总结一下这个过程中所涉及到的操作:获取互斥锁进入互斥状态,对全局kmod_list进行查询,处理对于对象以及hash表,更新kpatch以及function的kpatch状态。
接下来我么将会详细地介绍这个过程。
但是在了解kpatch模块提供的机制功能之前,我们需要先了解notifier block是什么。

什么是notifier block?

notifier block可以认为是一种监视器。通过注册监视器,可以在某个事件发生的时候调用对应的函数进行处理。在kpatch core.c中定义了两个notifier block

static struct notifier_block kpatch_module_nb_coming = {
    .notifier_call = kpatch_module_notify_coming,
    .priority = INT_MIN, /* called last */
};
static struct notifier_block kpatch_module_nb_going = {
    .notifier_call = kpatch_module_notify_going,
    .priority = INT_MAX, /* called first */
};

这里注册了两个notifier_block结构体,当一个事件到来的时候,上面两个notifier block都会收到这个通知。此时就需要通过notifier_block中的传入的action来判断这个消息需要由哪一个函数来进行处理。如果不是当前函数需要处理的事件,直接返回即可。而结构体中的priority则定义了这个监视器的调用顺序。也就是说,当一个hotfix事件来临,两个监视器都会检测到这个时间,但是kpatch_module_nb_going是会最先处理被调用的。
当插入一个hotfix的时候,两个notifier block都会收到通知,而且kpatch_module_nb_going会最先被调用。然后就会调用notifier_call中定义的函数进行处理。action中会带有处理状态。两个监视器判断来源action是否属于是自己需要处理的状态。如果不是自己需要处理的状态,那就不处理了,直接返回即可。

notifier_block中的处理函数

在kpatch core module中,用于处理notifier block的消息的函数有两个:一个是kpatch_module_notify_coming;另一个是kpatch_module_notify_going。
kpatch_module_notify_coming,从名字中就可以看出来,这个是一个hotfix被加载的时候调用的;而另一个则是hotfix被卸载的时候调用的。

kpatch_module_notify_coming

这个函数主要处理的事情主要有以下几件:1、找出加载的模块中的对象是否已经被打过patch了;2、链接加载的模块对象与目标对象;3、调用用户定义的回调函数;4、更新全局hash表

找出加载的模块中的对象是否已经被打过patch了

首先,在kpatch core中,有一个全局变量kpmod_list。这个kmod_list是一个链表,里面的节点是kpatch_module类型的结构体。kpatch_module类型的结构体描述如下:

struct kpatch_module {
    /* public */
    struct module *mod; // 一个指向module的指针,这个用于描述一个内核模块
    struct list_head objects; // 这个是一个链表,这个链表是当前这个hotfix设计的objects

    /* public read-only */
    bool enabled; // 这个用于指示当前这个hotfix是否是使能状态

    /* private */
    struct list_head list; //用于链接kpatch_module的链表结构
    struct kobject kobj; //这个是用于表述在sysfs中的kobject
};

这个结构体中的mod描述的就是这个模块本身。在kpatch_resigter对这个module进行注册的时候,会把这个模块插入到kpmod的链表的尾部。也许你们会很好奇这个kobject是由什么用的,这个kobject是用于在sysfs中向外透出当前patch的操作的一个object。

对于kpatch中的patch我们应该怎么理解呢?我们知道,我们可以通过一个patch来制作一个hotfix。一个hotfix制作出来了以后就是一个模块module。一个patch里面,我们可能会对很多文件进行了修改,所以这一个patch可以导致多个object的修改。这就是为什么,kpatch_module里面会链接多个object了。

检查当前加载的module中的object对象是否被link了

首先,这里我们要介绍一下object。Object大家都知道是对象。在热补丁中,一个patch可能会修改多个文件。每格.c文件都会对应一个.o的对象文件。因此,每个patch都是对一个或者多个object进行操作的。所以,热补丁从patch的粒度上可以分成三级:module,object,function。module是最大的一级,就是模块,可以认为就是这个ko;object就是对象,一个patch中可能会修改多个文件,每个文件就是一个对象;function就是最细的粒度。我们打patch就是针对函数进行修改的,热补丁也是如此,每个修改的object下面会有一个或者多个function的修改。kpatch流程中的三级的关系就是这样:

kpmod-|
      |-> object1
      |-> object2
      |-> object3-|
      ...         |-> function1
                  |-> function2
                  ...

在kpatch_module_notify_coming中,判断这个object是否被链接的方法的判断很简单,就是判断这个object的mod成员是否被设置了或者这个object是否是vmlinx本身。如果是,那么这个object就是被链接到对应的模块上了。接着,就要遍历每一个加载了的kpatch module的object链表,查找是否有对应名字来判断该object是否曾经被加载过。

链接对象

针对那些没有被链接过的,或者是没有加载过的对象,kpatch需要找出这个要被修复的对象所在的模块并将其链接上去。链接对象的实质就是把需要修补的对象注册到ftrace上并获取需要修补的函数的地址
kpatch在链接一个对象的时候,分四步走:1、找出对象的模块;2、写入引用了这个对象的修补模块的重定向;3、计算需要修复的函数的地址;4、使用ftrace注册修复后的函数
在开始对对象进行链接的时候,需要先判断一下这个模块是否是vmlinux所属对象。因为如果不是vmlinux的所属对象,那在进行重定向信息修改的时候则先需要找到对应的模块才能正确处理需要修复的函数的地址。
如果这个对象本身不属于vmlinux的,说明了需要修补的这个对象是一个外部模块。那就需要找出这个对象所属的模块,然后设置这个object的mod成员完成链接。通过查找当前系统下的已经加载的模块,检查已加载的模块中是否有与object匹配的模块。

写入重定向信息

在链接对象的时候,由于热补丁是以一个模块的形式加载的,所以在写入重定向信息的时候需要获取内核中模块的相关信息的。

struct module_layout {
    /* The actual code + data. */
    void *base; // 这个base是指向模块的代码以及数据的指针
    /* Total size. */
    unsigned int size; // size描述了模块的大小
    /* The size of the executable code.  */
    unsigned int text_size; // 描述了模块中可执行代码的大小
    /* Size of RO section of the module (text+rodata) */
    unsigned int ro_size; // 描述了模块中只读部分的大小
    /* Size of RO after init section */
    unsigned int ro_after_init_size; // 描述了模块初始化部分以后的只读代码的大小

#ifdef CONFIG_MODULES_TREE_LOOKUP
    struct mod_tree_node mtn; // 当内核中使用了模块树结构查询的配置的时候,这个结构体就
    用来描述模块树查找的结构体
#endif
};

上面的结构体是module_layout。这个结构体定义在’include/linux/module/h’中,用于描述linux内核中的模块的布局。详细的介绍在上面代码的注释中。这个module_layout是struct module中的成员,在module.h中有对struct module的定义。这里就不展开描述了。

#if (( LINUX_VERSION_CODE >= KERNEL_VERSION(4, 5, 0) ) || \
      ( LINUX_VERSION_CODE >= KERNEL_VERSION(4, 4, 0) && \
       UTS_UBUNTU_RELEASE_ABI >= 7 ) \
    )
     unsigned long core = (unsigned long)kpmod->mod->core_layout.base;
     unsigned long core_size = kpmod->mod->core_layout.size;
 #else
     unsigned long core = (unsigned long)kpmod->mod->module_core;
     unsigned long core_size = kpmod->mod->core_size;
 #endif

在kpatch_write_relocation的开始,我们可以看到这段代码。这段代码的意思是根据内核版本来获取core以及core_size。core以及core_size是什么呢?在kpatch中的这个core就是module中的core_layout中的base,也就是指向这个模块的代码以及数据的指针。获取这个core,相当于定位了这个模块。这里的逻辑就是获取传入的这个kpmod这个模块的代码以及数据的指针。
随后,kpatch将会遍历这个对象的动态重定向链表。什么是动态重定向?需要理解动态重定向,就需要先理解什么是重定向。在编译器构建对象的时候,编译器是不知道需要调用的这个外部的函数具体的地址是在哪里的,因此会现在对象文件中进行“留白”处理。然后在正式使用的时候,会对其进行重定向,重新计算这个需要引用的这个函数的地址。这个过程叫做重定向。那么动态重定向则是在发生在程序运行的过程中的,在内核运行的过程中,加载一个模块到内存中运行的时候,它可能会被放在物理内存中的随意的位置上。具体会在什么地方,这个取决于当前内存的使用情况以及内存分配策略等等的因素。所以,此时内存中的引用就需要用到动态重定向来确保引用的地址是正确的。

判断当前的动态重定向是否为外部的

这个是kpatch动态重定向的external标识。如果这个标识被设置了,那么就说明了当前需要被动态重定向的这个符号是在模块外部的。如果这个符号是模块外部符号,那么先根据模块的名字调用函数find_symbol返回这个符号的kernel_symbol类型的结构体(如果找到的话)。如果find_symbol找不到的话,那就直接调用kallsyms_on_each_symbol函数对每一个symbol进行检查,查找对应条件的符号。

根据动态重定向计算值

在社区的kpatch core module中,只做了对x86_64的动态重定向类型的开发。针对不同的动态重定向的类型进行计算。下面举例R_X86_64_PLT32的计算方法来介绍:
首先,要知道R_X86_64_PLT32的计算方法:ADDR=L+A-P
其中,L:是符号过程链接表条目的位置,就是区段偏移或者地址,是重定向表中的这个记录的位置
A: 通常是被重定位处的原值,表示引用符号的内存地址与L的偏移
P:需要修正的内存地址
因此,kpatch core中:
val = (u32)(dynrela->src + dynrela->addend - dynrela->dest);
这里的src就是这个动态重定向在条目中的位置,dest成员则是重定位的地址
依据不同的重定向类型,计算出来val的值
在kpatch core中,loc就是这个动态重定向的dest。在kpatch中,kpatch的动态重定向有自己定义的描述,kpatch使用结构体struct kpatch_dynrela对动态重定向进行描述:

struct kpatch_dynrela {
    unsigned long dest; // 目标地址,重定位条目应用的位置
    unsigned long src; // 源地址,通常是符号的地址
    unsigned long type; // 重定向的类型
    unsigned long sympos; // 符号在符号表中的位置
    const char *name; // 符号的名称
    int addend; // 重定向的补偿值,用于调整符号的地址
    int external; // 用于判断该符号是否为模块外部的符号
    struct list_head list; 
};

在处理每一中动态重定向类型的过程中,kpatch会记录下目前正在处理的动态重定向类型所占的大小。目前需要处理的动态重定向类型有R_X86_64_PLT32;R_X86_64_32S;R_X86_64_64。针对32位系统,一个指令是32位,也就是4个字节,而64位系统中则是8个字节。
然后,kpatch会调用memchr_inv函数找出当前重定向条目地址中是否存在非零值。如果出现了非零值,说明了当前处理的这个指令发生了变化,此时就不该对这个发生修改的指令进行变更。

修改指令地址

接下来,我们就要介绍kpatch是怎么完成写入重定向的操作的。
首先,kpatch要获取当前这个重定向是否在只读区间里面。

#if defined(CONFIG_DEBUG_SET_MODULE_RONX) || defined(CONFIG_ARCH_HAS_SET_MEMORY)
#if (( LINUX_VERSION_CODE >= KERNEL_VERSION(4, 5, 0) ) || \
     ( LINUX_VERSION_CODE >= KERNEL_VERSION(4, 4, 0) && \
      UTS_UBUNTU_RELEASE_ABI >= 7 ) \
    )
               readonly = (loc < core + kpmod->mod->core_layout.ro_size);
#else
               readonly = (loc < core + kpmod->mod->core_ro_size);
#endif
#endif

上面的代码比较简单,通过判断内核中的设置以及内核版本来决定使用那个判断逻辑。这两个判断逻辑都是找出只读区间的结束地址。判断loc是否落在只读区间之前,如果loc < {只读区间结束地址},那么就说明了当前处理的这个指令在只读内存中。
获取当前处理指令跨越了多少个页

numpages = (PAGE_SIZE - (loc & ~PAGE_MASK) >= size) ? 1 : 2;

上面这个语句是获取当前指令跨越了多少个内存页。PAGE_SIZE代表当前内核中一个页的大小。PAGE_MASK是一个位掩码,用于获取一个地址在其所在页中的偏移量。PAGE_MASK通常被定义为PAGE_SIZE - 1,所以~PAGE_MASK会将地址的低位清零,只保留高位。(loc & ~PAGE_MASK)会获取loc在其所在页中的偏移量。最后,这条语句的意思就是如果这个loc在这个页后面的位置比这个指令本身大小还要大,这个loc指令就能完全落在当前页面下;否则这个指令就会跨页。

对内存进行修改

如果指令在只读内存中,kpatch会把当前这个页设置为读写类型(如果这个指令跨页了,那就设置这两个页面为读写)。然后,把val的值写入loc中,完成重定向地址的替换。最后恢复内存原本的只读属性。

把发生修改的函数注册到ftrace上

在这一步上,遍历object中所有发生了修改的函数,然后找出这个函数的旧地址。然后把修补的函数注册到ftrace上并设置好ftrace过滤器。
至此,这个object的链接操作就属于完成了。
最后,把每一个发生修改的函数添加到kpatch_func_hash这个rcu链表中,完成kpatch_module_notify_coming的操作

kpatch_module_notify_going

kpatch_module_notify_going是hotfix被卸载/去使能时候调用的函数。在一个热补丁被去使能的时候,执行的主要步骤为:1、检查目标对象是存在的;2、调用用户调用的pre_unpatch_callback;3、把需要被去使能的模块中的所有修补函数从kpatch_hash_func中摘掉;4、调用post_unpatch_callback;5、最后把需要被去使能的对象给unlink掉。
其中,pre_unpatch_callback以及post_unpatch_callback是用户定义的在unpatch过程中调用的函数,这里就不展开讲述了。

kpatch_unlink_object

在最后需要把被去使能的对象给unlink的时候,遍历object中的每一个函数,把这里的函数调用ftrace_set_filter_ip从ftrace中给unregister掉。
如果所有的hotfix都被卸载,那么同时需要把ftrace handler给unregister掉。
当最后所有函数都完成了unregister操作以后,kpatch就会把这个object从模块中摘掉。

hotfix的register与unregister

从文章一开始的流程图中我们可以看到,在插入一个模块的时候,notifier block会收到对应的通知,并且执行相对应的操作。notifier block执行的是一些正式应用之前的初始化以及准备操作。当正式使能/去使能一个hotfix的时候,通过hotfix register和unregister进行对应的操作。
在kpatch中,使用了一个互斥信号量kpatch_mutex用于控制hotfix状态更新以及操作的互斥性,同时只允许一个hotfix进行操作,因此需要一个互斥信号量进行同步。kpatch_register函数的主要作用就是提供了暴露出sysfs接口的状态显示、更新的操作,同时确保一个hotfix在加载的过程中的安全性、完整性。接下来,我们将解析这个函数的行为,看看是如何加载一个具体的hotfix的操作的。

kpatch register

当一个hotfix被打上了以后,kpatch_register会被调用;当一个hotfix被disable以后,再被enable的时候,kpatch_register也是会被调用的。在kpatch_register刚被调用的时候,需要先检查当前这个hotfix的module的状态enable是否为true。这个hotfix由kpatch_module结构体来描述,下面是对这个结构体的解析

链接对象

跟模块来临时的通知机制中的动作类似,在使能一个hotfix的时候也需要对对象进行链接。如何链接一个对象,我们在前面已经介绍得比较清楚了,这里就不重复叙述了。在kpatch中,使用结构体struct kpatch_object来描述module中的object对象。一个object中可能存在多个函数的修改,在kpatch中则使用结构体struct kpatch_func结构体来对发生修改的函数进行描述。我们来解析一下这个结构体:

struct kpatch_func {
    /* public */
    unsigned long new_addr;
    unsigned long new_size;
    unsigned long old_addr;
    unsigned long old_size;
    unsigned long sympos;
    const char *name;
    struct list_head list;
    int force;

    /* private */
    struct hlist_node node;
    enum kpatch_op op;
    struct kobject kobj;
};

这个结构体中包含了一个发生了修改的函数的所需要的信息。其中的addr和size分别是这个函数的地址以及函数的大小,前缀修饰这个地址或者大小是这个函数修复前的原始版本还是修复后的新版本。sympos存储的是符号在符号表中的位置。而list则是用于链接其他kpatch_func结构体的链表节点头。force是一个标志,具体有什么作用后面会提到的。
针对这个结构体的私有成员,node是一个哈希节点,用于组织kpatch_func_hash的链表的;op是一个枚举值,在kpatch中,这是指示当前hotfix所处状态的,这个值有KPATCH_OP_NONE、KPATCH_OP_PATCH以及KPATCH_OP_UNPATCH这三个值。这三个值的意思分别表示当前函数无操作、当前函数正在打patch以及当前函数正在被卸载这三个状态。最后的kobj则是显示在sysfs中的当前函数的kobject节点。
当一个object被成功链接以后,这个object涉及到的所有的function的状态op将会被设置为KPATCH_OP_PATCH,表示当前函数正式进入打修复的流程中。

kpatch replace

在这里需要注意一个点,那就是kpatch replace。在kpatch-build中也有对REPLACE进行设置的点。这个kpatch replace的作用是打上当前的patch以后,需要卸载掉与这个hotfix相关的其他所有的hotfix。这个是因为red hat出hotfix的策略是累积出包的策略。red hat每一次出一个hotfix,都是包含了以前所有的修复的,并不是针对单独问题出包的,所以会出现这个功能。大家注意一下就好了。

更新kpatch状态

前面是对这个函数的状态进行设置,设置当前函数的状态为修复中。但是对于kpatch而言,自己本身也是有好几个状态来指示kpatch操作执行状态的。这个通过一个原子变量kpatch_state来表示当前kpatch执行状态。kpatch一共有四个状态:空闲、更新中、成功、失败。
在object被链接成功后,kpatch正式执行热补丁修复工作,此时就会把kpatch的状态设置为更新中。然后针对每一个object执行用户指定的pre_patch_callback回调函数,但凡在正式patch之前的任何一个回调函数的调用出现了问题,都会导致kpatch的状态变更为失败。

stop machine

在所有对象的回调函数执行成功以后,kpatch就会调用stop_machine函数把CPU给idle掉。检查接下来的替换操作的安全性、并且让新函数对于ftrace处理器可见。stop_machine会调用一个叫做kpatch_apply_patch的函数,这个函数首先会检查替换活动的安全性。
kpatch_apply_patch是如何检查替换活动的安全性的呢?
检查的方式其实不太难。kpatch会遍历所有进程(以及线程)的函数调用栈。然后,从kpatch_func_hash中查找是否存在当前这个修复函数的旧地址,如果这个修复函数的旧地址存在于kpatch_func_hash中,说明了当前这个函数是需要被patch过的;如果这个函数的旧地址不存在,那么这个地址可能是没有被patch过,或者是patch以后又被unpatch了。
如果这是第一种情况下,那么我们需要用的当前函数的地址是这个函数旧地址的新地址。这里可能有点绕,我们来仔细解析一下。如果这个函数存在与kpatch_func_hash中,说明这个函数是被patch过的。那么这个kpatch_func结构体中存储的old_addr是原始的函数地址,而new_addr则是这一个版本的函数的地址。目前运行在系统中的应该是这个版本函数的修复过的地址,所以用到的地址是这个版本函数的new_addr;如果是第二种情况,说明了当前函数可能没有被patch过,所以,我们只需要用当前版本函数记录的old_addr即可。
我们可以看一下这个图,描述的是第一种情况:
地址的获取
在这个图中,FuncA2是我们当前需要打patch的函数,但是原始版本的FuncA已经被patch过了,patch的函数是FuncA1,我们从kpatch_func_hash中获取到的是FuncA1,所以,我们要检查当前函数是否有被call,需要检查的是FuncA1的new_addr。
而下面这个图描绘的就是第二种情况:
在这里插入图片描述
在这个图中,我们发现FuncA是没有被patch过的,被call的就是原始的函数;而此时我们的kpatch_func描述的对象FuncA1中的old_addr就是这个函数的原始地址,所以,我们只需要检查这个old_addr是否在call stack上即可判断这个函数是否有在被调用。
最后,就是要比较所有函数调用栈里面有没有函数地址落在我们这个正在运行的函数版本的地址区间上。如果没有,说明当前函数没有正在被调用。
当安全性检查通过了以后,就会把每一个修复函数的旧地址作为key给添加到kpatch_func_hash上。设置kpatch状态为SUCCESS。最后再对每一个object调用post_patch_callback
其实做完这些操作以后,基本上kpatch的主要替换过程就已经执行完毕了。在最后的结尾,把kpatch状态设置为idle以后,设置rcu同步,确保所有rcu读侧临界区已经完全结束。并且把所有涉及修改的func的状态设置为NONE。
针对内核中配置了TAINT选项的话,会给当前模块添加污染标记。污染标记的作用在于当后续出现问题的时候,可以有助于快速定位到问题点。
至此,kpatch的register操作就全部结束了。

kpatch unregister

相比于kpatch_reigster,kpatch_unregister类似于其反操作。kpatch_unregister主要的操作就是状态设置、安全检查、把活动的函数摘掉、把模块摘掉这几个操作。

状态设置

在进入了kpatch_unregister以后,首先需要给涉及的模块中的所有的hotfix函数设置状态op为KPATCH_OP_UNPATCH,表示当前函数处于unpatch状态下。然后使用stop_machine调用kaptch_remove_patch函数。
kpatch_remove_patch
这个函数的操作比register中的会简单,基本就是安全检查,然后进行pre_unpatch_callback调用,最后把函数从kpatch_func_hash上摘掉。基本从这个kpatch_func_hash哈希表中摘掉了以后,这个函数就不是活动函数了。
当所有的函数都摘掉以后,更新涉及函数的状态到NONE。然后把这个hotfix涉及的object给unlink掉。
kpatch是怎么unlink掉一个object的呢?
我们在前面的介绍中知道,kpatch要link一个object,需要提前进行重定向的重写操作。但是在unlink的时候却没有这么复杂。我们只需要把这个函数从ftrace注册器上给卸掉,后续不会再对其感知即可。最后便把这个模块module从sysfs中摘掉。
kpatch_unregister的过程可以见下图:
在这里插入图片描述

至此,kpatch如果加载/卸载一个hotfix的过程我们就介绍完毕了。这个是kpatch提供热补丁的工作机制,是kpatch core module的工作流程。

patch

patch跟core在一个层级的目录下,core是提供kpatch整套流程机制的代码;我们知道,hotfix要提供完整的功能,除了需要有core提供热补丁的加载卸载的完整机制以外,还需要制作出来的hotfix兼容这一套机制的功能。就好像乐高积木一样,有凹进去的坑位,如果要稳固插上去,那需要另一块积木有对应凸出来的点。如果类比core提供的功能是凹,那么patch就是对应的凸。

patch中有哪些文件?分别有什么用?

patch中由图中的这些文件组成:
在这里插入图片描述
Makefile就不用介绍了吧,大家都知到是make过程中需要用到的文件。我们将对这里面的文件逐一介绍,大家看完这些文件是干啥的以后,也就知道patch文件的工作方式了。
patch中兼容了对livepatch以及kpatch.ko的两种工作方式。我们看一下kmod中Makefile中的描述:

include ../Makefile.inc
KPATCH_BUILD ?= /lib/modules/$(shell uname -r)/build
KERNELRELEASE := $(lastword $(subst /, , $(dir $(KPATCH_BUILD))))

all: clean
ifeq ($(BUILDMOD),yes)
	$(MAKE) -C core
endif

install:
ifeq ($(BUILDMOD),yes)
	$(INSTALL) -d $(MODULESDIR)/$(KERNELRELEASE)
	$(INSTALL) -m 644 core/kpatch.ko $(MODULESDIR)/$(KERNELRELEASE)
	$(INSTALL) -m 644 core/Module.symvers $(MODULESDIR)/$(KERNELRELEASE)
endif
	$(INSTALL) -d $(DATADIR)/patch
	$(INSTALL) -m 644 patch/* $(DATADIR)/patch

uninstall:
ifeq ($(BUILDMOD),yes)
	$(RM) -R $(MODULESDIR)
endif
	$(RM) -R $(DATADIR)

clean:
ifeq ($(BUILDMOD),yes)
	$(MAKE) -C core clean
endif

这里根据make时候是否有定义BUILDMOD来判断是否需要构建core。如果make的时候设置了BUILDMOD=yes的话,会构建core中的内容。因此,当配置了BUILDMOD的时候,会使用kpatch.ko的构建逻辑,构建kpatch.ko以及符号导出表Module.symvers。

patch是什么时候使用的呢?

需要注意的是,patch在构建kpatch项目的过程中是会根据是否配置了BUILDMOD来进行livepatch模式还是kpatch模式的选择,来构建patch-hook.o的。但是patch并不是构建kpatch这个项目的过程中用到的,而是在kpatch-build在构建一个热补丁的过程中用到的。这里是一个知识点。
首先,我们看看patch目录下的Makefile:

KPATCH_BUILD ?= /lib/modules/$(shell uname -r)/build
KPATCH_MAKE = $(MAKE) -C $(KPATCH_BUILD) M=$(PWD) CFLAGS_MODULE='$(CFLAGS_MODULE)'
LDFLAGS += $(KPATCH_LDFLAGS)

# object files that this Makefile can (re)build on its own
BUILDABLE_OBJS=$(filter-out output.o, $(wildcard *.o))

obj-m += $(KPATCH_NAME).o
ldflags-y += -T $(src)/kpatch.lds
targets += kpatch.lds

$(KPATCH_NAME)-objs += patch-hook.o output.o

all: $(KPATCH_NAME).ko

$(KPATCH_NAME).ko:
	$(KPATCH_MAKE)

$(obj)/$(KPATCH_NAME).o: $(src)/kpatch.lds

patch-hook.o: patch-hook.c kpatch-patch-hook.c livepatch-patch-hook.c
	$(KPATCH_MAKE) patch-hook.o

clean:
	$(RM) -Rf .*.o.cmd .*.ko.cmd .tmp_versions $(BUILDABLE_OBJS) *.ko *.mod.c \
	Module.symvers

看看all的构建,最后的hotfix是$(KPATCH_NAME).ko。这个ko是通过patch-hook.o以及output.o链接得到的。注意一下BUILDABLE_OBJS。这个地方的objs是指可以通过patch下的文件自己重新构建的目标文件。output.o是内核二次编译以后制作出来的diff obejct,patch中不包含对其进行二次构建的能力。所有这里需要排除output.o
我们看看kpatch-build中对应的代码:

export KPATCH_BUILD="$KERNEL_SRCDIR" KPATCH_NAME="$MODNAME" \
KBUILD_EXTRA_SYMBOLS="$KBUILD_EXTRA_SYMBOLS" \
KPATCH_LDFLAGS="$KPATCH_LDFLAGS" \
CROSS_COMPILE="$CROSS_COMPILE"
save_env


if [[ $USE_KLP -eq 1 ]]; then
	export CFLAGS_MODULE=$CFLAGS_MODULE" -DUSE_KLP=1"
else
	export CFLAGS_MODULE=$CFLAGS_MODULE" -DUSE_KLP=0"
fi

make "${MAKEVARS[@]}" 2>&1 | logger || die

刚接触kpatch-build的时候肯定会很迷惑这个地方make了啥。其实kpatch-build前面会先把patch文件拷贝到.kpatch(TEMPDIR)下的,这里就是已经进入了patch目录。make的时候调用了patch目录下的Makefile,然后把output.o(打了patch以后的输出产物)跟patch-hook.o做链接,得到hotfix的ko产物。
既然我们现在知道了patch是什么时候使用的,下面我们就来解析一下patch中的各个文件的使用。
我把patch中的文件分成两类:一种是功能类,指的是具体提供功能的代码;另一种是定义类,就是声明变量、结构体以及函数等的文件。在这两种分类下,patch-hook.c\livepatch-patch-hook.c\patch-hook.c以及kpatch.lds.S属于第一类;剩余的属于第二类。

patch-hook.c

patch-hook.c非常简单,就是几行代码,但是这个存在有点意思,我们看看这个代码:

#if IS_ENABLED(CONFIG_LIVEPATCH)
#include "livepatch-patch-hook.c"
#else
#include "kpatch-patch-hook.c"
#endif

Anolis kpatch-build
#if IS_ENABLED(CONFIG_LIVEPATCH) && (USE_KLP == 1)
#include "livepatch-patch-hook.c"
#else
#include "kpatch-patch-hook.c"
#endif

构建的时候,是构建的patch-hook.c这个文件。但是根据内核配置是否配置了LIVEPATCH来选择引livepatch-patch-hook.c还是kpatch-patch-hook.c。这种方式当内核配置了LIVEPATCH以后,patch-hook是根据livepatch来进行具体的构建的。如果没有支持livepatch的话,那么就使用kpatch的方式进行构建。从而简要地完成了不同模式下的选择。
在Anolis社区的kpatch-build中,增加了对USE_KLP == 1的判断,这样就实现了在支持livepatch的前提下的向前兼容,允许在支持livepatch的内核上使用kpatch core module的模式加载使用热补丁。

kpatch-patch-hook

在kpatch-build中,构建出来的diff object只是一个elf文件。这个elf文件里面包含了diff信息,不是一个模块,无法直接使用,也无法作为一个模块使用。在patch目录下make的时候,会把output和patch-hook链接,链接器会合并所有输入的所有代码和数据段;然后解析符号引用,确保每个符号引用都能找到定义;最后,生成重定位信息,以便在加载模块时能正确地更新符号引用。
kpatch-patch-hook中提供了模块加载和卸载的入口函数,也就是提供了module_init以及module_exit的函数。

patch_init

在把patch-hook与output链接以后的hotfix中,提供了module_init的调用函数patch_init。当使用insmod插入这个hotfix的时候,patch_init会被调用。
进入patch_init的时候,会调用kobject_init_and_add初始化一个kobject.

ret = kobject_init_and_add(&kpmod.kobj, &patch_ktype,
				   kpatch_root_kobj, "%s",
				   THIS_MODULE->name);

kpmod.kobj传入的是一个指针,这个是我们需要初始化和添加的kobj;patch_ktype是一个struct kobj_type的类型,每一个嵌入kobject的结构体都需要有一个ktype用于控制kobject在创建以及被销毁时的操作;kpatch_root_kobject则是我们kpatch的根kobject,我们打的hotfix全部都挂在这个kpatch的root_kobject下。也就是说,当前打的这个kpmod.kobj这个节点是当前hotfix的跟节点,挂在kpatch下面。最后的参数就是当前模块的名字。
在kobject_init_and_add的时候,kpmod已经是一个有合法内存空间的结构体。传入这个函数以后,kobject_init_and_add会初始化这个kpmod的kobject成员并将其添加到kpatch_root_kobj这个根下。

kobject的设置
static struct kobj_type patch_ktype = {
        .release = patch_kobj_free,
        .sysfs_ops = &kobj_sysfs_ops,
        .default_attrs = patch_attrs,
};

这个是patch中patch_ktype的设置。从这个设置里面,我们可以看到patch_ktype里面设置了release、sysfs_ops以及default_attrs的几个属性。patch_kobj_free是一个函数,是当这个kobject被释放的时候调用的函数;sysfs_ops默认使用kobj_sysfs_ops;patch_attrs用于设置当前kobject的默认属性。
patch_attrs里面注册了两个属性:一个是enabled;另一个是checksum。enabled支持show和store的操作,说明了我们除了可以读取enabled的值以外,也支持对这个值进行修改。而checksum只支持读操作。

static struct kobj_attribute patch_enabled_attr =
	__ATTR(enabled, 0644, patch_enabled_show, patch_enabled_store);

这个代码对enabled属性进行注册,注册该数据的操作权限以及读写操作对应调用的函数。然后把这个属性注册到patch_attrs上作为patch_ktype的属性。
因此,在kobject视角下,我大概进行了总结了它们的关系:
在这里插入图片描述

生成函数列表

上面这个操作是对patch这个object进行处理的,生成patch对象的默认属性并将patch挂到kpatch的sysfs的节点内。
然后,针对这个hotfix中修改的每一个函数,找出这个函数对应修改的object。从kpmod的objects成员中进行查找,如果当前函数所属的object不在objects列表中,那么就会创建一个新的这个函数的object的kobject节点,并把这个刚创建的对象挂在kpmod的kobject下。意思就是,假设这个hotfix的名字为hotfix1,那么加载的时候将会在kpatch下创建一个hotfix1的节点。当一个hotfix刚加载的时候,kpmod的objects链表是空的,所以会先创建一个当前对象的kobject。创建完毕以后,以后每次遇到相同object下的函数的修改的时候,只需要服用即可。
如果这个func属于TestObject,不存在与hotfix1的对象链表中,那么就会创建TestObject的kobject并挂在hotfix1下。所以,查看/sys/kernel/kpatch/hotfix1目录下将会出现一个TestObject的文件夹。当func2也是属于TestObject下的函数的时候,此时func2的kobject节点将会服用在TestObject下。
在设置好函数的状态以后,把当前函数作为一个kobject的节点,挂在该函数所属对象的kobj节点下。

设置当前对象的动态重定向链表

kpatch在生成最终可用的hotfix模块之前,会把相同属性的seciton都放在一起。
针对动态重定向,只需要遍历动态重定向的区间,找出每一项动态重定向,设置当前动态重定向的信息以后,将其挂在所属对象的动态重定向链表中即可。
但是这个动态重定向链表属于是当前对象的信息,并没有从kobject中生成节点透出给用户。

设置当前对象的回调函数链表

在kpatch中,回调函数一共有四种:分别是(pre/post)_(patch/unpatch)_callbacks。分别用于patch过程中的patch前、patch后以及unpatch过程中的patch前以及patch后。类似于我们的rpm包的rpm spec中的post_install 以及pre_install的操作。用于做正式的安装\卸载前的准备和收尾工作。
设置当前对象的回调函数链表的过程跟设置动态重定向链表的过程类似。他们都被整理到所属的section下。针对pre_patch_callback,我们需要遍历这个section的内存区间,把每一个回调函数挂在其所属的object下即可。同样的,回调函数作为对象的内部信息,是不会通过kobject向外透出的。
在完成了上面的操作以后,patch模块就会调用kpatch_register,进入hotfix的正式加载过程。

效果

下面,给大家看看一个patch的示例:

From 4afcbd8425a0331150d1ec03f30ac39df5c8a194 Mon Sep 17 00:00:00 2001
From: "wardenjohn" <zhangwarden@gmail.com>
Date: Tue, 9 Jan 2024 16:41:34 +0800
Subject: [PATCH] test_meminfo


diff --git a/fs/proc/meminfo.c b/fs/proc/meminfo.c
index 0dcda1b4bbae..c95c6b71d2e1 100644
--- a/fs/proc/meminfo.c
+++ b/fs/proc/meminfo.c
@@ -95,8 +95,8 @@ static int meminfo_proc_show(struct seq_file *m, void *v)
        sreclaimable = global_node_page_state_pages(NR_SLAB_RECLAIMABLE_B);
        sunreclaim = global_node_page_state_pages(NR_SLAB_UNRECLAIMABLE_B);
 
-       show_val_kb(m, "MemTotal:       ", i.totalram);
-       show_val_kb(m, "MemFree:        ", i.freeram);
+       show_val_kb(m, "**MemTotal:       ", i.totalram);
+       show_val_kb(m, "**MemFree:        ", i.freeram);
        show_val_kb(m, "MemAvailable:   ", ext.available);
        show_val_kb(m, "Buffers:        ", i.bufferram);
        show_val_kb(m, "Cached:         ", ext.cached);
-- 
2.37.3

这个patch修改的是meminfo中的show函数,cat /proc下的meminfo
在这里插入图片描述在/sys/kernel/kpatch下,我们打了一个名为kpatch_419的hotfix,这个hotfix名为kpatch_419.ko,可以看到kpatch节点下有一个hotfix名字的节点。
在这里插入图片描述
在/sys/kernel/kpatch下,我们打了一个名为kpatch_419的hotfix,这个hotfix名为kpatch_419.ko,可以看到kpatch节点下有一个hotfix名字的节点。
在这里插入图片描述
我们看看这个patch下面有什么:
在这里插入图片描述
可以看到,跟我们之前描述的一样,我们修改的这个对象是vmlinux中的函数,因此这个patch下出现的object有vmlinux。而checksum以及enabled就是这个hotfix的属性。
进入vmliux下,可以看到修改过的函数是meminfo_proc_show,挂在vmlinux这个object下,而vmlinux的修改属于kpatch_419下的。这个就是这个hotfix的加载过程。

patch_exit

对于patch_exit的退出函数就比较简单了。退出的时候,把每一个func和object从kobject中摘除即可。
当然啦,不是简单的就可以摘掉的。在卸载模块之前,需要先把hotfix给disable调用。通过echo 0 到enabled中,会调用sysfs中设定的操作函数。如果改变的值是0,此时函数会调用kpatch_unregister,把hotfix给注销掉以后才能执行patch_exit的操作。

总结

在文章的开头部分,我给大家画了几张大图,包含了hotfix加载以及卸载的部分操作。在使用kpatch core module的模式下,在加载hotfix的时候,notifier_block会执行相应的通知行为。然后,在insmod的时候,会调用hotfix制作过程中由patch模块加入的patch_init的函数,进而调用热补丁的加载kpatch_register的操作。同样的,在热补丁卸载的过程中,同样的notifier_block会执行相应的通知行为。但是在卸载的过程中,需要先对热补丁进行disable,在进行disable的时候,就会调用kpatch_unregister,然后才能执行patch_exit的过程。
在kpatch中,kpatch_func_hash这个哈希表可以认为是用于存储当前系统中活跃的通过kpatch修复的函数哈希链表。在这上面的节点都是活跃的修复函数。
总的说来,kpatch项目中,kmod是用于产生最终模块的。core是用于提供热补丁整套流程的基座模块,生成kpatch.ko,用于插入内核提供热补丁的流程;patch则是提供凸的接口,在kpatch-build中制作出来的diff object需要加入patch中的接口代码,才能接入到热补丁的直接流程中使用。
因此,我们可以把core看成凹,patch看成凸,凹凸结合,组成一套系统运行过程中的内核函数热修复机制。

最后

各位对我的文章如果有什么意见,或者觉得我有理解错误或者写错的地方,欢迎给我指出来,我会很感激的!Wink~

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,让我来回答你的问题。你想了解System.Threading序列的第一篇文章,它从Thread的线程单元状态ApartmentState说起。 Thread类在.NET Framework中是一个非常重要的类,它提供了一种在程序中创建和管理线程的方式。在使用Thread类之前,我们需要了解线程单元状态(ApartmentState)。线程单元状态是指一个线程所在的单元,它决定了线程在运行时的行为。 在.NET Framework中,线程单元状态有两种:Single Threaded Apartment和Multi-Threaded Apartment。Single Threaded Apartment是指只有一个线程可以进入该单元,它适用于单线程的应用程序。Multi-Threaded Apartment是指多个线程可以进入该单元,它适用于多线程的应用程序。 在Thread类中,我们可以使用ApartmentState属性来设置线程单元状态。默认情况下,线程单元状态是Multi-Threaded Apartment。如果我们想将线程单元状态设置为Single Threaded Apartment,可以使用以下代码: ```csharp Thread t = new Thread(new ThreadStart(TestMethod)); t.SetApartmentState(ApartmentState.STA); t.Start(); ``` 在以上代码中,我们创建了一个新的线程,并将线程单元状态设置为Single Threaded Apartment。然后,我们启动线程并开始执行TestMethod方法。 总之,了解线程单元状态对于使用Thread类来创建和管理线程是非常重要的。在下一篇文章中,我们将继续探讨System.Threading序列的内容。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值