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

热补丁的原理——替换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扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版