liveness配置_我心依旧的技术博客_51CTO博客 (2025)

内核kpatch机制是函数级的热替换技术,主要包含四大主件:

kpatch-build:用于将源码patch生成ko热补丁

patch module:指生成的ko热补丁,包括需要新的函数和被替换函数的记录信息

kpatch core module: kpatch核心代码模块,为新旧函数热替换提供接口,使用kpatch时候是kpatch.ko模块,使用livepatch的时候不存在,因为内核已经支持livepatch

kpatch utility: kpatch管理工具,主要是kpatch命令(kpatch list/load/unload 查询/加载/卸载)

kpatch-build作为生成热补丁的用户态工具,同时支持kpatch和livepatch的生成,具体哪个取决于CONFIG_LIVEPATCH,centos 4.18内核开始支持livepatch。

本文主要聚焦kpatch-build生成热补丁的流程和livepatch的工作原理,基于最新的kpatch-build工具(https://github.com/dynup/kpatch.git),目录树:

.├── contrib│ ├── kpatch.conf│ ├── kpatch.service│ ├── kpatch.spec│ └── Makefile├── COPYING├── doc│ └── patch-author-guide.md├── examples│ ├── tcp_cubic-better-follow-cubic-curve-converted.patch│ └── tcp_cubic-better-follow-cubic-curve-original.patch├── kmod│ ├── core│ │ ├── core.c│ │ ├── kpatch.h│ │ ├── Makefile│ │ └── shadow.c│ ├── Makefile│ └── patch│ ├── kpatch.h -> ../core/kpatch.h│ ├── kpatch.lds.S│ ├── kpatch-macros.h│ ├── kpatch-patch.h│ ├── kpatch-patch-hook.c│ ├── livepatch-patch-hook.c│ ├── Makefile│ └── patch-hook.c├── kpatch│ ├── kpatch│ └── Makefile├── kpatch-build│ ├── create-diff-object│ ├── create-diff-object.c│ ├── create-diff-object.d│ ├── create-diff-object.o│ ├── create-klp-module│ ├── create-klp-module.c│ ├── create-klp-module.d│ ├── create-klp-module.o│ ├── create-kpatch-module│ ├── create-kpatch-module.c│ ├── create-kpatch-module.d│ ├── create-kpatch-module.o│ ├── gcc-plugins│ │ ├── gcc-common.h│ │ ├── gcc-generate-rtl-pass.h│ │ └── ppc64le-plugin.c│ ├── insn│ │ ├── asm│ │ │ ├── inat.h│ │ │ ├── inat_types.h│ │ │ └── insn.h│ │ ├── inat.c│ │ ├── inat.d│ │ ├── inat.o│ │ ├── inat-tables.c│ │ ├── insn.c│ │ ├── insn.d│ │ └── insn.o│ ├── kpatch-build│ ├── kpatch-cc│ ├── kpatch-elf.c│ ├── kpatch-elf.d│ ├── kpatch-elf.h│ ├── kpatch-elf.o│ ├── kpatch.h│ ├── kpatch-intermediate.h│ ├── list.h│ ├── log.h│ ├── lookup.c│ ├── lookup.d│ ├── lookup.h│ ├── lookup.o│ └── Makefile├── Makefile├── Makefile.inc├── man│ ├── kpatch.1│ ├── kpatch.1.gz│ ├── kpatch-build.1│ ├── kpatch-build.1.gz│ └── Makefile├── README.md

 本文以如下简单patch为例,简要介绍下,livepatch的完整制作和加载流程。

 0001-xfs-debug-xfs-log.patch:

From e63f82904ace5c35aab5af05b17f00b949c28e6b Mon Sep 17 00:00:00 2001From: hanjinke <didiglobal.com>Date: Thu, 8 Apr 2021 15:18:04 +0800Subject: [PATCH] xfs: debug xfs logSigned-off-by: hanjinke <didiglobal.com>--- fs/xfs/xfs_log.c | 2 ++ 1 file changed, 2 insertions(+)diff --git a/fs/xfs/xfs_log.c b/fs/xfs/xfs_log.cindex 2466b0f5b..b2f8e0568 100644--- a/fs/xfs/xfs_log.c+++ b/fs/xfs/xfs_log.c@@ -3331,6 +3331,8 @@ xfs_log_force( XFS_STATS_INC(mp, xs_log_force); trace_xfs_log_force(mp, 0, _RET_IP_); +dump_stack();+ xlog_cil_force(log); spin_lock(&log->l_icloglock);--

 0001-xfs-debug-xfs-log.patch这个patch很简单,就是在xfs_log_force函数里添加了一行dump_stack(),用于打印同步xfs log的调用流程。

生成livepatch的命令: 

kpatch-build -v /usr/lib/debug/lib/modules/4.18.0-193.6.3.el8_2.x86_64/vmlinux -c /boot/config-4.18.0-193.6.3.el8_2.x86_64 -s kernel-4.18.0-193.6.3.el8_2/ 0001-xfs-debug-xfs-log.patch

 最终生成livepatch-0001-xfs-debug-xfs-log.ko,kpatch loadlivepatch-0001-xfs-debug-xfs-log.ko完成加载,kpatch list查询是否安装成功。

 kpatch-build生成livepatch的步骤如下:

 (一)找到更改的xfs_log.o

kpatch-build首先编译一遍orig内核,在打入0001-xfs-debug-xfs-log.patch,再编译一遍patched 内核。那么如何找到patch涉及到哪些.o文件的更改。

看下kpatch-buiild的编译内核的CC工具:

888 echo "Building original source" 889 [[ -n "$OOT_MODULE" ]] || ./scripts/setlocalversion --save-scmversion || die 890 unset KPATCH_GCC_TEMPDIR 891 892 KPATCH_CC_PREFIX="$TOOLSDIR/kpatch-cc " 893 declare -a MAKEVARS 894 if [ "$CONFIG_CC_IS_CLANG" -eq 1 ]; then 895 MAKEVARS+=("CC=${KPATCH_CC_PREFIX}clang") 896 MAKEVARS+=("HOSTCC=clang") 897 else 898 MAKEVARS+=("CC=${KPATCH_CC_PREFIX}gcc") 899 fi 900 901 if [ "$CONFIG_LD_IS_LLD" -eq 1 ]; then 902 MAKEVARS+=("LD=${KPATCH_CC_PREFIX}ld.lld") 903 MAKEVARS+=("HOSTLD=ld.lld") 904 else 905 MAKEVARS+=("LD=${KPATCH_CC_PREFIX}ld") 906 fi 907

  892行,CC的前缀是"$TOOLSDIR/kpatch-cc ",而898行CC=${KPATCH_CC_PREFIX}gcc") 也就是CC="$TOOLSDIR/kpatch-cc  gcc"。相当于每次执行CC的时候都会执行kpatch-cc脚本,相当于对CC进行了一次hook。

看下kpatch-cc脚本:

1 #!/bin/bash 2 3 if [[ ${KPATCH_GCC_DEBUG:-0} -ne 0 ]]; then 4 set -o xtrace 5 fi 6 TOOLCHAINCMD="$1" 7 shift 8 9 if [[ -z "$KPATCH_GCC_TEMPDIR" ]]; then 10 exec "$TOOLCHAINCMD" "$@" 11 fi 12 13 declare -a args=("$@") 14 15 if [[ "$TOOLCHAINCMD" =~ ^(.*-)?gcc$ || "$TOOLCHAINCMD" =~ ^(.*-)?clang$ ]] ; then 16 while [ "$#" -gt 0 ]; do 17 if [ "$1" = "-o" ]; then 18 obj="$2" 19 20 # skip copying the temporary .o files created by 21 # recordmcount.pl 22 [[ "$obj" = */.tmp_mc_*.o ]] && break; 23 24 [[ "$obj" = */.tmp_*.o ]] && obj="${obj/.tmp_/}" 25 relobj=${obj//$KPATCH_GCC_SRCDIR\//} 26 case "$relobj" in 27 *.mod.o|\ 28 *built-in.o|\ 29 *built-in.a|\ 30 vmlinux.o|\ 31 .tmp_kallsyms1.o|\ 32 .tmp_kallsyms2.o|\ 33 init/version.o|\ 34 arch/x86/boot/version.o|\ 35 arch/x86/boot/compressed/eboot.o|\ 36 arch/x86/boot/header.o|\ 37 arch/x86/boot/compressed/efi_stub_64.o|\ 38 arch/x86/boot/compressed/piggy.o|\ 39 kernel/system_certificates.o|\ 40 arch/x86/vdso/*|\ 41 arch/x86/entry/vdso/*|\ 42 drivers/firmware/efi/libstub/*|\ 43 arch/powerpc/kernel/prom_init.o|\ 44 arch/powerpc/kernel/vdso64/*|\ 45 lib/*|\ 46 .*.o|\ 47 */.lib_exports.o) 48 break 49 ;; 50 *.o) 51 echo "$relobj changed!!!!" >> /mnt/outlog 52 mkdir -p "$KPATCH_GCC_TEMPDIR/orig/$(dirname "$relobj")" 53 [[ -e "$obj" ]] && cp -f "$obj" "$KPATCH_GCC_TEMPDIR/orig/$relobj" 54 echo "$relobj" >> "$KPATCH_GCC_TEMPDIR/changed_objs" 55 break 56 ;; 57 *) 58 break 59 ;; 60 esac 61 fi 62 shift 63 done 64 elif [[ "$TOOLCHAINCMD" =~ ^(.*-)?ld || "$TOOLCHAINCMD" =~ ^(.*-)?ld.lld ]] ; then 65 while [ "$#" -gt 0 ]; do 66 if [ "$1" = "-o" ]; then 67 obj="$2" 68 relobj=${obj//$KPATCH_GCC_SRCDIR\//} 69 case "$obj" in 70 *.ko) 71 mkdir -p "$KPATCH_GCC_TEMPDIR/module/$(dirname "$relobj")" 72 cp -f "$obj" "$KPATCH_GCC_TEMPDIR/module/$relobj" 73 break 74 ;; 75 .tmp_vmlinux*|vmlinux) 76 args+=(--warn-unresolved-symbols) 77 break 78 ;; 79 *) 80 break 81 ;; 82 esac 83 fi 84 shift 85 done 86 fi 87 88 exec "$TOOLCHAINCMD" "${args[@]}"

 在编译orig 内核的时候,第9行,$KPATCH_GCC_TEMPDIR没有设置,脚本会在第10行返回,在编译patched kernel的时候,kpatch-build会设置$KPATCH_GCC_TEMPDIR,同时由于kennel是增量编译,我们修改了xfs_log.c文件,只会重新编译xfs_log.o文件,最终会进入case 2也就是50行,会把xfs_log.o文件拷贝到$KPATCH_GCC_TEMPDIR/orig/目录下,并把xfs_log.o这个名字写入$KPATCH_GCC_TEMPDIR/changed_objs,我们就拿到了被修改的.o文件xfs_log.o。

(二)、拿到xfs_log.o之后,如何知道xfs_log.o里的那些函数被修改了

现在拿到了新的xfs_log.o和原来的xfs_log.o文件,如何知道哪个函数被修改了并提取新修改的函数的代码片段。
   create-diff-object工具负责完成以上工作,主要实现位于kpatch/kpatch-build/create-diff-object.c。

 前面提到kptatch-build会编译origin kernel和patched kernel,这和普通的编译内核不太一样,主要区别kpatch-build编译时候使用gcc的”-ffunction-sections “和”-fdata-sections“

 -ffunction-sections “会把每个函数单独编译成一个elf的代码section,”-fdata-sections“把每个数据变量单独编译成一个elf的数据section。

 看下例子:

[root@localhost xfs]# readelf -S xfs_log.o | grep text.*PROGBITS [ 1] .text PROGBITS 0000000000000000 00000040 [ 4] .text.xlog_grant_ PROGBITS 0000000000000000 00000040 [ 8] .text.xlog_get_ic PROGBITS 0000000000000000 000007e0 [11] .text.xlog_space_ PROGBITS 0000000000000000 00000960 [13] .text.xlog_grant_ PROGBITS 0000000000000000 00000ad0 [15] .text.xlog_grant_ PROGBITS 0000000000000000 00000bd0 [19] .text.xlog_state_ PROGBITS 0000000000000000 00001460 [21] .text.xlog_bdstra PROGBITS 0000000000000000 00001570 [25] .text.xlog_alloc_ PROGBITS 0000000000000000 00001770 [27] .text.xlog_deallo PROGBITS 0000000000000000 00002010 [30] .text.xlog_state_ PROGBITS 0000000000000000 00002210 [36] .text.xlog_grant_ PROGBITS 0000000000000000 00002310 [40] .text.xlog_regran PROGBITS 0000000000000000 00002500 [42] .text.xlog_pack_d PROGBITS 0000000000000000 00002850 [44] .text.xlog_state_ PROGBITS 0000000000000000 000029b0 [46] .text.xlog_iclogs PROGBITS 0000000000000000 00002a00 [48] .text.xlog_get_lo PROGBITS 0000000000000000 00002a50 [50] .text.xlog_state_ PROGBITS 0000000000000000 00002ad0 [52] .text.xlog_grant_ PROGBITS 0000000000000000 00002c00 [58] .text.xlog_grant_ PROGBITS 0000000000000000 00003c30 [61] .text.xlog_state_ PROGBITS 0000000000000000 00003d00 [63] .text.xlog_state_ PROGBITS 0000000000000000 00004140 [65] .text.xlog_iodone PROGBITS 0000000000000000 00004260 [67] .text.xlog_grant_ PROGBITS 0000000000000000 000043c0 [69] .text.xfs_log_reg PROGBITS 0000000000000000 00004580 [72] .text.xfs_log_not PROGBITS 0000000000000000 00004940 [76] .text.xfs_log_mou PROGBITS 0000000000000000 00004cd0 [78] .text.xfs_log_ite PROGBITS 0000000000000000 00005320 [80] .text.xfs_log_spa PROGBITS 0000000000000000 00005390 [82] .text.xlog_ungran PROGBITS 0000000000000000 00005590 [84] .text.xlog_assign PROGBITS 0000000000000000 000058b0 [86] .text.xlog_assign PROGBITS 0000000000000000 000059d0 [88] .text.xfs_log_wor PROGBITS 0000000000000000 00005a70 [90] .text.xlog_cksum PROGBITS 0000000000000000 00005b10 [93] .text.xlog_sync PROGBITS 0000000000000000 00005c50 [95] .text.xlog_state_ PROGBITS 0000000000000000 00006260

 每个函数都是单独的代码段,而正常的编译内核一个.o目标文件只有一个text section,包含本文件内所有函数汇编。

 总体来说,create-diff-object工具完成以下几种工作:

 1.create-diff-object工具使用有七个参数:

  参数1:origin_obj,原始xfs_log.o的路径

  参数2:patched_obj,patch之后xfs_log.o的路径

参数3: parent_name,一般是patch所在的模块名字,如果不是模块则是vmlinux。本文中是xfs

参数4:parent_symtab,patch之后,xfs_ko.symtab,包含xfs模块符号表(由readelf -s xfs.ko > xfs_ko.symtab,此xfs.ko是patched后的)

  参数5:mod_symvers,patch之后内核的Module.symvers,包含内核的所有导出符号的校验信息

  参数6:patch_name,要生成的livapatch的名字,本文是livepatch_0001_debug_xfs

  参数7:output_name,输出文件的路径,输出文件依然是xfs_log.o

2.elf解析工作

  create_diff_object中一个elf用kpatch_elf表示:

struct kpatch_elf { Elf *elf;           // 标准ELF描述符 struct list_head sections; // section的链表 struct list_head symbols;  // 符号链表 struct list_head strings;  // 字符串链表 int fd;             // elf文件的打开描述符       };

kpatch_elf_open主要负责解析elf文件,分别对origin和patched xfs_log.o进行解析。

根据ellf文件格式解析出所有的section放入kpatch_elf的section链表。

从.symtab解析出所有symbol放入kpatch-elf的symbols链表。

对于重定位的section,解析出每个rela挂入相应sec->relas链表。

扫描每个func类型的symblol所在的section的所有rela,如果有__fentry_的重定位项,设置sym->has_func_profiling=1,表明此函数可以被ftrace跟踪,也就可以被patch,因为kpatch/livepatch是基于ftrace机制。__fentry__重定位项位于函数首部,内核初始化或者模块加载的时候__fentry__类型的重定位项会被替换为5个nop指令。

3.符号解析工作

lookup_open负责符号解析工作,解析结果放入struct lookup_table:

struct lookup_table { int obj_nr, exp_nr; struct object_symbol *obj_syms; struct export_symbol *exp_syms; struct object_symbol *local_syms; char *objname; };

首先解析内核的Module.symvers文件,Module.symvers文件存放vmlinux的所有导出符号的名字和对应函数的crc校验,解析结果放入lookup_table->exp_syms,相当于内核导出符号表。

接下来解析patch后的xfs.ko的符号表,结果存在在loopup_table->obj_syms。

最后,找到xfs_log.c所对应的symbol给lookup_table->local_sym,这也是xfs_log.c文件内local的起始位置。以后查找xfs_log.c内的local symbol是从这里开始查找。

4.找出改变的函数

找出改变的函数,也就是找出具体需要patch的函数,这是是create-diff-object的关键。

主要分两步完成:

  第一步,通过kpatch_correlate_elfs函数完成原始的xfs_log.o和patched的xfs_log.o的section的配对。

  前面提过,由于gcc编译选项”-ffunction-sections“,每个函数对应一个单独的text section,例如函数xlog_sync对应的text section的名字为.text.xlog_sync。

kpatch_correlate_elfs遍历oriigin xfs_log.o和patched xfs_log.o的所有func类型的section,如果section的名字相同,说明就是相同函数的section,original和patched xfs_log.o的同一函数的section被成为twin。

  function类型的section如果没有找到相应的twin,说明是patch的的新函数,sec->status设置为NEW。

第二步,通过kpatch_compare_correlated_elements函数比较每个twin中的两个section是否相同来确定函数是否改变。

  快速比较sec->sh.sh_size和sec->data->d_size,即比较sec的hdr的长度和内容长度,如果不相等设置sec->status = CHANGED,如果相等再进行sec->data->d_buf的sec的内容的全比较。

  最后根据sec的status来设置相应的symbol的status。

5.构建livepatchsymbol和livepatchrelocation

最终生成livepatch的本质是ko文件,ko文件是在模块加载时候进行重定位操作,类似动态库加载。

  不同的是,普通内核模块除了使用内部local 符号外,对于外部符号,只能引用内核导出符号,模块的加载机制也只能解析内核导出符号,并进行重定位操作。

  对于livepatch,patched的函数不可避免的要使用大量本地符号和内核未导出符号,这样原有的模块加载机制就无法工作。

  为此,livepatch给自己开了后门,定义了自己的livepatchsymbol和livepatchrelocation。

livepatch symbol指livepatch引用的普通模块加载机制无法解析的符号集,livepatchrelocation 是引用livapatch symbols的重定位项。

  kpatch_create_intermediate_sections函数负责构建.kpatch.relocations和.kpatch.symbol两个section。

static void kpatch_create_intermediate_sections(struct kpatch_elf *kelf, struct lookup_table *table, char *objname, char *pmod_name){ int nr, index; struct section *sec, *ksym_sec, *krela_sec; struct rela *rela, *rela2, *safe; struct symbol *strsym, *ksym_sec_sym; struct kpatch_symbol *ksyms; struct kpatch_relocation *krelas; struct lookup_result symbol; bool special; bool vmlinux = !strcmp(objname, "vmlinux"); struct special_section *s; /* count rela entries that need to be dynamic */ nr = 0; list_for_each_entry(sec, &kelf->sections, list) { if (!is_rela_section(sec)) continue; if (!strcmp(sec->name, ".rela.kpatch.funcs")) { continue; } list_for_each_entry(rela, &sec->relas, list) { /* upper bound on number of kpatch relas and symbols */ nr++; /* * We set 'need_dynrela' here in the first pass because * the .toc section's 'need_dynrela' values are * dependent on all the other sections. Otherwise, if * we did this analysis in the second pass, we'd have * to convert .toc dynrelas at the very end. * * Specifically, this is needed for the powerpc * internal symbol function pointer check which is done * via .toc indirection in need_dynrela(). */ if (need_dynrela(table, rela)) toc_rela(rela)->need_dynrela = 1; } } /* create .kpatch.relocations text/rela section pair */ krela_sec = create_section_pair(kelf, ".kpatch.relocations", sizeof(*krelas), nr); krelas = krela_sec->data->d_buf; /* create .kpatch.symbols text/rela section pair */ ksym_sec = create_section_pair(kelf, ".kpatch.symbols", sizeof(*ksyms), nr); ksyms = ksym_sec->data->d_buf; /* create .kpatch.symbols section symbol (to set rela->sym later) */ ALLOC_LINK(ksym_sec_sym, &kelf->symbols); ksym_sec_sym->sec = ksym_sec; ksym_sec_sym->sym.st_info = GELF_ST_INFO(STB_LOCAL, STT_SECTION); ksym_sec_sym->type = STT_SECTION; ksym_sec_sym->bind = STB_LOCAL; ksym_sec_sym->name = ".kpatch.symbols"; /* lookup strings symbol */ strsym = find_symbol_by_name(&kelf->symbols, ".kpatch.strings"); if (!strsym) ERROR("can't find .kpatch.strings symbol"); /* populate sections */ index = 0; list_for_each_entry(sec, &kelf->sections, list) { if (!is_rela_section(sec)) continue; if (!strcmp(sec->name, ".rela.kpatch.funcs") || !strcmp(sec->name, ".rela.kpatch.relocations") || !strcmp(sec->name, ".rela.kpatch.symbols")) continue; special = false; for (s = special_sections; s->name; s++) if (!strcmp(sec->base->name, s->name)) special = true; list_for_each_entry_safe(rela, safe, &sec->relas, list) { if (!rela->need_dynrela) continue; /* * Starting with Linux 5.8, .klp.arch sections are no * longer supported: now that vmlinux relocations are * written early, before paravirt and alternative * module init, .klp.arch is technically not needed. * * For sanity we just need to make sure that there are * no .klp.rela.{module}.{section} sections for special * sections. Otherwise there might be ordering issues, * if the .klp.relas are applied after the module * special section init code (e.g., apply_paravirt) * runs due to late module patching. */ if (!KLP_ARCH && !vmlinux && special) ERROR("unsupported dynrela reference to symbol '%s' in module-specific special section '%s'", rela->sym->name, sec->base->name); if (!lookup_symbol(table, rela->sym->name, &symbol)) ERROR("can't find symbol '%s' in symbol table", rela->sym->name); log_debug("lookup for %s: obj=%s sympos=%lu", rela->sym->name, symbol.objname, symbol.sympos); /* Fill in ksyms[index] */ if (vmlinux) ksyms[index].src = symbol.addr; else /* for modules, src is discovered at runtime */ ksyms[index].src = 0; ksyms[index].sympos = symbol.sympos; ksyms[index].type = rela->sym->type; ksyms[index].bind = rela->sym->bind; /* add rela to fill in ksyms[index].name field */ ALLOC_LINK(rela2, &ksym_sec->rela->relas); rela2->sym = strsym; rela2->type = ABSOLUTE_RELA_TYPE; rela2->addend = offset_of_string(&kelf->strings, rela->sym->name); rela2->offset = (unsigned int)(index * sizeof(*ksyms) + \ offsetof(struct kpatch_symbol, name)); /* add rela to fill in ksyms[index].objname field */ ALLOC_LINK(rela2, &ksym_sec->rela->relas); rela2->sym = strsym; rela2->type = ABSOLUTE_RELA_TYPE; rela2->addend = offset_of_string(&kelf->strings, symbol.objname); rela2->offset = (unsigned int)(index * sizeof(*ksyms) + \ offsetof(struct kpatch_symbol, objname)); /* Fill in krelas[index] */ if (is_gcc6_localentry_bundled_sym(rela->sym) && rela->addend == (int)rela->sym->sym.st_value) rela->addend -= rela->sym->sym.st_value; krelas[index].addend = rela->addend; krelas[index].type = rela->type; krelas[index].external = !vmlinux && symbol.exported; /* add rela to fill in krelas[index].dest field */ ALLOC_LINK(rela2, &krela_sec->rela->relas); if (sec->base->secsym) rela2->sym = sec->base->secsym; else ERROR("can't create dynrela for section %s (symbol %s): no bundled or section symbol", sec->name, rela->sym->name); rela2->type = ABSOLUTE_RELA_TYPE; rela2->addend = rela->offset; rela2->offset = (unsigned int)(index * sizeof(*krelas) + \ offsetof(struct kpatch_relocation, dest)); /* add rela to fill in krelas[index].objname field */ ALLOC_LINK(rela2, &krela_sec->rela->relas); rela2->sym = strsym; rela2->type = ABSOLUTE_RELA_TYPE; rela2->addend = offset_of_string(&kelf->strings, objname); rela2->offset = (unsigned int)(index * sizeof(*krelas) + \ offsetof(struct kpatch_relocation, objname)); /* add rela to fill in krelas[index].ksym field */ ALLOC_LINK(rela2, &krela_sec->rela->relas); rela2->sym = ksym_sec_sym; rela2->type = ABSOLUTE_RELA_TYPE; rela2->addend = (unsigned int)(index * sizeof(*ksyms)); rela2->offset = (unsigned int)(index * sizeof(*krelas) + \ offsetof(struct kpatch_relocation, ksym)); /* * Mark the referred to symbol for removal but * only if it is not from this object file. * The symbols from this object file may be needed * later (for example, they may have relocations * of their own which should be processed). */ if (!rela->sym->sec) rela->sym->strip = 1; list_del(&rela->list); free(rela); index++; } } /* set size to actual number of ksyms/krelas */ ksym_sec->data->d_size = index * sizeof(struct kpatch_symbol); ksym_sec->sh.sh_size = ksym_sec->data->d_size; krela_sec->data->d_size = index * sizeof(struct kpatch_relocation); krela_sec->sh.sh_size = krela_sec->data->d_size;}

函数主要流程:

 <1>.19-45行,遍历所有rela的section中的所有重定位项,找出需要livepatch自己重定位的重定位项,设置rela->need_dynrela=1筛选函数是need_dynrela函数。

 need_dynrela函数不做展开,主要有以下标准:

 patch中引入的新函数,不需要dynrela,普通的rela就可以。

 patch的object(xfs_log.o)中的本地函数和全局但未导出的函数,需要dynrela。

 位于vmlinux中的导出符号不需要dynrela,但其他模块中的导出符号需要dynrela,主要防止在加载livepatch的时候,这个函数所在的模块还没加载,那样是无法找到它,也就无法进行重定位。

<2>.创建.kpatch.symbol段和.kpatch.reloctions段

需要说明的是kpatch.symbol是指上文中的livapatch symbol,.kpatch.relocations是指livepatch relocation。这些符号和引用他们的重定位项普通的内核模块加载机制无法处理,需要特殊处理所以需要单独成段,以区别普通的符号表和重定位段。

遍历xfs_log.o的所有的section,对每个section的重定位段,只处理need_dynrela的重定位项(livepatch relocation)。

针对每个rela和它引用的symbol,rela加入.kpatch.relacation段,symbol加入.kpatch.symbol段。

之后针对每个rela和symbol创建若干个相关联的重定位段,比如:

针对rela的dest成员创建一个rela,用于修正rela的offset成员。

针对rela的object_name创建一个rela,用于修正rela的object的object_name。

针对rela的sym创建一个rela创建一个rela,用于修正rela的sym成员。

同样针对每个symbol的object_name和name成员分别创建两个rela,用于修正symbol的object_name和name成员。

这些新创建的rela由create-klp-module工具来进行重定位操作,用来还原真实的livepatch rela。rela的生成和重定位也是create-diff-object和create-klp-module约定的操作,并不是elf标准。

之所以rela和symbol本身需要自己的重定位项,是因为create-diff-object最后会生成output.o的时候,symtab表、shstrtab和strtab表都会发生变化(比如strip掉无用的symbol),这样原有的rela和symbol就不准确了。

<3>.生成.kpatch.function段

.kpatch.function段包含的是changged的function,前面第5步说明了找到changed function的过程,每个changed function对应一个struct kpatch_patch_func:

struct kpatch_patch_func { unsigned long new_addr; //新函数的地址 unsigned long new_size; //新函数的大小 unsigned long old_addr; //旧函数地址 unsigned long old_size; //旧函数大小 unsigned long sympos; //在.kpatch.symbol中的index char *name; //函数名字 char *objname; //函数所在的文件的.o的名字 };

根据changed func的个数创建.kpatch.function段,然后为每个changed func生成一个struct kpatch_patch_func,放入.kpatch.function段中即可。

6.生成xfs_log.o

create-diff-object工具比较origined xfs_log.o和patched xfs_log.o最终生成xfs_log.o。

最终生成的xfs_log.o elf文件主要组件具体包括:

<1>.shstrtab、strtab、rodata等这些来自xfs_log.o的段。

<2>.changed func对应的text section、rela section和对应的symbol要包含。orgined和patched xfs_log.o中相同的section及其对应的rela section和symbol,则不需要包含。

因此最终xfs_log.o的shrstrtab(段名字符串表)和strtab(字符串表)相对于patched xfs_log.o也会做相应的瘦身。

<3>.livepatch特有的.kpatch.function段,.kpatch.relocation段和.kpatch.symbol段,分别是changed function列表段,引用livepatch symbol的重定位项组成的段和livepatch symbol段。

(三).生成livepatch的ko文件(livepatch-0001-debug-xfs.ko)

  kpatch-build里面把cearte-diff-object所有的输出.o文件拷贝到kpatch/kmod/patch目录下,本文的例子只有xfs_log.o,实际上可能很多。patch目录树:

patch├── kpatch.h -> ../core/kpatch.h├── kpatch.lds.S├── kpatch-macros.h├── kpatch-patch.h├── kpatch-patch-hook.c├── livepatch-patch-hook.c├── Makefile└── patch-hook.c

livepatch-patch-hook.c和kpatch-patch-hook.c主要包含ko的初始化函数和卸载函数,livepatch机制使用livepatch-patch-hook.c文件。

kpatch-build首先把所有的create-diff-object的输出.o文件使用ld命令链接为一个ouput.o文件。patch目录下make,livepatch-patch-hook.c生成patch-hook.o,最终依赖output.o和patch-hook.o生成livepatch-0001-debug-xfs.ko文件。

(四).create-klp-module生成最终的livepatch的ko文件

create-klp-module负责生成符合livepatch module elf规范的liveptach ko,livepatch module elf规范参照内核文档Documentation/livepatch/module-elf-format.txt。

具体来说,create-klp-module工作如下:

1.完成.kpatch.symbol和.kpatch.relocation的重定位工作

前面说过,create-diff-object工具针对每个livepatch symbol和livepatch relocation的都生成多个重定位项。这里create-klp-module根据.rela.kpatch.symbol和.rela.kpatch.relocation完成对.kpatch.symbol和.kpatch.relocation的重定位工作,还原出真实的.kpatch.symbol和.kpatch.relocation。

2..kpatch.symbol段和.kpatch.relocation段的最终归属

.kpatch.symbol全部归入符号表.symtab,.kpatch.relocation用来生成.klp.rela段,以本文为例生成.klp.rela.xfs.text段。

对于.kpatch.symbol,全部加入.symtab,由于.kpatch.symbol会存在多个重复的livepatch symbol,所以加入的时候要完成去重。并且对于每个livepatch symbol需要在完成重命名之后再加入.symtab,重命名规则:name命名之后,.klp.sym.name.objname,pos。

.klp.sym:表明这是个livepatch symbol

name: 符号名字

objname: 符号所在的object的名字,内核里符号是vmlinux,内核模块是模块名字

pos: 对于local符号,表示在object中的index,否则是0

对于.kpatch.relocation,用来生成.klp.rela.xfs.text段,这是livepatch ko的elf的专有的段。

3.重新布局livepatch ko的elf,生成最终的livepatch ko文件

.kpatch.symbol/.rela.kpatch.symbol/.kpatch.relocation/.rela.kpatch.relocation这些section不再需要,无需在写入新的elf文件。同时又产生了新的段.klp.rela.xfs.text,所以需要重建节头名字字符串表.shstrtab。

在第2步中,把所有的livepatch symbol全部加入到了.symtab中,这改变了.symtab的布局,需要调用kpatch_reindex_elements对symtab中的symbol重新进行编号,使symbol->index正确反应其在.symtab中的位置。symbol在symtab中的index变了之后,需要调用kpatch_rebuild_rela_section_data函数对每一个重定位段进行修正,具体来说修改其中每个重定位项的r_info字段,使其指向其引用符号的在symtab中的新位置。

同时由于symtab的改变,相应的symbol的字符串表.strtab也需要扩容,把新加入的livepatch symbol的名字全部加进去。

比较重要的一点,在新的符号表.symtab中,对于未定义符号(sym->sec为空),如果是livepatch symbol,symbol的st_shndx 为SHN_LIVEPATCH,对于正常的未定义符号,symbol的st_shndx为SHN_UNDEF。在livepatch ko加载的时候,前者由livepatch的专有机制去解析,后者由内核模块的加载机制去解析。

最后生成新的.shstrtab、.symtab和.strtab,重新布局老的elf后,写入新的livepatch ko文件,生成最终的livepatch ko文件。

(五).livepatch ko的动态链接

本文为例livepatch ko文件是livepatch-0001-debug-xfs.ko从elf文件格式的角度来看,livepatch ko文件本质是内核ko文件,但同时又具有livepatch的血统,因此加载又与普通的内核模块加载流程有所不同。本章节主要从动态加载的角度来看下livepatch ko文件在加载的时候的链接过程。

总体来说,livepatch ko的链接主要是包括livepatch相关的链接和正常内核ko的链接两部分。

1.livepatch相关的链接

livepatch的链接包括livepatch symbol的符号解析和livepatch relocation的重定位。符号解析和重定位都是在在klp_write_object_relocations函数里完成。

调用栈:

klp_enable_patch klp_init_patch klp_init_object klp_init_object_loaded klp_write_object_relocations

klp_write_object_relocations函数:

static int klp_write_object_relocations(struct module *pmod, struct klp_object *obj){ int i, cnt, ret = 0; const char *objname, *secname; char sec_objname[MODULE_NAME_LEN]; Elf_Shdr *sec; if (WARN_ON(!klp_is_object_loaded(obj))) return -EINVAL; objname = klp_is_module(obj) ? obj->name : "vmlinux"; /* For each klp relocation section */ for (i = 1; i < pmod->klp_info->hdr.e_shnum; i++) { sec = pmod->klp_info->sechdrs + i; secname = pmod->klp_info->secstrings + sec->sh_name; if (!(sec->sh_flags & SHF_RELA_LIVEPATCH)) continue; /* * Format: .klp.rela.sec_objname.section_name * See comment in klp_resolve_symbols() for an explanation * of the selected field width value. */ cnt = sscanf(secname, ".klp.rela.%55[^.]", sec_objname); if (cnt != 1) { pr_err("section %s has an incorrectly formatted name\n", secname); ret = -EINVAL; break; } if (strcmp(objname, sec_objname)) continue; ret = klp_resolve_symbols(sec, pmod); if (ret) break; ret = apply_relocate_add(pmod->klp_info->sechdrs, pmod->core_kallsyms.strtab, pmod->klp_info->symndx, i, pmod); if (ret) break; } return ret;}

只处理sh_flags有SHF_RELA_LIVEPATCH标志的节,前面说过这些节是livepatch的特有的重定位节,名字格式为.rela.klp.objname。

调用klp_resolve_symbols函数进行符号解析,apply_relocate_add函数完成重定位工作。

klp_resolve_symbols函数:

static int klp_resolve_symbols(Elf_Shdr *relasec, struct module *pmod){ int i, cnt, vmlinux, ret; char objname[MODULE_NAME_LEN]; char symname[KSYM_NAME_LEN]; char *strtab = pmod->core_kallsyms.strtab; Elf_Rela *relas; Elf_Sym *sym; unsigned long sympos, addr; BUILD_BUG_ON(MODULE_NAME_LEN < 56 || KSYM_NAME_LEN != 128); relas = (Elf_Rela *) relasec->sh_addr; /* For each rela in this klp relocation section */ for (i = 0; i < relasec->sh_size / sizeof(Elf_Rela); i++) { sym = pmod->core_kallsyms.symtab + ELF_R_SYM(relas[i].r_info); if (sym->st_shndx != SHN_LIVEPATCH) { pr_err("symbol %s is not marked as a livepatch symbol\n", strtab + sym->st_name); return -EINVAL; } /* Format: .klp.sym.objname.symname,sympos */ cnt = sscanf(strtab + sym->st_name, ".klp.sym.%55[^.].%127[^,],%lu", objname, symname, &sympos); if (cnt != 3) { pr_err("symbol %s has an incorrectly formatted name\n", strtab + sym->st_name); return -EINVAL; } /* klp_find_object_symbol() treats a NULL objname as vmlinux */ vmlinux = !strcmp(objname, "vmlinux"); ret = klp_find_object_symbol(vmlinux ? NULL : objname, symname, sympos, &addr); if (ret) return ret; sym->st_value = addr; } return 0;} 205,4 16%

遍历重定位节的每个rela项,找到每个rela引用的symbol,每个symbol的st_shndx必须为SHN_LIVEPATCH,代表这是个livepatch symbol,否则出错。

每个livepatch symbol的名字格式为: .klp.sym.objname,name.pos,根据symbol的nam解析出符号的objname,name和pos。

调用klp_find_object_symbol进行符号查找,如果内核符号,调用kallsyms_on_each_symbol进行查找,如果是模块内部符号调用module_kallsyms_on_each_symbol在相应的模块里进查找。

无论那种查找,都会进行objname(内核符号不用),name和pos的全匹配,livepatch symbol都不是导出符号,仅仅根据名字进行查找,不一定全局唯一,[objname、name、pos]全匹配确保精准查找。

找到之后符号地址赋值给sym->st_value。

重定位工作由apply_relocate_add函数完成。重定位是对引用的函数和变量根据实际的地址进行修正。函数代码比较简单,还是通过本文的livepatch-0001-debug-xfs.ko例子进行说明。

查看livepatch-0001-debug-xfs.ko中的livepatch relocation项:

readelf -r livepatch-0001-debug-xfs.ko | grep klp.sym000000000032 007400000002 R_X86_64_PC32 0000000000000000 .klp.sym.xfs.xfsstats, - 400000000008f 007500000002 R_X86_64_PC32 0000000000000000 .klp.sym.xfs.xlog_cil_ - 40000000000e8 007400000002 R_X86_64_PC32 0000000000000000 .klp.sym.xfs.xfsstats, - 4000000000238 004b00000002 R_X86_64_PC32 0000000000000000 .klp.sym.xfs.xlog_stat - 4000000000250 004c00000002 R_X86_64_PC32 0000000000000000 .klp.sym.xfs.xlog_stat - 400000000029d 007600000002 R_X86_64_PC32 0000000000000000 .klp.sym.xfs.__tracepo + 240000000002d7 004b00000002 R_X86_64_PC32 0000000000000000 .klp.sym.xfs.xlog_stat - 4

上图有7个livepatch relocation,其中6个是对函数引用的重定位,1个是对tracepoint的重定位,重定位类型都是R_X86_64_PC32。R_X86_64_PC32是x86_64平台上的对一个使用PC相对地址的引用,这个offset是32位的。X86_64是指x86_64平台,PC32表示这是个pc相对地址引用且offset范围是32位。

R_X86_64_PC32类型的重定位公式为:S+A-P

S:符号的实际地址

  A:加数(addend,也称修正值)

  P:重定位项所在的地址

  PC相对寻址是目标符号到本指令下一条指令的地址,S+A-P = S-(P-A),在上图中,最后一列的数字为addend,S+A -P = S-(P-A) = S -(P+4)。

套用公式在函数中apply_relocate_add中修正如下:

  *(u32 *)loc = sym->st_value + rel[i].r_addend

2.正常的模块动态链接

  livepatch本质是内核模块,除了livepatch relocation的链接,还有属于正常模块的动态链接。

  正常模块动态链接在load_module函数里处理,调用栈:

do_syscall_64  __do_sys_finit_module     load_module

  其中,simplify_symbols函数负责符号解析,apply_relocations函数负责重定位。

  simplify_symbols主要处理st_shndx为SHN_UNDEF的符号,SHN_LIVEPATCH的符号上一步已经处理了。

  simplify_symbol函数最终调用find_symbol进行symbol的查找工作。因为正常模块的未定义符号只能是导出符号,symbol的名字是全局唯一的,故仅根据符号本身的名字就可以找到。

simplify_symbol函数首先在内核导出符号表ksymtab里查找,没找到的话遍历系统已加载的模块,在模块的导出符号表里查找。

  apply_relocations函数负责重定位,最终调用apply_relocate_add进行重定位。

  举例来说,livepatch-0001-debug-xfs.ko调用了内核函数dump_stack函数:

readelf -r livepatch-0001-debug-xfs.ko | grep dump_stack0000000000a7 005e00000002 R_X86_64_PC32 0000000000000000 dump_stack - 4

  重定位类型为R_X86_64_PC32,具体的重定位方法前面说过。

(六).livepatch的函数的热替换

  网上有关的介绍比较多,这里只简要写下流程。livepatch使用了ftrace机制来实现函数热替换,所以这里以ftarce的框架来说明。

  1.ftrace一级hook构造

  一级hook就是ftrace的通用trampoline,用于在ftrace使能之后,使用callq指令跳转到的位置。

  livepatch中trampoline的创建流程如下:

entry_SYSCALL_64_after_hwframe do_syscall_64 load_module do_init_module do_one_initcall patch_init klp_enable_patch klp_patch_object register_ftrace_function ftrace_startup __register_ftrace_function arch_ftrace_update_trampoline create_trampoline

  trampoline的构造在create_trampoline函数,这个函数以内核里ftrace_regs_caller函数的汇编为蓝本来制作trampoline。ftrace_regs_caller函数的定义如下:

ENTRY(ftrace_regs_caller) /* Save the current flags before any operations that can change them */ pushfq /* added 8 bytes to save flags */ save_mcount_regs 8 /* save_mcount_regs fills in first two parameters */GLOBAL(ftrace_regs_caller_op_ptr) /* Load the ftrace_ops into the 3rd parameter */ movq function_trace_op(%rip), %rdx /* Save the rest of pt_regs */ movq %r15, R15(%rsp) movq %r14, R14(%rsp) movq %r13, R13(%rsp) movq %r12, R12(%rsp) movq %r11, R11(%rsp) movq %r10, R10(%rsp) movq %rbx, RBX(%rsp) /* Copy saved flags */ movq MCOUNT_REG_SIZE(%rsp), %rcx movq %rcx, EFLAGS(%rsp) /* Kernel segments */ movq $__KERNEL_DS, %rcx movq %rcx, SS(%rsp) movq $__KERNEL_CS, %rcx movq %rcx, CS(%rsp) /* Stack - skipping return address and flags */ leaq MCOUNT_REG_SIZE+8*2(%rsp), %rcx movq %rcx, RSP(%rsp) /* regs go into 4th parameter */ leaq (%rsp), %rcxGLOBAL(ftrace_regs_call) call ftrace_stub /* Copy flags back to SS, to restore them */ movq EFLAGS(%rsp), %rax movq %rax, MCOUNT_REG_SIZE(%rsp) /* Handlers can change the RIP */ movq RIP(%rsp), %rax movq %rax, MCOUNT_REG_SIZE+8(%rsp) /* restore the rest of pt_regs */ movq R15(%rsp), %r15 movq R14(%rsp), %r14 movq R13(%rsp), %r13 movq R12(%rsp), %r12 movq R10(%rsp), %r10 movq RBX(%rsp), %rbx restore_mcount_regs /* Restore flags */ popfq /* * As this jmp to ftrace_epilogue can be a short jump * it must not be copied into the trampoline. * The trampoline will add the code to jump * to the return. */GLOBAL(ftrace_regs_caller_end) jmp ftrace_epilogueENDPROC(ftrace_regs_caller)

首先调用alloc_tramp函数申请size+MCOUNT_INSN_SIZE+sizeof(void *)大小的vmalloc内存。size为ftrace_regs_caller_end - ftrace_regs_caller的大小,可以认为是ftrace_regs_caller函数的大小,MCOUNT_INSN_SIZE为5字节,用来构造跳转指令,在ftrace_regs_caller结束之后跳转到函数ftrace_epilogue(trampoline收尾工作),在跳转指令之后还需要一个指针的位置用来存放livepatch的struct ftrace_ops的地址。

  申请过trampoline内存之后,将ftrace_regs_caller的代码全部拷贝到申请的内存里面。

  在紧挨着新内存ftrace_regs_caller函数的位置构造一个相对jmp指令,跳转到ftrace_epilogue的label处。看下这个label的代码:

181 GLOBAL(ftrace_epilogue)182 183 #ifdef CONFIG_FUNCTION_GRAPH_TRACER184 GLOBAL(ftrace_graph_call)185 jmp ftrace_stub186 #endif

ftrace_epilogue处的处理是jmp ftrace_stub,看下ftrace_stub的处理:

280 GLOBAL(ftrace_stub)281 retq

  ftrace_strub处就是retq指令。总结来说,ftrace_epilogue其实就是个retq指令,用于ftrace_regs_caller函数的收尾工作。

 在这条跳转指令之后的8个字节,用来存放livepatch的struct ftrace_ops的地址。

构造movq指令,位于ftrace_regs_caller_op_ptr这个label处,这个lable在ftrace_regs_caller内部,是为ftrace二级hook准备第三个参数,后面会提到这个第三个参数就是livepatch的ftrace_ops的地址。这个movq指令采用pc相对寻址,指令格式movq <offset>(%rip),%rdx,占位7个字节。

 在原有的movq指令里只有offset是不对的,所以这里仅仅需要修正offset即可。offset就是struct ftrace_ops指针的位置到这条movq下一条指令的位置。ftrace_ops指针存储的位置,在trampoline紧挨者ftrace_regs_caller函数体和jmp ftrace_epilogue指令之后。注意这里不是struct ftrace_ops内存和movq下一条指令的距离,而是struct ftrace_ops的指针存放的位置,因为xxx(rip)这里有个取值动作。

2.ftrace二级hook的使能

 ftrace二级hook开启在arch_ftrace_update_trampoline函数里完成,调用栈:

entry_SYSCALL_64_after_hwframe do_syscall_64 load_module do_init_module do_one_initcall patch_init klp_enable_patch klp_patch_object register_ftrace_function ftrace_startup __register_ftrace_function arch_ftrace_update_trampoline

4.18的ftrace的二级hook函数是ftrace_ops_assist_func函数。

arch_ftrace_update_trampoline在构造好ftrace_ops的trampoline之后,接下来使能ftrace的二级hook。

二级hook的位置位于trampoline的ftrace_regs_caller函数中的ftrace_regs_call的label的位置。如下:

GLOBAL(ftrace_regs_call) call ftrace_stub

 下面要做的就是把这个call ftrace_stub指令,替换成对ftrace_ops_assist_func的调用即可。需要构造一条调用指令,call+offset即可,offset是ftrace_ops_assist_func函数地址到call ftrace_stub下一条指令的距离,然后用这个调用指令替换call ftrace_stub这条指令。

 调用指令是5个字节,复制替换的时候不能保证是原子的,多核的情况下,在拷贝的过程中,如果cpu取到部分指令会导致指令非法异常或者跑飞。

x64采用int3指令过渡来解决这个问题,在ftrace_modify_code函数里实现。

ftrace_modify_code(unsigned long ip, unsigned const char *old_code, unsigned const char *new_code) { int ret; ret = add_break(ip, old_code); if (ret) goto out; run_sync(); ret = add_update_code(ip, new_code); if (ret) goto fail_update; run_sync(); ret = ftrace_write(ip, new_code, 1); /* * The breakpoint is handled only when this function is in progress. * The system could not work if we could not remove it. */ BUG_ON(ret); out: run_sync(); return ret; fail_update: /* Also here the system could not work with the breakpoint */ if (ftrace_write(ip, old_code, 1)) BUG(); goto out; }

  先通过add_break函数修改第一个字节位int3指令,run_sync作废指令cache和指令预取,重新来,然后调用add_update_code更新后4个字节,在调用run_sync,最后调用ftrace_write把int3指令替换为call指令的opcode最后再调用run_sync。

  在第一步中,其他cpu要么看到int3,要么完全看不到,没有问题。第二步中,拷贝后4个字节的过程中,因为有了int3,触发int3异常,在do_int3中发现这是ftrace在用int3做代码修改过渡,那么修改保存的ip返回地址,跳过包括in3指令在内的5个字节。在第三步中,cpu要么看到完正的5字节call指令,要么看到in3+4字节的offset,处理和第二步一样。

  总之,通过in3指令的过渡,使得整个5字节的指令拷贝,都处在int3异常的保护中,不会发生cpu去执行不完整的5字节的call指令。

  二级hook使能,trampoline里会调用ftrace_ops_assist_func函数,并且前面在构造trampoline的过程中,为它准备好了第三个入参ftrace_ops。

3.ftrace三级hook

看下二级hook函数ftrace_ops_assist_func:

static void ftrace_ops_assist_func(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *op, struct pt_regs *regs){ int bit; if ((op->flags & FTRACE_OPS_FL_RCU) && !rcu_is_watching()) return; bit = trace_test_and_set_recursion(TRACE_LIST_START, TRACE_LIST_MAX); if (bit < 0) return; preempt_disable_notrace(); op->func(ip, parent_ip, op, regs); preempt_enable_notrace(); trace_clear_recursion(bit);}

最终调用了ftrace_ops的func的钩子,在livepatch中这个钩子是klp_ftrace_handler,这是实现livepatch函数热替换的关键函数。

static void notrace klp_ftrace_handler(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *fops, struct pt_regs *regs){ struct klp_ops *ops; struct klp_func *func; int patch_state; ops = container_of(fops, struct klp_ops, fops); preempt_disable_notrace(); func = list_first_or_null_rcu(&ops->func_stack, struct klp_func, stack_node); if (WARN_ON_ONCE(!func)) goto unlock; smp_rmb(); if (unlikely(func->transition)) { smp_rmb(); patch_state = current->patch_state; WARN_ON_ONCE(patch_state == KLP_UNDEFINED); if (patch_state == KLP_UNPATCHED) { func = list_entry_rcu(func->stack_node.next, struct klp_func, stack_node); if (&func->stack_node == &ops->func_stack) goto unlock; } } if (func->nop) goto unlock; klp_arch_set_pc(regs, (unsigned long)func->new_func);unlock: preempt_enable_notrace();}

  这个函数里涉及到函数热替换的是klp_arch_set_pc函数,这是个体系相关函数,x64下实现如下:

static inline void klp_arch_set_pc(struct pt_regs *regs, unsigned long ip) { regs->ip = ip; }

  这里将regs->ip赋值为新函数的地址。

  看下ftrcae_regs_caller函数的汇编,重点关注下函数的恢复现场的流程:

ENTRY(ftrace_regs_caller) /* Save the current flags before any operations that can change them */ pushfq /* added 8 bytes to save flags */ save_mcount_regs 8 /* save_mcount_regs fills in first two parameters */GLOBAL(ftrace_regs_caller_op_ptr) /* Load the ftrace_ops into the 3rd parameter */ movq function_trace_op(%rip), %rdx /* Save the rest of pt_regs */ movq %r15, R15(%rsp) movq %r14, R14(%rsp) movq %r13, R13(%rsp) movq %r12, R12(%rsp) movq %r11, R11(%rsp) movq %r10, R10(%rsp) movq %rbx, RBX(%rsp) /* Copy saved flags */ movq MCOUNT_REG_SIZE(%rsp), %rcx movq %rcx, EFLAGS(%rsp) /* Kernel segments */ movq $__KERNEL_DS, %rcx movq %rcx, SS(%rsp) movq $__KERNEL_CS, %rcx movq %rcx, CS(%rsp) /* Stack - skipping return address and flags */ leaq MCOUNT_REG_SIZE+8*2(%rsp), %rcx movq %rcx, RSP(%rsp) /* regs go into 4th parameter */ leaq (%rsp), %rcxGLOBAL(ftrace_regs_call) call ftrace_stub  //替换为 call ftrace_ops_assist_func /* Copy flags back to SS, to restore them */ movq EFLAGS(%rsp), %rax movq %rax, MCOUNT_REG_SIZE(%rsp) /* Handlers can change the RIP */ movq RIP(%rsp), %rax movq %rax, MCOUNT_REG_SIZE+8(%rsp) /* restore the rest of pt_regs */ movq R15(%rsp), %r15 movq R14(%rsp), %r14 movq R13(%rsp), %r13 movq R12(%rsp), %r12 movq R10(%rsp), %r10 movq RBX(%rsp), %rbx restore_mcount_regs /* Restore flags */ popfq /* * As this jmp to ftrace_epilogue can be a short jump * it must not be copied into the trampoline. * The trampoline will add the code to jump * to the return. */GLOBAL(ftrace_regs_caller_end) jmp ftrace_epilogueENDPROC(ftrace_regs_caller)

 在36行的ftrace_ops_assist_func函数返回后,regs->ip地址已经是新函数的地址。

 42行新函数地址给rax。

 43行新函数地址rax赋值给MCOUNT_REG_SIZE+8(%rsp)。

 3行,MCOUNT_REG_SIZE(%rsp)地址保存的是rflags的值。

MCOUNT_REG_SIZE+8(%rsp)地址是栈中保存调用ftrace_regs_caller的旧函数的返回值位置,原来这个位置的值应该是old_func_addr+5,现在被替换为新函数的地址首地址。

 ftrace_regs_caller函数执行完之后,jmp_ftrace_epilogue,前面说过这本质是个retq指令。

 retq指令将当前的rsp位置的值,也就是新函数的地址装载到rip,执行新函数,完成新旧函数替换。

4.ftrace一级hook使能

  一、二、三级hook倶备,最后需要使能一级hook来开启livepatch。   

  使能一级hook就是构造call指令,调用ftrace_ops的trampoline,并用这条指令替换patch函数的前5个字节。替换流程:

entry_SYSCALL_64_after_hwframe   do_syscall_64    __do_sys_finit_module      load_module        do_init_module         do_one_initcall          patch_init            klp_enable_patch             klp_patch_object              register_ftrace_function                ftrace_startup                 ftrace_run_update_code                  arch_ftrace_update_code                   ftrace_modify_all_code                     ftrace_replace_code

指令更新在ftrace_replace_code函数里面,依然使用int3指令过渡,不再展开。

(七).livepatch的一致性

livepatch的一致性主要解决安全性问题,在新函数生效前,确保旧的含有没有在使用。

  新函数在整个系统中完全替换旧函数这个过程称为"transition",过渡的意思。

1.livepatch 加载

 kpatch loadlivepatch-0001-debug-xfs.ko完成livepatch的加载。

 这个过程主要要是两步:

 首先insmodlivepatch-0001-debug-xfs.ko

最后循环读取/sys/kernel/livepatch/livepatch_0001_debug_xfs/transition,直到不为1,超时时间15s。

2.初始化transition流程

 主要在__klp_enable_patch函数开启transition流程。

 初始化全部变量klp_transition_patch=KLP_PATCHED,这是我们的目标。

 初始化全局变量klp_transition_patch=patch,这是本次要打的patch。

初始化每个task的patch_state为KLP_UNPATCHED,这是我们的现状。

 整个transition的流程就是让系统中每个task的patch_state都追赶上klp_transition_patch的状态(KLP_PATCHED)。

3.开启transition的流程

 设置系统中每个task->thread_info->flags的TIF_PATCH_PENDING标志位。

4.进行trasition

由klp_try_complete_transition函数完成。

 <1>.遍历系统所有的task,如果本task没有完成transition(task的patch_state!=klp_transition_patch),进行task级的transition。

  对current和系统中其他非running的进程检查,如果task的本身内核栈中不包含旧函数,本task完成trasition,task的patch_state更新为klp_transition_patch,并清除TIF_PATCH_PENDING标志,该进程完成本次transition的kpi。

 <2>.经过<1>步的检查之后,发现还有task没有完成transition的kpi,启动klp_transition_work这个定时work,1s后在来次检查。

  如果多次klp_try_complete_transition依然有进程没有完成本身的transition,那就需要主动push一下。

  对于内核线程,调用wake_up_state直接唤醒s状态的进程。

  对于用户进程,调用signal_wake_up发送fake signal信号。对于处于s状态的用户进程,进行唤醒,如果进程处于running状态并且在运行,调用kick_process函数向进程所在的cpu发送reschedule的ipi中断,迫使进程让出cpu,这样进程才能接受stack check。

<3>进程自动的transition通过点

  处于以下两种状态的话。进程自动通过进程级的transition:

  对于用户进程,当返回用户态的时候调用klp_update_patch_state完成transition。

  对于swapper线程,当进入ild loop的时候调用klp_update_patch_state完成transition。

5.整个transition完成

  系统中所有的task都通过自己的trasition之后,调用klp_complete_transition函数结束这个transition。

klp_complete_transition工作如下:

  设置本patch的每个obj的每个func的transition为false。每个obj代表是livepatch中修改了的.o文件。

设置系统中所有task的patch_state为KLP_UNDEFINED。

  设置全局klp_target_state状态为KLP_UNPATCHED。

  设置全局klp_transition_patch为NULL,表示当前无在进行transition的patch。这样/sys/kernel/livepatch/livepatch_0001_debug_xfs/transition读到0,kpatch load流程结束。

(八).新旧函数替换时机

  看下klp_ftrace_handler函数:

static void notrace klp_ftrace_handler(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *fops, struct pt_regs *regs){ struct klp_ops *ops; struct klp_func *func; int patch_state; ops = container_of(fops, struct klp_ops, fops); /* * A variant of synchronize_rcu() is used to allow patching functions * where RCU is not watching, see klp_synchronize_transition(). */ preempt_disable_notrace(); func = list_first_or_null_rcu(&ops->func_stack, struct klp_func, stack_node); /* * func should never be NULL because preemption should be disabled here * and unregister_ftrace_function() does the equivalent of a * synchronize_rcu() before the func_stack removal. */ if (WARN_ON_ONCE(!func)) goto unlock; smp_rmb(); if (unlikely(func->transition)) { smp_rmb(); patch_state = current->patch_state; WARN_ON_ONCE(patch_state == KLP_UNDEFINED); if (patch_state == KLP_UNPATCHED) { /* * Use the previously patched version of the function. * If no previous patches exist, continue with the * original function. */ func = list_entry_rcu(func->stack_node.next, struct klp_func, stack_node); if (&func->stack_node == &ops->func_stack) goto unlock; } } /* * NOPs are used to replace existing patches with original code. * Do nothing! Setting pc would cause an infinite loop. */ if (func->nop) goto unlock; klp_arch_set_pc(regs, (unsigned long)func->new_func);unlock: preempt_enable_notrace();}

这个函数主要根据transiton的状态来决定使用新函数还是旧函数。

 分两种情况:

   <1>.整体transition在进行中(func->transition== true),如果进程本身通过的进程级的transition(task->patch_state== KLP_PATCHED),那么调用klp_arch_set_pc函数,本进程使用新函数。如果没有通过进程级的transition,那么使用旧函数。

   <2>.整体trasition已经完成,所有进程使用新函数。

有一种情况,如果整体transition在进行中,本进程还没有进程级的transition,但这个被patched函数已经有一个livepatch在运行,这种情况需要替换为原来livepatch的新函数。

liveness配置_我心依旧的技术博客_51CTO博客 (2025)
Top Articles
Latest Posts
Recommended Articles
Article information

Author: Carlyn Walter

Last Updated:

Views: 5655

Rating: 5 / 5 (70 voted)

Reviews: 85% of readers found this page helpful

Author information

Name: Carlyn Walter

Birthday: 1996-01-03

Address: Suite 452 40815 Denyse Extensions, Sengermouth, OR 42374

Phone: +8501809515404

Job: Manufacturing Technician

Hobby: Table tennis, Archery, Vacation, Metal detecting, Yo-yoing, Crocheting, Creative writing

Introduction: My name is Carlyn Walter, I am a lively, glamorous, healthy, clean, powerful, calm, combative person who loves writing and wants to share my knowledge and understanding with you.