kpatch-build工具和livepatch原理分析-程序员宅基地

技术标签: Linux内核  安全  Linux调试  

     内核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 2001
From: hanjinke <didiglobal.com>
Date: Thu, 8 Apr 2021 15:18:04 +0800
Subject: [PATCH] xfs: debug xfs log

Signed-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.c
index 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 load livepatch-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.构建 livepatch symbol和livepatch relocation 

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

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

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

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

       livepatch symbol指livepatch引用的普通模块加载机制无法解析的符号集,livepatch relocation 是引用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.sym
000000000032  007400000002 R_X86_64_PC32     0000000000000000 .klp.sym.xfs.xfsstats, - 4
00000000008f  007500000002 R_X86_64_PC32     0000000000000000 .klp.sym.xfs.xlog_cil_ - 4
0000000000e8  007400000002 R_X86_64_PC32     0000000000000000 .klp.sym.xfs.xfsstats, - 4
000000000238  004b00000002 R_X86_64_PC32     0000000000000000 .klp.sym.xfs.xlog_stat - 4
000000000250  004c00000002 R_X86_64_PC32     0000000000000000 .klp.sym.xfs.xlog_stat - 4
00000000029d  007600000002 R_X86_64_PC32     0000000000000000 .klp.sym.xfs.__tracepo + 24
0000000002d7  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_stack
0000000000a7  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), %rcx
GLOBAL(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_epilogue

ENDPROC(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_TRACER
184 GLOBAL(ftrace_graph_call)
185     jmp ftrace_stub
186 #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), %rcx
GLOBAL(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_epilogue

ENDPROC(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 load livepatch-0001-debug-xfs.ko完成livepatch的加载。

 这个过程主要要是两步:

 首先insmod livepatch-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的新函数。

 

      

  

 

 

 

  

  

  

   

      

      

      

 

 

  

 

 

  

 

  

       

      

   

    

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/hjkfcz/article/details/115705116

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法