linux动态替换内核函数,如何替换一个Linux内核函数的实现-热补丁原理

看题目, 替换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@@staticvoidset_next_buddy(structsched_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技术是可行的,比如下面文章里的方法:

https://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 ffffffff810b94504

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

0xffffffff810b9450:callq0xffffffff81646df0

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

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

0xffffffff810b9455:push%rbp

0xffffffff810b9456:cmpq $0x0,0x150(%rdi)

0xffffffff810b945e:mov%rsp,%rbp

// 新的set_next_buddy:

crash>dis ffffffffa03824104

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

0xffffffffa0382410:nopl0x0(%rax,%rax,1)[FTRACE NOP]

0xffffffffa0382415:push%rbp

0xffffffffa0382416:cmpq $0x0,0x150(%rdi)

0xffffffffa038241e:mov%rsp,%rbp

这就是热补丁的原理了。

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

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

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

#include

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

staticssize_tsample_read(structfile*file,char__user*ubuf,size_tcount,loff_t*ppos)

{

intn=0;

charkb[16];

if(*ppos!=0){

return0;

}

n=sprintf(kb,'%d\n',1234);

memcpy(ubuf,kb,n);

*ppos+=n;

returnn;

}

staticstructfile_operations sample_ops={

.owner=THIS_MODULE,

.read=sample_read,

};

staticstructproc_dir_entry*ent;

staticint__initsample_init(void)

{

ent=proc_create('test',0660,NULL,&sample_ops);

if(!ent)

return-1;

return0;

}

staticvoid__exitsample_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_read1

0xffffffffa038c000:nopl0x0(%rax,%rax,1)[FTRACE NOP]

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

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

// hijack.c

#include

#include

#include

char*stub;

char*addr=NULL;

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

//#define JMP 1

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

staticssize_tsample_read(structfile*file,char__user*ubuf,size_tcount,loff_t*ppos)

{

intn=0;

charkb[16];

if(*ppos!=0){

return0;

}

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

n=sprintf(kb,'%d\n',4321);

memcpy(ubuf,kb,n);

*ppos+=n;

returnn;

}

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

staticssize_thijack_stub(structfile*file,char__user*ubuf,size_tcount,loff_t*ppos)

{

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

asm('nop; nop; nop; nop; nop; nop; nop; nop;');

return0;

}

#defineFTRACE_SIZE5

#definePOKE_OFFSET0

#definePOKE_LENGTH5

#defineSKIP_LENGTH8

staticunsignedlong*(*_mod_find_symname)(structmodule*mod,constchar*name);

staticvoid*(*_text_poke_smp)(void*addr,constvoid*opcode,size_tlen);

staticstructmutex*_text_mutex;

unsignedcharsaved_inst[POKE_LENGTH];

structmodule*mod;

staticint__inithotfix_init(void)

{

unsignedcharjmp_call[POKE_LENGTH];

unsignedchare8_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;

#ifndefJMP// 如果是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);

#ifndefJMP

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();

return0;

}

staticvoid__exithotfix_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 ffffffffa039d0001

0xffffffffa039d000:callq0xffffffffa03a2000

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

crash>dis ffffffffa03a20201

0xffffffffa03a2020:nopl0x0(%rax,%rax,1)[FTRACE NOP]

crash>

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

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

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

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

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

原文链接:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值