4、第一个热补丁:编写简单的热补丁模块

说实话,很多初学者一上来就被「热补丁」这三个字吓住了。觉得这东西肯定特别复杂,得懂内核调度、懂内存管理、懂各种底层机制。其实不然。我当年第一次写热补丁模块时,也就花了半天时间。说白了,它就是一个特殊的内核模块,只不过多了一些「热补丁专属」的结构体。

这一节,我们就手把手写一个最简单的热补丁模块。目标很明确:替换一个内核函数,让它打印不同的日志。你跟着我走一遍,就会发现这东西没那么玄乎。

4.1 热补丁模块的基本骨架

先看整体结构。一个热补丁模块,本质上还是一个内核模块。它需要:

  • 一个 klp_modinfo 结构体,描述补丁信息
  • 一个 klp_patch 结构体,描述要替换的函数
  • 模块的 initexit 函数,负责注册和注销

嗯,这里要注意:klp_modinfo 是热补丁的「身份证」。内核靠它来识别这个补丁是谁打的、打了哪些函数、版本对不对。

核心概念:klp_modinfo 结构体是热补丁模块的元数据。它告诉内核:「我是谁,我替换了哪些函数,我的版本号是多少。」

4.2 编写 klp_modinfo 结构体

我个人习惯,写热补丁模块时,先把 klp_modinfo 定义好。就像盖房子先打地基一样。

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/livepatch.h>

// 被替换的原函数
static int original_func(int a, int b)
{
    printk(KERN_INFO "original_func: a=%d, b=%d, sum=%d\n", a, b, a + b);
    return a + b;
}

// 替换后的新函数
static int patched_func(int a, int b)
{
    printk(KERN_INFO "patched_func: a=%d, b=%d, product=%d\n", a, b, a * b);
    return a * b;
}

// 定义 klp_func 数组,描述要替换的函数
static struct klp_func funcs[] = {
    {
        .old_name = "original_func",
        .new_func = patched_func,
    },
    { }
};

// 定义 klp_object,描述要替换的对象(这里是 vmlinux)
static struct klp_object objs[] = {
    {
        .name = NULL,  // NULL 表示替换 vmlinux 中的函数
        .funcs = funcs,
    },
    { }
};

// 定义 klp_patch,描述整个补丁
static struct klp_patch patch = {
    .mod = THIS_MODULE,
    .objs = objs,
};

你看,代码其实不长。关键点就三个:

  • klp_func:描述「旧函数名」和「新函数指针」的对应关系
  • klp_object:描述要替换哪个模块里的函数(NULL 表示内核本身)
  • klp_patch:把上面两个打包成一个补丁

小技巧:klp_func 数组最后一定要加一个空结构体 { } 作为结束标记。我刚开始写时忘了加,结果内核加载时直接崩溃。嗯,血的教训。

4.3 注册与注销热补丁

结构体定义好了,接下来就是注册和注销。这部分和普通内核模块的 init/exit 很像,但用的是热补丁专用的 API。

static int __init livepatch_init(void)
{
    int ret;

    ret = klp_enable_patch(&patch);
    if (ret) {
        printk(KERN_ERR "livepatch: failed to enable patch (%d)\n", ret);
        return ret;
    }

    printk(KERN_INFO "livepatch: patch enabled successfully\n");
    return 0;
}

static void __exit livepatch_exit(void)
{
    // 热补丁模块卸载时,内核会自动恢复原函数
    // 不需要手动调用 klp_disable_patch
    printk(KERN_INFO "livepatch: patch disabled\n");
}

module_init(livepatch_init);
module_exit(livepatch_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");  // 标记这是一个热补丁模块

这里有个细节:为什么 exit 里不需要调用 klp_disable_patch? 我当初也困惑过。后来看了内核源码才明白——内核在卸载热补丁模块时,会自动遍历所有已启用的补丁,逐个恢复原函数。你手动调用反而可能出问题。

注意:MODULE_INFO(livepatch, "Y") 这行不能少。少了它,内核不会把这个模块当作热补丁模块处理,加载时会报错。我曾经在调试时漏掉这行,折腾了半小时才找到原因。

4.4 加载与卸载内核模块

模块写好了,怎么加载?和普通模块一样,用 insmod 和 rmmod。

# 编译模块
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载热补丁
sudo insmod livepatch.ko

# 查看内核日志,确认补丁已生效
dmesg | tail -5

# 测试:调用原函数(如果原函数被其他模块调用,会看到 patched_func 的输出)
# 卸载热补丁
sudo rmmod livepatch

加载成功后,你可以在 /sys/kernel/livepatch/ 目录下看到补丁的状态:

$ cat /sys/kernel/livepatch/livepatch/enabled
1

1 表示补丁已启用。如果显示 0,说明补丁加载了但没生效。这时候就要检查 klp_func 里的函数名是否拼写正确。

4.5 完整代码示例

我把上面所有代码整合到一起,方便你直接使用:

// livepatch.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/livepatch.h>

static int original_func(int a, int b)
{
    printk(KERN_INFO "original_func: a=%d, b=%d, sum=%d\n", a, b, a + b);
    return a + b;
}

static int patched_func(int a, int b)
{
    printk(KERN_INFO "patched_func: a=%d, b=%d, product=%d\n", a, b, a * b);
    return a * b;
}

static struct klp_func funcs[] = {
    {
        .old_name = "original_func",
        .new_func = patched_func,
    },
    { }
};

static struct klp_object objs[] = {
    {
        .name = NULL,
        .funcs = funcs,
    },
    { }
};

static struct klp_patch patch = {
    .mod = THIS_MODULE,
    .objs = objs,
};

static int __init livepatch_init(void)
{
    int ret = klp_enable_patch(&patch);
    if (ret) {
        printk(KERN_ERR "livepatch: failed (%d)\n", ret);
        return ret;
    }
    printk(KERN_INFO "livepatch: enabled\n");
    return 0;
}

static void __exit livepatch_exit(void)
{
    printk(KERN_INFO "livepatch: disabled\n");
}

module_init(livepatch_init);
module_exit(livepatch_exit);
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");

4.6 热补丁模块的核心流程

为了让你更直观地理解整个过程,我画了一张流程图:

热补丁模块核心流程 定义 klp_modinfo 编写 klp_func 数组 组装 klp_patch module_init 注册 insmod 加载模块 rmmod 卸载恢复 关键数据结构 • klp_modinfo:补丁元数据 • klp_func:函数替换映射 • klp_object:目标模块 • klp_patch:补丁整体

从这张图你能看到,整个流程是线性的:先定义数据结构,再注册,最后加载。卸载时内核自动帮你恢复原函数。说白了,就是「定义-注册-加载-卸载」四个步骤。

4.7 避坑指南

最后,分享几个我踩过的坑:

  • 函数名必须完全匹配:包括大小写。内核用 kallsyms 查找函数地址,拼写错一个字就找不到。
  • 被替换的函数不能是 static 的:static 函数不会被导出到符号表,热补丁找不到它。
  • 新函数的签名必须和原函数一致:参数个数、类型、返回值都要一样。否则调用时会栈崩溃。
  • 先测试再部署:我习惯在测试机上先 insmod/rmmod 三次,确认没问题再上生产。

总结:第一个热补丁模块,核心就是 klp_modinfo 结构体的定义 + klp_enable_patch 的调用。代码量不超过 80 行。你跟着本文写一遍,半小时内就能跑起来。


专注资料整理