再谈内核模块加载(三)—模块加载流程(下)

版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/119813552

更多内容可关注微信公众号  

   上接<再谈内核模块加载(二)—模块加载流程(上)>


9.检查是否有同名模块已加载或正在加载
    err = add_unformed_module(mod);
    if (err)
        goto free_module;

    unformed是指加载到一半(也就是执行完当前函数),但还没有完全加载好的模块,此函数根据this_module.name检测当前系统里是否有同名模块:

  • 如果有同名模块但状态是正在执行module_init或unformed(也就是和当前同状态),则等待之前的模块加载完成事件后重新检测
  • 如果有同名模块,但不属于前面两种状态,则直接加载失败(-EEXIST)
  • 如果没有同名模块,则将当前模块加到内核的模块树列表中(表头为mod_tree)
    此函数最后会通过mod_update_bounds函数更新内核已有模块的地址范围边界.
static int add_unformed_module(struct module *mod)
{
    int err;
    struct module *old;
    mod->state = MODULE_STATE_UNFORMED;

again:
    mutex_lock(&module_mutex);
    //查找当前内核是否已经有同名模块了,非正式的也算
    old = find_module_all(mod->name, strlen(mod->name), true);
    if (old != NULL) {
        //若此模块是正在执行module_init,或和当前同状态的模块
        if (old->state == MODULE_STATE_COMING
            || old->state == MODULE_STATE_UNFORMED) {
            /* Wait in case it fails to load. */
            mutex_unlock(&module_mutex);
            //则等待模块加载完毕,重新跑
            err = wait_event_interruptible(module_wq,
                           finished_loading(mod->name));
            if (err)
                goto out_unlocked;
            goto again;
        }
        //若非以上两个状态,代表同名模块已经存在了,注意这里模块的名字是mod->name
        err = -EEXIST;
        goto out;
    }
    //更新系统已有模块的VA地址边界
    mod_update_bounds(mod);
    list_add_rcu(&mod->list, &modules);
    //在模块树中更新此模块
    mod_tree_insert(mod);
    err = 0;

out:
    mutex_unlock(&module_mutex);
out_unlocked:
    return err;
}

10.为模块签名失败但放过的情况提示warning

#ifdef CONFIG_MODULE_SIG
    /*
      若模块签名校验失败,则提示warning设置taint
      如果要求强制模块签名,前面module_sig_check就直接break了
    */
    mod->sig_ok = info->sig_ok;
    if (!mod->sig_ok) {
        pr_notice_once("%s: module verification failed: signature "
                   "and/or required key missing - tainting "
                   "kernel\n", mod->name);
        add_taint_module(mod, TAINT_UNSIGNED_MODULE, LOCKDEP_STILL_OK);
    }
#endif

11.precpu变量分配内存,模块卸载数据结构的初始化,sysfs的mutex初始化
    err = percpu_modalloc(mod, info);
    if (err)
        goto unlink_mod;
    /*
      没开启CONFIG_MODULE_UNLOAD函数(代表不允许模块卸载)则这里为空
      开启了这里也只是个初始化函数
    */
    err = module_unload_init(mod);
    if (err)
        goto unlink_mod;

    //sysfs的mutex初始话
    init_param_lock(mod);

  这里要注意,模块除了init_layout和core_layout外,还单独为percpu变量分配了内存,记录在mod.percpu中


12.内存布局中struct module的初始化与检查

    err = find_module_sections(mod, info);
    if (err)
        goto free_unload;
    err = check_module_license_and_versions(mod);
    if (err)
        goto free_unload;

  find_module_sections 函数获取模块布局中节区的首地址返回到mod的各个字段中(如mod->kp),同时返回节区中的元素个数(如mod->num_kp),具体含义参考struct module结构体的定义,这里查找模块布局的方法实际上还是遍历模块内存二进制的节区头部表,但由于在move_module函数中分配模块布局内存时已经将节区头部表中的sh_addr修正为内存布局中此节区的地址,故这里返回的也是内存布局中的地址.

static int find_module_sections(struct module *mod, struct load_info *info)
{
    /*
        输出参数sizeof(*mod->kp)指定一个元素的大小,返回值为内存布局中此节区的首地址
       节区内元素个数记录重写回到mod->num_kp中.
    */
    mod->kp = section_objs(info, "__param",
                   sizeof(*mod->kp), &mod->num_kp);

    mod->syms = section_objs(info, "__ksymtab",
                 sizeof(*mod->syms), &mod->num_syms);
    mod->crcs = section_addr(info, "__kcrctab");
    mod->gpl_syms = section_objs(info, "__ksymtab_gpl",
                     sizeof(*mod->gpl_syms),
                     &mod->num_gpl_syms);
    mod->gpl_crcs = section_addr(info, "__kcrctab_gpl");
    mod->gpl_future_syms = section_objs(info,
                        "__ksymtab_gpl_future",
                        sizeof(*mod->gpl_future_syms),
                        &mod->num_gpl_future_syms);
    mod->gpl_future_crcs = section_addr(info, "__kcrctab_gpl_future");

#ifdef CONFIG_UNUSED_SYMBOLS
    mod->unused_syms = section_objs(info, "__ksymtab_unused",
                    sizeof(*mod->unused_syms),
                    &mod->num_unused_syms);
    mod->unused_crcs = section_addr(info, "__kcrctab_unused");
    mod->unused_gpl_syms = section_objs(info, "__ksymtab_unused_gpl",
                        sizeof(*mod->unused_gpl_syms),
                        &mod->num_unused_gpl_syms);
    mod->unused_gpl_crcs = section_addr(info, "__kcrctab_unused_gpl");
#endif
#ifdef CONFIG_CONSTRUCTORS
    mod->ctors = section_objs(info, ".ctors",
                  sizeof(*mod->ctors), &mod->num_ctors);
    if (!mod->ctors)
        mod->ctors = section_objs(info, ".init_array",
                sizeof(*mod->ctors), &mod->num_ctors);
    else if (find_sec(info, ".init_array")) {
        /*
         * This shouldn't happen with same compiler and binutils
         * building all parts of the module.
         */
        pr_warn("%s: has both .ctors and .init_array.\n",
               mod->name);
        return -EINVAL;
    }
#endif

  check_module_license_and_versions函数主要用来过滤一些模块,并检查CRC表是否正确. 此函数首先通过模块名过滤了一些模块,然后在内核定义了CONFIG_MODVERSIONS的情况下(代表要检查模块中函数的CRC),如果模块有某类型的导出函数,则要有此类型的crc表,没有则报错。

static int check_module_license_and_versions(struct module *mod)
{
    int prev_taint = test_taint(TAINT_PROPRIETARY_MODULE);
    /*
        这里还单独加了case....
    */
    if (strcmp(mod->name, "ndiswrapper") == 0)
        add_taint(TAINT_PROPRIETARY_MODULE, LOCKDEP_NOW_UNRELIABLE);

    /* driverloader was caught wrongly pretending to be under GPL */
    if (strcmp(mod->name, "driverloader") == 0)
        add_taint_module(mod, TAINT_PROPRIETARY_MODULE,
                 LOCKDEP_NOW_UNRELIABLE);

    /* lve claims to be GPL but upstream won't provide source */
    if (strcmp(mod->name, "lve") == 0)
        add_taint_module(mod, TAINT_PROPRIETARY_MODULE,
                 LOCKDEP_NOW_UNRELIABLE);

    if (!prev_taint && test_taint(TAINT_PROPRIETARY_MODULE))
        pr_warn("%s: module license taints kernel.\n", mod->name);

#ifdef CONFIG_MODVERSIONS
    /*
        这里是通用检查,如果当前模块有导出符号,但导出符号没有CRC,而当前内核又配置了
        CONFIG_MODVERSIONS,则代表当前内核被污染了.
    */
    if ((mod->num_syms && !mod->crcs)
        || (mod->num_gpl_syms && !mod->gpl_crcs)
        || (mod->num_gpl_future_syms && !mod->gpl_future_crcs)
#ifdef CONFIG_UNUSED_SYMBOLS
        || (mod->num_unused_syms && !mod->unused_crcs)
        || (mod->num_unused_gpl_syms && !mod->unused_gpl_crcs)
#endif
        ) {
        //这里要增加taint标记
        return try_to_force_load(mod,
                     "no versions for exported symbols");
    }
#endif
    return 0;
}

13.为init_layout中的静态链接符号表做符号决议

    err = simplify_symbols(mod, info);
    if (err < 0)
        goto free_modinfo;
  此函数叫simplify_symbols的意思是此函数决议了模块静态链接符号表中的符号,后面再使用此符号时候,不需要再决议了,所以称为简化.
  此函数实际上是让模块内存中的静态链接重定位表(init_layout中)中的符号,全部指向其真实的内存地址:
   * 对于未定义符号/弱符号找内核(已加载模块)的符号表,
   * 其他符号(属于当前模块)则直接使用st_value += 段基址计算
static int simplify_symbols(struct module *mod, const struct load_info *info)
{
    //获取模块内存中的符号表,这个是存在init_layout中的,完全复制自目标文件的符号表
    Elf_Shdr *symsec = &info->sechdrs[info->index.sym];
    Elf_Sym *sym = (void *)symsec->sh_addr;
    unsigned long secbase;
    unsigned int i;
    int ret = 0;
    const struct kernel_symbol *ksym;

    //遍历符号表
    for (i = 1; i < symsec->sh_size / sizeof(Elf_Sym); i++) {
        const char *name = info->strtab + sym[i].st_name;
        switch (sym[i].st_shndx) {
        /*
          正常应该是没有common符号的(-fno-common),也就是说.S中不能有.comm定义
          正常的未初始化全局变量都直接扔bss段.
        */
        case SHN_COMMON:
            /* Ignore common symbols */
            if (!strncmp(name, "__gnu_lto", 9))
                break;

            /* We compiled with -fno-common.  These are not
               supposed to happen.  */
            pr_debug("Common symbol: %s\n", name);
            pr_warn("%s: please compile with -fno-common\n",
                   mod->name);
            ret = -ENOEXEC;
            break;

        //绝对符号不处理
        case SHN_ABS:
            /* Don't need to do anything */
            pr_debug("Absolute symbol: 0x%08lx\n",
                   (long)sym[i].st_value);
            break;

        case SHN_LIVEPATCH:
            /* Livepatch symbols are resolved by livepatch */
            break;

        //未定义符号
        case SHN_UNDEF:
            /*
               此函数负责在内核和所有已加载模块中查找名为name的符号,若找到了返回此符号的符号表项,
              并设置ownername为其拥有者的名,如果开启了CRC校验则会同时检测CRC.
               模块中的未定义符号实际上是在编译期间引用了编译环境的内核或其他模块中的符号,虽然是
              未定义,但其编译时候会记录此符号的CRC,
               这里的CRC检查,实际上检查的是模块编译环境符号的CRC和模块运行环境符号的CRC是否一致.
            */
            ksym = resolve_symbol_wait(mod, info, name);
            /* Ok if resolved.  */
            if (ksym && !IS_ERR(ksym)) {
                //将静态链接符号表中,此符号的值,设置为内核符号中此符号的值
                sym[i].st_value = kernel_symbol_value(ksym);
                break;
            }

            /* Ok if weak.  */
            //如果在内核没有找到此符号,且此未定义符号是一个弱符号,则未定义就未定义了
            //弱符号应该在重定位的时候是按照未定义处理的,这样才会找一下是否有重名符号
            if (!ksym && ELF_ST_BIND(sym[i].st_info) == STB_WEAK)
                break;

            //否则就是没找到符号,报错
            ret = PTR_ERR(ksym) ?: -ENOENT;
            pr_warn("%s: Unknown symbol %s (err %d)\n",
                mod->name, name, ret);
            break;

        default:
            /* Divert to percpu allocation if a percpu var. */
            if (sym[i].st_shndx == info->index.pcpu)
                //percpu变量,则直接使用percpu(段)的首地址
                secbase = (unsigned long)mod_percpu(mod);
            else
                //sym[i].st_shndx是符号表所在的节区,这里获取节区首地址
                secbase = info->sechdrs[sym[i].st_shndx].sh_addr;
            //符号值直接加上节区首地址
            sym[i].st_value += secbase;
            break;
        }
    }
    return ret;
}

14.模块内存布局的静态链接重定位

    err = apply_relocations(mod, info);
    if (err < 0)
        goto free_modinfo;

  此函数遍历目标文件中的所有内存节区(SHF_ALLOC)的重定位节区(REL/RELA),并遍历每个节区中的每个静态链接重定位表项,对其做静态链接。apply_relocate_add是真正负责重定位的函数,其中一共处理了月40种重定位类型。这里需要说明的是,对于CALL26/JUMP26类型的指令,若短跳转不够有可能会尝试基于plt的长跳转(CONFIG_ARM64_MODULE_PLTS开启情况下,未开启则直接报错),此时会在模块core/init_layout的plt表中增加一个表项.

  此函数之后,模块作为目标文件的静态链接动态的完成了.

static int apply_relocations(struct module *mod, const struct load_info *info)
{
    unsigned int i;
    int err = 0;

    /* Now do relocations. */
    /*
       这里实际上是遍历所有重定位表(类型为SHT_REL或SHT_RELA),若其要重定位的节区是内存节区 (SHF_ALLOC),则处理,否则pass.
       在目标文件中只有静态链接重定位表,每个重定位表都只对应一个节区.
    */
    for (i = 1; i < info->hdr->e_shnum; i++) {
        unsigned int infosec = info->sechdrs[i].sh_info;
        /* Not a valid relocation section? */
        /*
          对于REL/RELA类型节区来说sh_info记录其要重定位的节区,这里是判断是否节区越界
        */
        if (infosec >= info->hdr->e_shnum)
            continue;

        /* Don't bother with non-allocated sections */
        //如果重定位的目标节区是内存节区
        if (!(info->sechdrs[infosec].sh_flags & SHF_ALLOC))
            continue;

        /* Livepatch relocation sections are applied by livepatch */
        if (info->sechdrs[i].sh_flags & SHF_RELA_LIVEPATCH)
            continue;

        if (info->sechdrs[i].sh_type == SHT_REL)
            err = apply_relocate(info->sechdrs, info->strtab,
                         info->index.sym, i, mod);
        else if (info->sechdrs[i].sh_type == SHT_RELA)
            /*
                sechdrs是节区头部表指针
                strtab是符号表字符串表
                info->index.sym是符号表节区索引
                i是当前的重定位节区索引
                mod是当前模块
                此函数负责具体类型的重定位操作,对于超过范围的call26/jump26指令,若开启了CONFIG_ARM64_MODULE_PLTS,则会在PLT表中为其增加一个表项.
            */
            err = apply_relocate_add(info->sechdrs, info->strtab,
                         info->index.sym, i, mod);
        if (err < 0)
            break;
    }
    return err;
}

  注:   静态链接一共分为三步:地址空间分配,符号决议,重定位,正好对应模块加载时的三个阶段:

  1. move_module包括了模块内存空间布局的分配
  2. simplify_symbols负责模块中的未定义符号的决议(和已定义符号的地址修正)
  3. apply_relocations负责静态链接的重定位(使用的是静态链接重定位表)
    此三步和静态链接的步骤一模一样,故模块加载实际上可以认为是内核运行时动态的执行了一次静态链接流程,向自身动态的静态链接了一个目标文件.

15.重排异常表、per-cpu变量复制、复制核心符号表

    err = post_relocation(mod, info);
    if (err < 0)
        goto free_modinfo;

  post_relocation一共包含3个主要函数,其中:

  • sort_extable 负责重排模块异常向量表
  • percpu_modcopy 负责将per-cpu变量的初始复制到各个cpu的内存
  • add_kallsyms负责从init_layout中的静态链接符号表复制所有需要复制的符号到core_layout的核心符号表(同时包括核心符号表字符串表、类型表的复制)
static int post_relocation(struct module *mod, const struct load_info *info)
{
    /* Sort exception table now relocations are done. */
    //重定位结束后要重排一下异常向量表
    sort_extable(mod->extable, mod->extable + mod->num_exentries);

    /* Copy relocated percpu area over. */
    //per-cpu变量复制到各个cpu对应内存
    percpu_modcopy(mod, (void *)info->sechdrs[info->index.pcpu].sh_addr,
               info->sechdrs[info->index.pcpu].sh_size);

    /* Setup kallsyms-specific fields. */
    //这里从静态链接符号表中复制模块的核心符号表
    add_kallsyms(mod, info);

    /* Arch-specific module finalizing. */
    return module_finalize(info->hdr, info->sechdrs, mod);
}

16.init/core_layout缓存刷新

   flush_module_icache(mod);

  此函数负责刷新init/core_layout VA[base,base+size]范围内的指令缓存


17.复制用户态参数到内核

    //用户参数复制到内核,并记录到args中
    mod->args = strndup_user(uargs, ~0UL >> 1);
    if (IS_ERR(mod->args)) {
        err = PTR_ERR(mod->args);
        goto free_arch_cleanup;
    }

18.导出符号检查与RONX保护

    err = complete_formation(mod, info);
    if (err)
        goto ddebug_cleanup;

  此函数包含两步操作:

  1)verify_exported_symbols函数遍历模块中的所有导出函数,并检查在内核中是否有出现同名的导出符号
    实际上遍历的是模块的syms/gpl_syms/gpl_future_syms等所有导出表,然后针对每个导出表中的每个导出符号,在内核查找是否已有同名的导出符号.
    但需要注意的是,这里只检查了模块导出的符号在内核是否有重名符号,并没有说模块中的所有符号(核心符号或静态链接符号表中的符号)都不能重名.
  2)为模块的init_layout/core_layout做RONX保护
    RONX的逻辑后面内核安全特性中单独说,这里需要强调一点事ro_after_init段在这里设置为rw的了,模块初始化之后才会设置回RO
static int complete_formation(struct module *mod, struct load_info *info)
{
    int err;
    mutex_lock(&module_mutex);
    /* Find duplicate symbols (must be called under lock). */
    /*
      这里是检验内核中是否有同名的导出符号,实际上是遍历syms/gpl_syms/gpl_future_syms等模块的多个导出表
     然后针对每个导出符号,在内核查找是否已有同名的导出符号.
    */
    err = verify_exported_symbols(mod);
    if (err < 0)
        goto out;

    //所有该RO的RO
    module_enable_ro(mod, false);
    //除了text段全NX
    module_enable_nx(mod);

    /* Mark state as coming so strong_try_module_get() ignores us,
     * but kallsyms etc. can see us. */
    mod->state = MODULE_STATE_COMING;
    mutex_unlock(&module_mutex);
    return 0;
out:
    mutex_unlock(&module_mutex);
    return err;
}

19.发送模块加载通知链

    err = prepare_coming_module(mod);
    if (err)
        goto bug_cleanup;

  这个没啥可说的

static int prepare_coming_module(struct module *mod)
{
    int err;

    ftrace_module_enable(mod);
    err = klp_module_coming(mod);
    if (err)
        return err;

    blocking_notifier_call_chain(&module_notify_list,
                     MODULE_STATE_COMING, mod);
    return 0;
}

20.参数解析与sysfs、livepatch设置

    /*
       此函数负责模块的参数解析,其中args是insmod传入的实参,而kp则是模块的实参类型,记录在模块的段__param中
    */
    after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,
                  -32768, 32767, mod,
                  unknown_module_param_cb);
    if (IS_ERR(after_dashes)) {
        err = PTR_ERR(after_dashes);
        goto coming_cleanup;
    } else if (after_dashes) {
        pr_warn("%s: parameters '%s' after `--' ignored\n",
               mod->name, after_dashes);
    }

    /* Link in to sysfs. */
    //链接到sysfs的输出
    err = mod_sysfs_setup(mod, info, mod->kp, mod->num_kp);
    if (err < 0)
        goto coming_cleanup;
    
    //livepatch用的,pass
    if (is_livepatch_module(mod)) {
        err = copy_module_elf(mod, info);
        if (err < 0)
            goto sysfs_cleanup;
    }

  这个暂时也没啥好说的

21. 调用模块初始化函数,卸载init_layout并完成加载
    return do_init_module(mod);

  此函数主要负责:

  • 调用模块的初始化函数(如果有构造函数则先调用构造函数,然后才是init函数)
  • 之后对ro_after_init段做只读保护,然后释放掉整个init_layout的内存
  • 同时清空mod->init_layout数据结构,将mod->kallsyms指向核心符号表(mod->core_kallsyms,静态链接符号表已随init_layout释放了)
static noinline int do_init_module(struct module *mod)
{
    int ret = 0;
    struct mod_initfree *freeinit;

    //分配一个 mod_initfree 结构体
    freeinit = kmalloc(sizeof(*freeinit), GFP_KERNEL);
    if (!freeinit) {
        ret = -ENOMEM;
        goto fail;
    }
    //free结构体指向init_layout基地址
    freeinit->module_init = mod->init_layout.base;

    current->flags &= ~PF_USED_ASYNC;

    //若模块指定了构造函数,则调用其构造函数
    do_mod_ctors(mod);
    
    /* Start the module */
    //若模块有init函数,则调用其init函数,用do_one_initcall包裹前后有事件追踪
    if (mod->init != NULL)
        ret = do_one_initcall(mod->init);
    if (ret < 0) {
        goto fail_free_freeinit;
    }
    if (ret > 0) {
        pr_warn("%s: '%s'->init suspiciously returned %d, it should "
            "follow 0/-E convention\n"
            "%s: loading module anyway...\n",
            __func__, mod->name, ret, __func__);
        dump_stack();
    }

    /* Now it's a first class citizen! */
    mod->state = MODULE_STATE_LIVE;
    //通知链
    blocking_notifier_call_chain(&module_notify_list,
                     MODULE_STATE_LIVE, mod);

    if (!mod->async_probe_requested && (current->flags & PF_USED_ASYNC))
        async_synchronize_full();

    ftrace_free_mem(mod, mod->init_layout.base, mod->init_layout.base +
            mod->init_layout.size);
    mutex_lock(&module_mutex);
    /* Drop initial reference. */
    module_put(mod);
    trim_init_extable(mod);
#ifdef CONFIG_KALLSYMS
    /* Switch to core kallsyms now init is done: kallsyms may be walking! */
    //模块符号表指向核心符号表
    rcu_assign_pointer(mod->kallsyms, &mod->core_kallsyms);
#endif
    //这里主要重新保护了一下ro_after_init
    module_enable_ro(mod, true);
    mod_tree_remove_init(mod);
    module_arch_freeing_init(mod);
    //清空init_layout相关结构体
    mod->init_layout.base = NULL;
    mod->init_layout.size = 0;
    mod->init_layout.ro_size = 0;
    mod->init_layout.ro_after_init_size = 0;
    mod->init_layout.text_size = 0;

    //这里插入模块的释放队列,实际上最终对应函数 do_free_init 释放掉整个init_layout
    //这里do_free_init应该是释放物理内存+释放VA空间,不需要提前设置init_layout的属性
    if (llist_add(&freeinit->node, &init_free_list))
        schedule_work(&init_free_wq);

    mutex_unlock(&module_mutex);
    wake_up_all(&module_wq);

    return 0;

fail_free_freeinit:
    kfree(freeinit);
fail:
    /* Try to protect us from buggy refcounters. */
    mod->state = MODULE_STATE_GOING;
    synchronize_rcu();
    module_put(mod);
    blocking_notifier_call_chain(&module_notify_list,
                     MODULE_STATE_GOING, mod);
    klp_module_going(mod);
    ftrace_release_mod(mod);
    free_module(mod);
    wake_up_all(&module_wq);
    return ret;
}

到此模块加载完毕,下图更清晰的描述了此整个过程 

再谈内核模块加载(三)—模块加载流程(下)_第1张图片

你可能感兴趣的:(linux-kernel,内核模块加载,linux模块加载)