你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

热补丁的原理——替换Linux内核函数的实现

[复制链接]
gaosmile 发布时间:2020-6-10 18:17

看题目, 替换Linux内核函数的实现 ,what?这不就是kpatch嘛!也就是我们所谓的 热补丁 。我们为内核做热补丁的时候,没人用汇编写吧,没人用二进制指令码去拼逻辑吧,我们一般都是直接修改内核函数的C代码的,然后形成一个patch文件,然后…然后…去读kpatch的Documents吧。

本文我将要描述的是热补丁的原理,而不是一个如何使用kpatch的Howto,更不是关于任何kpatch技术的源码分析。

以一个实际的3.10内核的Bugfix热补丁为例开始我们的故事。

在该实例中,我们修改了set_next_buddy的实现:

  • diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
  • ...
  • @@ -4537,8 +4540,11 @@ static void set_next_buddy(struct sched_entity *se)
  •     if (entity_is_task(se) && unlikely(task_of(se)->policy == SCHED_IDLE))
  •         return;

  • -   for_each_sched_entity(se)
  • +   for_each_sched_entity(se) {
  • +       if (!se->on_rq)
  • +           return;
  •         cfs_rq_of(se)->next = se;
  • +   }
  • }


看来,为了Fix一个已知的Bug,我们需要为set_next_buddy函数加几行代码,很显然,这很容易。

增加了这几行代码后,便形成了一个新的set_next_buddy函数,为了能让新的函数run起来,现在我们面临三个问题:

  • 我们如何可以将这个新的set_next_buddy函数编译成二进制?
  • 我们如何将这个新的set_next_buddy函数二进制码注入到正在运行的内核?
  • 我们如何用新的set_next_buddy二进制替换老的set_next_buddy函数?

我们一个一个问题看。

首先,第一个问题非常容易解决。

我们修改了一个C文件kernel/sched/fair.c,为了解决编译时的依赖问题,只需要将修改后形成的patch文件打入当前运行内核的源码树中就可以编译了,通过objdump之类的机制,我们可以把编译好的set_next_buddy二进制抠出来形成一个obj文件,然后组成一个ko就不是什么难事了。这便形成了一个内核模块,类似 kpatch-y8u59dkv.ko

接下来看第二个问题,如何将第一个问题中形成的ko文件中set_next_buddy二进制注入到内核呢?

这也不难,kpatch的模块加载机制就是干这个的。打入热补丁的内核就会出现两个set_next_buddy函数:

  • crash> dis set_next_buddy
  • dis: set_next_buddy: duplicate text symbols found:
  • // 老的set_next_buddy
  • ffffffff810b9450 (t) set_next_buddy /usr/src/debug/kernel-3.10.0/linux-3.10.0.x86_64/kernel/sched/fair.c: 4536
  • // 新的set_next_buddy
  • ffffffffa0382410 (t) set_next_buddy [kpatch_y8u59dkv


到了第三个问题,有点麻烦。新的set_next_buddy二进制如何替换老的set_next_buddy二进制呢?

显然,不能采用覆盖的方式,因为内核函数的布局是非常紧凑的且连续的,每个函数的空间在内核形成的时候就确定了,如果新函数比老函数大很多,就会越界覆盖掉其它的函数。

采用我前面文章里描述的二进制hook技术是可行的,比如下面文章里的方法:
http://blog.csdn.net/dog250/article/details/105206753
通过二进制diff,然后紧凑地poke需要修改的地方,这无疑是一种妙招!然而这种方法并不优雅,充满了奇技淫巧,它最大的问题就是逆经理。

最正规的方法就是使用ftrace的hook,即 修改老函数的开头5个字节的ftrace stub,将其修改成“jmp/call 新函数”的指令,并且在stub函数中skip老函数的栈帧。 如此一来彻底绕过老的函数。

我们来看上面提到的两个set_next_buddy的二进制:

  • // 老的set_next_buddy:
  • crash> dis ffffffff810b9450 4
  • // 注意,老函数的ftrace stub已经被替换
  • 0xffffffff810b9450 <set_next_buddy>:    callq  0xffffffff81646df0 <ftrace_regs_caller>
  • // 后面这些如何被绕过呢?ftrace_regs_caller返回后如何被skip掉呢?这需要平衡堆栈的技巧!
  • // 后面通过实例来讲如何平衡堆栈,绕过老的函数。
  • 0xffffffff810b9455 <set_next_buddy+5>:  push   %rbp
  • 0xffffffff810b9456 <set_next_buddy+6>:  cmpq   $0x0,0x150(%rdi)
  • 0xffffffff810b945e <set_next_buddy+14>: mov    %rsp,%rbp
  • // 新的set_next_buddy:
  • crash> dis ffffffffa0382410 4
  • // 新函数则是ftrace_regs_caller最终要调用的函数
  • 0xffffffffa0382410 <set_next_buddy>:    nopl   0x0(%rax,%rax,1) [FTRACE NOP
  • 0xffffffffa0382415 <set_next_buddy+5>:  push   %rbp
  • 0xffffffffa0382416 <set_next_buddy+6>:  cmpq   $0x0,0x150(%rdi)
  • 0xffffffffa038241e <set_next_buddy+14>: mov    %rsp,%rbp


这就是热补丁的原理了。

本文到这里都是纸上的高谈阔论,就此结束未免尴尬且遗憾,接下来我要用一个实际的例子来说明这一切。这个例子非常简单,随便摆置几下就能run起来看到效果。

我比较讨厌源码分析,所以我不会去走读注释ftrace_regs_caller的源码,我用我自己的方式来实现类似的需求,并且要简单的多,这非常有利于咱们工人理解事情的本质。

我的例子不会去patch内核中既有的函数,我的例子patch的是我编写的一个简单的内核模块里的函数,该模块代码如下:

  • #include <linux/module.h>
  • #include <linux/proc_fs.h>

  • // 下面的sample_read就是我将要patch的函数
  • static ssize_t sample_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
  • {
  •     int n = 0;
  •     char kb[16;

  •     if (*ppos != 0) {
  •         return 0;
  •     }

  •     n = sprintf(kb, "%d\n", 1234);
  •     memcpy(ubuf, kb, n);
  •     *ppos += n;
  •     return n;
  • }

  • static struct file_operations sample_ops = {
  •     .owner = THIS_MODULE,
  •     .read = sample_read,
  • };

  • static struct proc_dir_entry *ent;
  • static int __init sample_init(void)
  • {
  •     ent = proc_create("test", 0660, NULL, &sample_ops);
  •     if (!ent)
  •         return -1;

  •     return 0;
  • }

  • static void __exit sample_exit(void)
  • {
  •     proc_remove(ent);
  • }

  • module_init(sample_init);
  • module_exit(sample_exit);
  • MODULE_LICENSE("GPL");


我们加载它,然后去read一下/proc/test:

  • [root@localhost test# insmod sample.ko
  • [root@localhost test# cat /proc/test
  • 1234


OK,一切如愿。此时,我们看看sample_read的前面的5个字节:

  • crash> dis sample_read 1
  • 0xffffffffa038c000 <sample_read>:       nopl   0x0(%rax,%rax,1) [FTRACE NOP


来来来,在已经加载了sample.ko的前提下,我们现在patch它。我的目标是,Fix掉sample_read函数,使得它返回4321而不是1234。

以下是全部的代码,要点都在注释里:

  • // hijack.c
  • #include <linux/module.h>
  • #include <linux/kallsyms.h>
  • #include <linux/cpu.h>

  • char *stub;
  • char *addr = NULL;

  • // 可以用JMP模式,也可以用CALL模式
  • //#define JMP    1

  • // 和sample模块里同名的sample_read函数
  • static ssize_t sample_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
  • {
  •     int n = 0;
  •     char kb[16;

  •     if (*ppos != 0) {
  •         return 0;
  •     }
  •     // 这里我们把1234的输出给fix成4321的输出
  •     n = sprintf(kb, "%d\n", 4321);
  •     memcpy(ubuf, kb, n);
  •     *ppos += n;
  •     return n;
  • }

  • // hijack_stub的作用就类似于ftrace kpatch里的ftrace_regs_caller
  • static ssize_t hijack_stub(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
  • {
  •     // 用nop占位,加上C编译器自动生成的函数header代码,这么大的函数来容纳stub应该够了。
  •     asm ("nop; nop; nop; nop; nop; nop; nop; nop;");
  •     return 0;
  • }

  • #define FTRACE_SIZE       5
  • #define POKE_OFFSET        0
  • #define POKE_LENGTH        5
  • #define SKIP_LENGTH        8

  • static unsigned long *(*_mod_find_symname)(struct module *mod, const char *name);
  • static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
  • static struct mutex *_text_mutex;
  • unsigned char saved_inst[POKE_LENGTH;
  • struct module *mod;

  • static int __init hotfix_init(void)
  • {
  •     unsigned char jmp_call[POKE_LENGTH;
  •     unsigned char e8_skip_stack[SKIP_LENGTH;
  •     s32 offset, i = 5;

  •     mod = find_module("sample");
  •     if (!mod) {
  •         printk("没加载sample模块,你要patch个啥?\n");
  •         return -1;
  •     }
  •     _mod_find_symname = (void *)kallsyms_lookup_name("mod_find_symname");
  •     if (!_mod_find_symname) {
  •         printk("还没开始,就已经结束。");
  •         return -1;
  •     }
  •     addr = (void *)_mod_find_symname(mod, "sample_read");
  •     if (!addr) {
  •         printk("一切还没有准备好!请先加载sample模块。\n");
  •         return -1;
  •     }
  •     _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
  •     _text_mutex = (void *)kallsyms_lookup_name("text_mutex");
  •     if (!_text_poke_smp || !_text_mutex) {
  •         printk("还没开始,就已经结束。");
  •         return -1;
  •     }

  •     stub = (void *)hijack_stub;

  •     offset = (s32)((long)sample_read - (long)stub - FTRACE_SIZE);

  •     // 下面的代码就是stub函数的最终填充,它类似于ftrace_regs_caller的作用!
  •     e8_skip_stack[0 = 0xe8;
  •     (*(s32 *)(&e8_skip_stack[1)) = offset;
  • #ifndef JMP    // 如果是call模式,则需要手工平衡堆栈,跳过原始函数的栈帧
  •     e8_skip_stack[i++ = 0x41; // pop %r11
  •     e8_skip_stack[i++ = 0x5b; // r11寄存器为临时使用寄存器,遵循调用者自行保护原则
  • #endif
  •     e8_skip_stack[i++ = 0xc3;
  •     _text_poke_smp(&stub[0, e8_skip_stack, SKIP_LENGTH);

  •     offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);

  •     memcpy(&saved_inst[0, addr, POKE_LENGTH);
  • #ifndef JMP
  •     jmp_call[0 = 0xe8;
  • #else
  •     jmp_call[0 = 0xe9;
  • #endif
  •     (*(s32 *)(&jmp_call[1)) = offset;
  •     get_online_cpus();
  •     mutex_lock(_text_mutex);
  •     _text_poke_smp(&addr[POKE_OFFSET, jmp_call, POKE_LENGTH);
  •     mutex_unlock(_text_mutex);
  •     put_online_cpus();

  •     return 0;
  • }

  • static void __exit hotfix_exit(void)
  • {
  •     mod = find_module("sample");
  •     if (!mod) {
  •         printk("一切已经结束!\n");
  •         return;
  •     }
  •     addr = (void *)_mod_find_symname(mod, "sample_read");
  •     if (!addr) {
  •         printk("一切已经结束!\n");
  •         return;
  •     }
  •     get_online_cpus();
  •     mutex_lock(_text_mutex);
  •     _text_poke_smp(&addr[POKE_OFFSET, &saved_inst[0, POKE_LENGTH);
  •     mutex_unlock(_text_mutex);
  •     put_online_cpus();
  • }

  • module_init(hotfix_init);
  • module_exit(hotfix_exit);
  • MODULE_LICENSE("GPL");


OK,我们载入它吧,然后重新read一下/proc/test:

  • [root@localhost test# insmod ./hijack.ko
  • [root@localhost test# cat /proc/test
  • 4321


可以看到,已经patch成功。到底发生了什么?我们看下反汇编:

  • crash> dis sample_read
  • dis: sample_read: duplicate text symbols found:
  • ffffffffa039d000 (t) sample_read [sample
  • ffffffffa03a2020 (t) sample_read [hijack
  • crash>


嗯,已经有两个同名的sample_read函数符号了,sample模块里的是老的函数,而hijack模块里的是新的fix后的函数。我们分别看一下:

  • // 先看老的sample_read,它的ftrace stub已经被改成了call hijack_stub
  • crash> dis ffffffffa039d000 1
  • 0xffffffffa039d000 <sample_read>:       callq  0xffffffffa03a2000 <hijack_stub>
  • // 再看新的sample_read,它就是最终被执行的函数
  • crash> dis ffffffffa03a2020 1
  • 0xffffffffa03a2020 <sample_read>:       nopl   0x0(%rax,%rax,1) [FTRACE NOP
  • crash>


当新的sample_read执行完毕,返回hijack_stub后,如果是CALL模式,此时需要skip掉老的sample_read函数的栈帧,所以一个pop %r11来完成它,之后直接ret即可,如果是JMP模式,则直接ret,不需要skip栈帧,因为JMP指令根本就不会压栈。

好了,这就是我要讲的故事。说白了,本文描述的依然是一个手艺活,我只是希望用大家都能理解的最简单的方式,来展示相对比较复杂的热补丁的实现原理。我觉得工友们有必要对底层的原理有深刻的认知。

经理也爱吃辣椒,但不很,不过显而易见的是,经理洒不了水。


收藏 评论0 发布时间:2020-6-10 18:17

举报

0个回答

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版