当前位置:首页 > 嵌入式 > 嵌入式微处理器
[导读]看题目, 替换Linux内核函数的实现 ,what?这不就是kpatch嘛!也就是我们所谓的 热补丁 。我们为内核做热补丁的时候,没人用汇编写吧,没人用二进制指令码去拼逻辑吧,我们一般都是直接修改内核函数的C代码的,然后形成一个patch文件,然后…然后…去读kpatc



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

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

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

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

   
  1. diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c

  2. ...

  3. @@ -4537,8 +4540,11 @@ static void set_next_buddy(struct sched_entity *se)

  4. if (entity_is_task(se) && unlikely(task_of(se)->policy == SCHED_IDLE))

  5. return;


  6. - for_each_sched_entity(se)

  7. + for_each_sched_entity(se) {

  8. + if (!se->on_rq)

  9. + return;

  10. cfs_rq_of(se)->next = se;

  11. + }

  12. }


看来,为了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函数:

   
  1. crash> dis set_next_buddy

  2. dis: set_next_buddy: duplicate text symbols found:

  3. // 老的set_next_buddy

  4. ffffffff810b9450 (t) set_next_buddy /usr/src/debug/kernel-3.10.0/linux-3.10.0.x86_64/kernel/sched/fair.c: 4536

  5. // 新的set_next_buddy

  6. ffffffffa0382410 (t) set_next_buddy [kpatch_y8u59dkv]


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

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

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

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

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

   
  1. // 老的set_next_buddy:

  2. crash> dis ffffffff810b9450 4

  3. // 注意,老函数的ftrace stub已经被替换

  4. 0xffffffff810b9450 <set_next_buddy>: callq 0xffffffff81646df0 <ftrace_regs_caller>

  5. // 后面这些如何被绕过呢?ftrace_regs_caller返回后如何被skip掉呢?这需要平衡堆栈的技巧!

  6. // 后面通过实例来讲如何平衡堆栈,绕过老的函数。

  7. 0xffffffff810b9455 <set_next_buddy+5>: push %rbp

  8. 0xffffffff810b9456 <set_next_buddy+6>: cmpq $0x0,0x150(%rdi)

  9. 0xffffffff810b945e <set_next_buddy+14>: mov %rsp,%rbp

  10. // 新的set_next_buddy:

  11. crash> dis ffffffffa0382410 4

  12. // 新函数则是ftrace_regs_caller最终要调用的函数

  13. 0xffffffffa0382410 <set_next_buddy>: nopl 0x0(%rax,%rax,1) [FTRACE NOP]

  14. 0xffffffffa0382415 <set_next_buddy+5>: push %rbp

  15. 0xffffffffa0382416 <set_next_buddy+6>: cmpq $0x0,0x150(%rdi)

  16. 0xffffffffa038241e <set_next_buddy+14>: mov %rsp,%rbp


这就是热补丁的原理了。

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

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

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

   
  1. #include <linux/module.h>

  2. #include <linux/proc_fs.h>


  3. // 下面的sample_read就是我将要patch的函数

  4. static ssize_t sample_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)

  5. {

  6. int n = 0;

  7. char kb[16];


  8. if (*ppos != 0) {

  9. return 0;

  10. }


  11. n = sprintf(kb, "%d\n", 1234);

  12. memcpy(ubuf, kb, n);

  13. *ppos += n;

  14. return n;

  15. }


  16. static struct file_operations sample_ops = {

  17. .owner = THIS_MODULE,

  18. .read = sample_read,

  19. };


  20. static struct proc_dir_entry *ent;

  21. static int __init sample_init(void)

  22. {

  23. ent = proc_create("test", 0660, NULL, &sample_ops);

  24. if (!ent)

  25. return -1;


  26. return 0;

  27. }


  28. static void __exit sample_exit(void)

  29. {

  30. proc_remove(ent);

  31. }


  32. module_init(sample_init);

  33. module_exit(sample_exit);

  34. MODULE_LICENSE("GPL");


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

   
  1. [root@localhost test]# insmod sample.ko

  2. [root@localhost test]# cat /proc/test

  3. 1234


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

   
  1. crash> dis sample_read 1

  2. 0xffffffffa038c000 <sample_read>: nopl 0x0(%rax,%rax,1) [FTRACE NOP]


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

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

   
  1. // hijack.c

  2. #include <linux/module.h>

  3. #include <linux/kallsyms.h>

  4. #include <linux/cpu.h>


  5. char *stub;

  6. char *addr = NULL;


  7. // 可以用JMP模式,也可以用CALL模式

  8. //#define JMP 1


  9. // 和sample模块里同名的sample_read函数

  10. static ssize_t sample_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)

  11. {

  12. int n = 0;

  13. char kb[16];


  14. if (*ppos != 0) {

  15. return 0;

  16. }

  17. // 这里我们把1234的输出给fix成4321的输出

  18. n = sprintf(kb, "%d\n", 4321);

  19. memcpy(ubuf, kb, n);

  20. *ppos += n;

  21. return n;

  22. }


  23. // hijack_stub的作用就类似于ftrace kpatch里的ftrace_regs_caller

  24. static ssize_t hijack_stub(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)

  25. {

  26. // 用nop占位,加上C编译器自动生成的函数header代码,这么大的函数来容纳stub应该够了。

  27. asm ("nop; nop; nop; nop; nop; nop; nop; nop;");

  28. return 0;

  29. }


  30. #define FTRACE_SIZE 5

  31. #define POKE_OFFSET 0

  32. #define POKE_LENGTH 5

  33. #define SKIP_LENGTH 8


  34. static unsigned long *(*_mod_find_symname)(struct module *mod, const char *name);

  35. static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);

  36. static struct mutex *_text_mutex;

  37. unsigned char saved_inst[POKE_LENGTH];

  38. struct module *mod;


  39. static int __init hotfix_init(void)

  40. {

  41. unsigned char jmp_call[POKE_LENGTH];

  42. unsigned char e8_skip_stack[SKIP_LENGTH];

  43. s32 offset, i = 5;


  44. mod = find_module("sample");

  45. if (!mod) {

  46. printk("没加载sample模块,你要patch个啥?\n");

  47. return -1;

  48. }

  49. _mod_find_symname = (void *)kallsyms_lookup_name("mod_find_symname");

  50. if (!_mod_find_symname) {

  51. printk("还没开始,就已经结束。");

  52. return -1;

  53. }

  54. addr = (void *)_mod_find_symname(mod, "sample_read");

  55. if (!addr) {

  56. printk("一切还没有准备好!请先加载sample模块。\n");

  57. return -1;

  58. }

  59. _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");

  60. _text_mutex = (void *)kallsyms_lookup_name("text_mutex");

  61. if (!_text_poke_smp || !_text_mutex) {

  62. printk("还没开始,就已经结束。");

  63. return -1;

  64. }


  65. stub = (void *)hijack_stub;


  66. offset = (s32)((long)sample_read - (long)stub - FTRACE_SIZE);


  67. // 下面的代码就是stub函数的最终填充,它类似于ftrace_regs_caller的作用!

  68. e8_skip_stack[0] = 0xe8;

  69. (*(s32 *)(&e8_skip_stack[1])) = offset;

  70. #ifndef JMP // 如果是call模式,则需要手工平衡堆栈,跳过原始函数的栈帧

  71. e8_skip_stack[i++] = 0x41; // pop %r11

  72. e8_skip_stack[i++] = 0x5b; // r11寄存器为临时使用寄存器,遵循调用者自行保护原则

  73. #endif

  74. e8_skip_stack[i++] = 0xc3;

  75. _text_poke_smp(&stub[0], e8_skip_stack, SKIP_LENGTH);


  76. offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);


  77. memcpy(&saved_inst[0], addr, POKE_LENGTH);

  78. #ifndef JMP

  79. jmp_call[0] = 0xe8;

  80. #else

  81. jmp_call[0] = 0xe9;

  82. #endif

  83. (*(s32 *)(&jmp_call[1])) = offset;

  84. get_online_cpus();

  85. mutex_lock(_text_mutex);

  86. _text_poke_smp(&addr[POKE_OFFSET], jmp_call, POKE_LENGTH);

  87. mutex_unlock(_text_mutex);

  88. put_online_cpus();


  89. return 0;

  90. }


  91. static void __exit hotfix_exit(void)

  92. {

  93. mod = find_module("sample");

  94. if (!mod) {

  95. printk("一切已经结束!\n");

  96. return;

  97. }

  98. addr = (void *)_mod_find_symname(mod, "sample_read");

  99. if (!addr) {

  100. printk("一切已经结束!\n");

  101. return;

  102. }

  103. get_online_cpus();

  104. mutex_lock(_text_mutex);

  105. _text_poke_smp(&addr[POKE_OFFSET], &saved_inst[0], POKE_LENGTH);

  106. mutex_unlock(_text_mutex);

  107. put_online_cpus();

  108. }


  109. module_init(hotfix_init);

  110. module_exit(hotfix_exit);

  111. MODULE_LICENSE("GPL");


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

   
  1. [root@localhost test]# insmod ./hijack.ko

  2. [root@localhost test]# cat /proc/test

  3. 4321


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

   
  1. crash> dis sample_read

  2. dis: sample_read: duplicate text symbols found:

  3. ffffffffa039d000 (t) sample_read [sample]

  4. ffffffffa03a2020 (t) sample_read [hijack]

  5. crash>


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

   
  1. // 先看老的sample_read,它的ftrace stub已经被改成了call hijack_stub

  2. crash> dis ffffffffa039d000 1

  3. 0xffffffffa039d000 <sample_read>: callq 0xffffffffa03a2000 <hijack_stub>

  4. // 再看新的sample_read,它就是最终被执行的函数

  5. crash> dis ffffffffa03a2020 1

  6. 0xffffffffa03a2020 <sample_read>: nopl 0x0(%rax,%rax,1) [FTRACE NOP]

  7. crash>


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

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

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


浙江温州皮鞋湿,下雨进水不会胖!

版权声明:本文为CSDN博主「dog250」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:

https://blog.csdn.net/dog250/article/details/105254739


-END-




推荐阅读



【01】10 大白帽黑客专用的 Linux 操作系统
【02】STM32好找工作,所以学linux终究是错付了吗?
【03】Linux内核中I2C总线及设备长啥样?这回终于搞清楚了!
【04】Linux如何调试内存泄漏?超牛干货奉献给你(代码全)
【05】嵌入式Linux驱动离不开的知识:深入解析Linux Platform_device


免责声明:整理文章为传播相关技术,版权归原作者所有,如有侵权,请联系删除

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

嵌入式ARM

扫描二维码,关注更多精彩内容

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭