内核动态补丁(katch)解释
本文阅读体验不好,因此做了pdf版本,点击下载,如果你没有分数,可以直接留言找我要pdf版本。
内核可以在运行时动态执行补丁中的代码(kpatch),而不需要重启后再运行补丁代码。对于kpatch的运行原理,可以参看[3], kpatch基于ftrace技术,可以在内核运行时动态的(即不需要重启)、整体替换某个函数(但需要暂时停止所有运行时进程)。如图1所示,具体的,当内核运行之前,加入原来的内核函数名字为func_original(),则内核编译时就先把patch做好(假设替换函数为func_replace()),同时编译进内核中。注意,图1中Original Function部分大小要>=Replacement Function部分的大小,这也导致在kpatch相关代码中,会添加一些nop指令,这些nop指令的作用就是填充指令段,用于对齐原来的函数
图1 kpatch的过程[3]
本文主要解释从gnu汇编原语角度如何实现kpatch,具体kpatch机制可以参考文献[3]。内核文件arch/arm64/include/asm/alternative.h中有一段汇编,涉及内核动态patch技术,以此为例介绍如下:
关键词:Linux kernel, 动态补丁,kpatch, .pushsection, .popsection, .previous, .org
阅读代码__tlbi()[arch/arm64/include/asm/tlbflush.h]
图2 __tlbi()定义
追踪__tlbi()定义如图2所示,如果__tlbi()函数中带其他参数,例如__tlbi(vale1is, addr);则通过上述宏会翻译成__TLBI_1(),否则翻译成__TLBI_0()。上图中CONFIG_QCOM_FALKOR_ERRATUM_1009=1, ARM64_WORKAROUND_REPEAT_TLBI=17[arch/arm64/include/asm/cpucaps.h],这也可以从图4预处理结果看到继续追踪ALTERNATIVE的定义
arch/arm64/include/asm/alternative.h:
244 #define ALTERNATIVE(oldinstr, newinstr, ...) \
245 _ALTERNATIVE_CFG(oldinstr, newinstr, __VA_ARGS__, 1)
63 #define _ALTERNATIVE_CFG(oldinstr, newinstr, feature, cfg, ...) \
64 __ALTERNATIVE_CFG(oldinstr, newinstr, feature, IS_ENABLED(cfg))
继续追踪__ALTERNATIVE_CFG定义
arch/arm64/include/asm/alternative.h
图3 __ALTERNATIVE_CFG的定义
图4 ALTINSTR_ENTRY和结构体alt_instr定义
为了理解上述宏定义,通过内核编译时修改编译选项-E得到预处理结果[4]如下:
图5 预处理结果
编译完成后如下:
图6 编译后结果
按照上述图3中宏定义,套入__tlbi(vale1is, addr),展开macro如下:
为了方便对比,把图3再次复制如下:
asm ("tlbi " "vale1is" ", %0\n"
.if 1 == 1
661:
nop //insn1
nop //insn1
662:
.pushsection .altinstructions, “a” //把下述代码,填充图4结构体alt_instr
.word 661b - . //存储被替换指令相对于当前地址的偏移:661b的地址减去当前地址(负)
.word 663f - . //存储插入指令相对于当前地址的偏移:663f的地址减去当前地址(正)
.hword 17 //以16进制存储值17,特征位,用于区分该部分代码针对哪个bug
.byte 662b-661b //存储被替换指令(两条nop)的长度
.byte 664f-663f //存储插入指令的长度(<=被替换指令的长度)
.popsection //把指向当前section altinstructions的指针,再次指向栈中上一个section
.pushsection .altinstr_replacement, "a” //把下述代码只读填充到section altinstr_replacement
663:
dsb ish //insn2
tlbi “vale1is" ", %0" //insn2
664:
.popsection //把指向当前section altinstructions的指针,再次指向栈中上一个section
//判断长度insn1==insn2,注意上图中的注释
.org . - (664b-663b) + (662b-661b) //当前地址-(664-663)+(662-661)
.org . - (662b-661b) + (664b-663b) //当前地址-(662-661)+(664-663),
.endif
: :"r" (addr));
上述代码就是一条asm嵌入汇编,但是其中包含了pushsecton/popsection等操作。
.pushsection .altinstructions, “a”:这句意思是,在section altinstruction上面继续push,并swtich到altinstructions上继续操作(可以认为把指针指向altinstructions了),并且当前section属性为allocatable,类似于汇编指令rodata,read only。后续指令一直到popsection之前,都是为了填充结构体alt_instr,该结构体用于指导内核运行时替换原来的执行代码。
popsection: 作用是把上述指针指向执行pushsection之前的section,一般之前的section都是.data
具体的,可以参考摘自[8]的描述:
The .section
directive switches the current target section to the one described by its arguments. The .pushsection
directive pushes the current target section onto a stack, and switches to the section described by its arguments. The .popsection
directive takes no arguments, and reverts the current target section to the previous one on the stack. The rest of the directives (.text
, .data
, .rodata
, .bss
) switch to one of the built-in sections.
If continuing a previous section, and the flags, type, or other arguments do not match the previous definition of the section, then the arguments of the current .section
directive will have no effect on the section. Instead, the assembler uses the arguments from the previous .section
directive. The assembler does not currently emit a diagnostic when this happens.
因此,上述pushsection和popsection的组合,思路是(以从.data section转换而来为例):从.data section转到当前altinstructions section,并以只读方式填充alt_instr(具体该结构的用法,请参考kpatch的实现[3]),然后把section指针再次指向.data section。再次从.data section跳转到altinstr_replacement,以只读方式填充该section,把未来将要用到的替换指令dsb/tlbi填充到当前section,再次退回到.data section。
真正动态使用该altinstr_replacement的流程:查询altinstructions section,获取用于补丁或替换原有指令的地址及偏移等信息(即alt_instr)通过该信息查询altinstr_replacement段,执行然后返回。
内核vmlinux.lds文件中对于两个段有定义:
crch/arm64/kernel/vmlinux.lds:
上述push/pop等操作就是对这两个section进行操作。
.previous:交换当前section和距离当前section最近的那个section的指针。如果连续使用两个previous,就会回到当前section
.puhsection name: 把当前目标section(也就是pushsection后面跟着的section名name)压栈到name所在的section的顶部,然后把section指针指向name所在的section
.popsection :不需要参数,直接把指向当前section得指针,指向上一个section
.section A
.subsection 1
# Now in section A subsection 1
.word 0x1234
.section B
.subsection 0
# Now in section B subsection 0
.word 0x5678
.subsection 1
# Now in section B subsection 1
.word 0x9abc
.previous
# Now in section B subsection 0
.word 0xdef0
上述命令会把0x1234插入sectionA,0x5678和0xdef0插入sectionB中subsection0,把0x9abc插入sectionB的subsection1。
原因:当执行到.previous时,当前的section是 section subsection 1,那么,previous会交换当前的section和距离当前section最近的那个section(section B subsection0)的指针,因此会把0xdef0插入section B的 subsection0中。
.data //进入section data,存储0
.byte 0
.section A //进入sectionA,存储1
.byte 1
.previous //再次进入section data,存储0
.byte 0
.previous //再次进入section data之前的section(sectionA),存储1
.byte 1
.pushsection B //把sectionB中的2压栈,并把当前section指针指向section B,
.byte 2
.previous //进入section A,存储1
.byte 1
.previous //进入section B,存储2
.byte 2
.pushsection C //把section C中的3压栈,并把当前section指针指向section C,
.byte 3
.previous //进入section B,存储2
.byte 2
.previous //进入section C,存储3
.byte 3
.popsection //弹出当前section C,恢复top层的section 指针,即进入section B,存储2
.byte 2
.previous //进入距离当前sectionB最近的那个section,即section A,存储1
.byte 1
.previous //进入距离当前section A最近的sectionB 存储2
.byte 2
.popsection //弹出当前section B,恢复top层section指针,进入sectionA,存储1
.byte 1
.previous //进入距离当前section A最近的section:data section,存储0
.byte 0
.previous //从data section 进入section A,存储1
.byte 1
上述操作,把所有的0值存储在.data section,把所有的1存储到section A,把所有的2存储到section B,把所有的3存储到section C
参考:
[1] https://en.wikipedia.org/wiki/Kpatch
[2] https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/alternative-asm.h
[3] kpatch: https://en.wikipedia.org/wiki/Kpatch
[4] 内核预处理:http://ju.outofmemory.cn/entry/271369
[5] popsection: http://web.mit.edu/rhel-doc/3/rhel-as-en-3/popsection.html
[6] pushsection: http://web.mit.edu/rhel-doc/3/rhel-as-en-3/pushsection.html
[7] section: https://sourceware.org/binutils/docs/as/Section.html#Section
[8] push/pop section ARM官方解释:
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0774i/bpl1510589893923.html
[9] push/previous example:http://www.sourceware.org/ml/binutils/2000-08/msg00043.html
[10] previous section操作举例:https://sourceware.org/binutils/docs/as/Previous.html#Previous
[11] previous section例2: https://www.sourceware.org/ml/binutils/2000-08/msg00043.html
477 CONFIG_QCOM_FALKOR_ERRATUM_1003=y
478 CONFIG_QCOM_FALKOR_ERRATUM_1009=y
479 CONFIG_QCOM_QDF2400_ERRATUM_0065=y