ps:kernel symbol内核符号表,就是在内核的内部函数或变量中,可供外部引用的函数和变量的符号表。. 其实说白了就是一个索引文件,它存在的目的就是让外部软件可以知道kernel文件内部实际分配的位置。
先来个图:
在用户空间,用insmod这样的命令来向内核空间安装一个内核模块,本节将详细讨论模块加载时的内核行为。当调用“insmod demodev.ko”来安装demodev.ko这样的内核模块时,insmod会首先利用文件系统的接口将其数据读取到用户空间的一段内存中,然后通过系统调用sys_init_module让内核去处理模块加载的整个过程。
sys_init_module
原型:
asmlinkage long sys_init_module(void __user *umod, unsigned long len, const char __user *uargs);
其中,第一参数umod是指向用户空间demodev.ko文件映像数据的内存地址,第二参数len是该文件的数据大小,第三参数是传给模块的参数在用户空间下的内存地址。
sys_init_module实现
SYSCALL_DEFINE3宏定义
#define SYSCALL_DEFINEx(x, sname, ...) \ SYSCALL_METADATA(sname, x, __VA_ARGS__) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) #define __PROTECT(...) asmlinkage_protect(__VA_ARGS__) #define __SYSCALL_DEFINEx(x, name, ...) \ asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \ __attribute__((alias(__stringify(SyS##name)))); \ static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \ asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \ asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \ { \ long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \ __MAP(x,__SC_TEST,__VA_ARGS__); \ __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \ return ret; \ } \ static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { int err; struct load_info info = { }; err = may_init_module();//确定具有加载或去除内核模块的能力 并是否使能modules if (err) return err; pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags); if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS |MODULE_INIT_IGNORE_VERMAGIC)) return -EINVAL; err = copy_module_from_fd(fd, &info);//copy_module_from_fd () 将打开的模块文件读到申请的内核内存中,并交由 load_module () 处理。 if (err) return err; return load_module(&info, uargs, flags); }
在sys_init_module函数中,加载模块的任务主要是通过调load_module函数来完成的,该函数的定义为:
/* Allocate and load the module: note that size of section 0 is always zero, and we rely on this for optional sections. */ //二进制数据使用load_module传输到内核地址空间中,锁有需要的重定位都会完成,所有iny都会解决 //在load_module中创建module实例已近添加到全局的modues链表后,内核内需要调用模块的初始化函数并释放 //初始化数据占用的内存空间 static int load_module(struct load_info *info, const char __user *uargs, int flags) { struct module *mod; long err; char *after_dashes; err = module_sig_check(info);//module_sig_check () 需要开启 //CONFIG_MODULE_SIG 才会起作用 if (err) goto free_copy; err = elf_header_check(info);//主要是检查一下 ELF 文件头信息,别忘了模块文 //件.ko 是 ELF relocatable 文件,所以需要检查一下是否是正确的 ELF 文件格 //式。 if (err) goto free_copy; /* Figure out module layout, and allocate all the memory. */ mod = layout_and_allocate(info, flags);//获取模块的布局,重新申请一次内存 //,并对相关section对内容进行修改 if (IS_ERR(mod)) { err = PTR_ERR(mod); goto free_copy; } /* Reserve our place in the list. */ err = add_unformed_module(mod);//将模块加入到内核的模块链表中。 if (err) goto free_module; #ifdef CONFIG_MODULE_SIG 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 /* To avoid stressing percpu allocator, do this once we're unique. */ err = percpu_modalloc(mod, info);//对模块里面的percpu进行特殊的内存申请, //这个percpu变量比较特殊,在每个CPU上都存在一份,所以用特殊的申请内存函数 //去处理 if (err) goto unlink_mod; /* Now module is in final location, initialize linked lists, etc. */ err = module_unload_init(mod);//为后续执行rmmod卸载命令时初始化一些引用变 //量以及链表等。 if (err) goto unlink_mod; /* Now we've got everything in the final locations, we can * find optional sections. */ err = find_module_sections(mod, info);//找模块里面的一些section, //比如参数的__param节,内核符号表__ksymtab节,GPL范围协议的符号 //__ksymtab_gpl节,ftrace增加的_ftrace_events节等待。 if (err) goto free_unload; err = check_module_license_and_versions(mod); //主要是检查模块的license是否存在污染内核的可能 if (err) goto free_unload; /* Set up MODINFO_ATTR fields */ setup_modinfo(mod, info);//对sys属性进行一些预先setup处理。 /* Fix up syms, so that st_value is a pointer to location. */ err = simplify_symbols(mod, info);//修复加载模块的符号,使符号指向内核正确的运行地址。 if (err < 0) goto free_modinfo; err = apply_relocations(mod, info);//是对.rel节和.rela节进行重定位的一个过程。 if (err < 0) goto free_modinfo; err = post_relocation(mod, info);//对重定位后的percpu变量重新赋值,并将即 //将加载完成的模块的符号加入到内核模块的符号链表中,如果成功加载此模块且 //内核配置了CONFIG_KALLSYMS,那么在/proc/kallsyms下可以看到此模块的符号 if (err < 0) goto free_modinfo; flush_module_icache(mod);//执行刷新模块的init_layout和core_layout的cache。 /* Now copy in args */ mod->args = strndup_user(uargs, ~0UL >> 1);//复制一下模块的参数 if (IS_ERR(mod->args)) { err = PTR_ERR(mod->args); goto free_arch_cleanup; } dynamic_debug_setup(info->debug, info->num_debug);//需要开启内核CONFIG_DYNAMIC_DEBUG才会启用 /* Ftrace init must be called in the MODULE_STATE_UNFORMED state */ ftrace_module_init(mod);//需要开启相关的ftrace配置 /* Finally it's fully formed, ready to start executing. */ err = complete_formation(mod, info);//对模块的内存属性进行修改,比如.text的读+执行,.data的读写属性。 if (err) goto ddebug_cleanup; /* Module is ready to execute: parsing args may do that. */ after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp, -32768, 32767, unknown_module_param_cb);//解析模块参数 if (IS_ERR(after_dashes)) { err = PTR_ERR(after_dashes); goto bug_cleanup; } else if (after_dashes) { pr_warn("%s: parameters '%s' after `--' ignored\n", mod->name, after_dashes); } /* Link in to syfs. */ err = mod_sysfs_setup(mod, info, mod->kp, mod->num_kp);//对sys进行创建的过程 if (err < 0) goto bug_cleanup; /* Get rid of temporary copy. */ free_copy(info);//释放最初内核申请的用于保存模块原文件信息的内存 /* Done! */ trace_module_load(mod);//trace相关的 return do_init_module(mod);//最后模块执行init函数的过程 bug_cleanup: /* module_bug_cleanup needs module_mutex protection */ mutex_lock(&module_mutex); module_bug_cleanup(mod); mutex_unlock(&module_mutex); blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_GOING, mod); /* we can't deallocate the module until we clear memory protection */ unset_module_init_ro_nx(mod); unset_module_core_ro_nx(mod); ddebug_cleanup: dynamic_debug_remove(info->debug); synchronize_sched(); kfree(mod->args); free_arch_cleanup: module_arch_cleanup(mod); free_modinfo: free_modinfo(mod); free_unload: module_unload_free(mod); unlink_mod: mutex_lock(&module_mutex); /* Unlink carefully: kallsyms could be walking list. */ list_del_rcu(&mod->list); wake_up_all(&module_wq); /* Wait for RCU synchronizing before releasing mod->list. */ synchronize_rcu(); mutex_unlock(&module_mutex); free_module: /* Free lock-classes; relies on the preceding sync_rcu() */ lockdep_free_key_range(mod->module_core, mod->core_size); module_deallocate(mod, info); free_copy: free_copy(info); return err; }
所有参数同sys_init_module函数中的完全一样,实际上在sys_init_module函数的一开始便会调用该函数,调用时传入的实参完全来自于sys_init_module函数,没有经过任何的处理或者修改。
为了更清楚地解释模块加载时的内核行为,我们把sys_init_module分为两个部分:第一部分是调用load_module,完成模块加载最核心的任务:第二部分是在模块被成功加载到系统之后的后续处理。我们将在讨论完load_module部分之后再继续讨论sys_init_module的第二部分。不过,在继续load_module话题之前,先要看一个内核中非常重要的数据结构—------struct module。
struct module
load module函数的返回值是一个struct module类型的指针,struct module是内核用来管理系统中加载的模块时使用的一个非常重要的数据结构,一个struct module对象代表着现实中一个内核模块在Linux系统中的抽象,该结构的定义如下〈删除了一些trace和unused symbol相关的部分):
struct module { enum module_state state;//模块的状态 /* Member of list of modules */ struct list_head list;//用作模块链表的链表元素 /* Unique handle for this module */ char name[MODULE_NAME_LEN];//该模块唯一的句柄,模块名称 /* Sysfs stuff. */ //导出符号 struct module_kobject mkobj; struct module_attribute *modinfo_attrs; const char *version; const char *srcversion; struct kobject *holders_dir; /* Exported symbols */ const struct kernel_symbol *syms; const unsigned long *crcs;//内核模块导出符号的校验码所在起始地址。 unsigned int num_syms; /* Kernel parameters. */ struct kernel_param *kp;//内核模块参数所在的起始地址。 unsigned int num_kp; /* GPL-only exported symbols. */ //只适用于GPL的导出符号 unsigned int num_gpl_syms; const struct kernel_symbol *gpl_syms; const unsigned long *gpl_crcs; #ifdef CONFIG_UNUSED_SYMBOLS /* unused exported symbols. */ const struct kernel_symbol *unused_syms; const unsigned long *unused_crcs; unsigned int num_unused_syms; /* GPL-only, unused exported symbols. */ unsigned int num_unused_gpl_syms; const struct kernel_symbol *unused_gpl_syms; const unsigned long *unused_gpl_crcs; #endif #ifdef CONFIG_MODULE_SIG /* Signature was verified. */ bool sig_ok; #endif /* symbols that will be GPL-only in the near future. */ const struct kernel_symbol *gpl_future_syms; const unsigned long *gpl_future_crcs; unsigned int num_gpl_future_syms; /* Exception table */ //异常表 unsigned int num_exentries; struct exception_table_entry *extable; /* Startup function. */ //初始化函数 int (*init)(void);//init是一个指针,指向一个在模块初始化时调用的函数 //指向内核模块初始化函数的指针,在内核模块源码中由module init宏指定。 /* If this is non-NULL, vfree after init() returns */ void *module_init; /* Here is the actual code + data, vfree'd on unload. */ void *module_core; /* Here are the sizes of the init and core sections */ unsigned int init_size, core_size; /* The size of the executable code in each section. */ unsigned int init_text_size, core_text_size; /* Size of RO sections of the module (text+rodata) */ unsigned int init_ro_size, core_ro_size; /* Arch-specific module values */ struct mod_arch_specific arch; unsigned int taints; /* same bits as kernel:tainted */ #ifdef CONFIG_GENERIC_BUG /* Support for BUG */ unsigned num_bugs; struct list_head bug_list; struct bug_entry *bug_table; #endif #ifdef CONFIG_KALLSYMS /* * We keep the symbol and string tables for kallsyms. * The core_* fields below are temporary, loader-only (they * could really be discarded after module init). */ Elf_Sym *symtab, *core_symtab; unsigned int num_symtab, core_num_syms; char *strtab, *core_strtab; /* Section attributes */ struct module_sect_attrs *sect_attrs; /* Notes attributes */ struct module_notes_attrs *notes_attrs; #endif /* The command line arguments (may be mangled). People like keeping pointers to this stuff */ char *args; #ifdef CONFIG_SMP /* Per-cpu data. */ void __percpu *percpu; unsigned int percpu_size; #endif #ifdef CONFIG_TRACEPOINTS unsigned int num_tracepoints; struct tracepoint * const *tracepoints_ptrs; #endif #ifdef HAVE_JUMP_LABEL struct jump_entry *jump_entries; unsigned int num_jump_entries; #endif #ifdef CONFIG_TRACING unsigned int num_trace_bprintk_fmt; const char **trace_bprintk_fmt_start; #endif #ifdef CONFIG_EVENT_TRACING struct ftrace_event_call **trace_events; unsigned int num_trace_events; struct trace_enum_map **trace_enums; unsigned int num_trace_enums; #endif #ifdef CONFIG_FTRACE_MCOUNT_RECORD unsigned int num_ftrace_callsites; unsigned long *ftrace_callsites; #endif #ifdef CONFIG_LIVEPATCH bool klp_alive; #endif #ifdef CONFIG_MODULE_UNLOAD //用来在内核模块间建立依赖关系。 /* What modules depend on me? */ struct list_head source_list; /* What modules do I depend on? */ struct list_head target_list; /* Destruction function. */ void (*exit)(void); atomic_t refcnt; #endif #ifdef CONFIG_CONSTRUCTORS /* Constructor functions. */ ctor_fn_t *ctors; unsigned int num_ctors; #endif };
load_module
作为内核模块加载器中最核心的函数,load_module负责最艰苦的模块加载全过程。我们将仔细讨论该函数,因为除了可以了解内核模块加载的幕后机制之外,还能了解到一些非常有趣的特性,诸如内核模块如何调用内核代码导出的函数,被加载的模块如何向系统中其他的模块导出自己的符号,以及模块如何接收外部的参数等。在介绍这部分内容时,如果完全按照内核代码的顺序依序进行的话,逻辑上可能会显得比较凌乱。所以此处文字组织的基本思路是:将load_module函数按照各主要功能分成若干部分,各部分在下文中的出现顺序尽可能维持在代码中的出现顺序:如果某些功能之间存在着某种依赖关系,比如有A和B两个功能,A功能的叙述需要用到B功能中提供的机制,则先介绍B功能:独立于功能模块之外的一些基础设施,比如某些功能性函数,则尽量往前放。
模块ELF静态的内存视图
用户空间程序insmod首先通过文件系统接口读取内核模块demodev.ko的文件数据,将其放在一块用户空间的存储区域中(图中void *umod所示)。然后通过系统调用sys_init_module进入到内核态,同时将umod指针作为参数传递过去(同时传入的还有umod所指向的空间大小len和存放有模块参数的地址空间指针uargs)。
sys_init_module调用load_module,后者将在内核空间利用vmalloc分配一块大小同样为len的地址空间,如图1·2中Elf_Ehdr *hdr所示。然后通过copy_from_user函数的调用将用户空间的文件数据复制到内核空间中,从而在内核空间构造出demodev.ko的一个ELF静态的内存视图。接下来的操作都将以此视图为基础,为使叙述简单起见,我们称该视图为HDR视图(图1·2下方点画线椭圆部分)。HDR视图所占用的内存空间在load_module结束时通过vfree予以释放。
字符串表(string Table)
字符串表是ELF文件中的一个section,用来保存ELF文件中各个section的名称或符号名,这些名称以字符串的形式存在。图1·3给出了一个具体的字符串表实例:
由图1·3可见,字符串表中各个字符串的构成和c语言中的字符串完全一样,都以’\0’作为一个字符串的结束标记。由index指向的字符串是从字符串表第index个字符开始,直到遇到一个’\0’标记,如果index处恰好是,那么index指向的就是个空串(null stnng)。
在驱动模块所在的ELF文件中,一般有两个这样的字符串表section,一个用来保存各section名称的字符串,另一个用来保存符号表中每个符号名称的字符串。虽然同样都是字符串表section,但是得到这两个section的基地址的方法并不一样。
section名称字符串表的基地址为char *secstrings=(char *)hdr+entry[hdr->e_shstrndx].sh_offset。而获得符号名称字符串表的基地址则有点绕:首先要遍历Section
header table中所有的entry,去找一个entry[i].sh_type=SHT_SYMTAB的entry,SHT_SYMTAB表明这个entry所对应的section是一符号表。这种情况下,entry[i].sh_link是符号名称字符串表section在Section header table中的索引值,换句话说,符号名称字符串表所在section的基地址为char *strtab=(char *)hdr+entry[entry[i].shlink].sh_offset。
如此,若想获得某一section的名称(假设该section在section header table中的索引值是i),那么用secstrings+entry[i].sh_name即可。
至此,load_module函数通过以上计算获得了section名称字符串表的基地址secstrings和符号名称字符串表的基地址stb,留作将来使用。
HDR视图的第一次改写
在获得了section名称字符串表的基地址secstrings和符号名称字符串表的基地址strtab之后,函数开始第一次遍历section header table中的所有entry,将每个entry中的sh_addr改写为entry[i].sh_addr=(size_t)hdr+entry[i].offset,这样entry[i].sh_addr将指向该entry所对应的section在HDR视图中的实际存储地址。
在遍历过程中,如果发现CONFIG_MODULE_UNLOAD宏没有定义,表明系统不支持动态卸载一个模块,这样,对于名称为".exit”的section,将来就没有必要把它加载到内存中,内核代码于是清除对应entry中sh_flags里面的SHF_ALLOC标志位。
相对于刚复制到内核空间的HDR视图,HDR视图的第一次改写只是在自身基础上修改了Section header table中的某些字段,其他方面没有任何变化。接下来在“HDR视图的第2次改写”一节中将会看到改写后的HDR视图会再次被改写,在那里,HDR视图中的绝大部分会被搬移到一个新的内存空间中,那也是它们最终的内存位置。
find_sec函数
内核用find_sec来寻找section再Section header table中的索引值,load_module->find_module_sections->find_sec
static unsigned int find_sec(const struct load_info *info, const char *name) { unsigned int i; for (i = 1; i < info->hdr->e_shnum; i++) { Elf_Shdr *shdr = &info->sechdrs[i]; /* Alloc bit cleared means "ignore it." */ if ((shdr->sh_flags & SHF_ALLOC) && strcmp(info->secstrings + shdr->sh_name, name) == 0) return i; } return 0; }
函数返回该的索引值,如果没有找到对应的section,则返回0。该函数的前两个参数分别是ELF文件的ELF header和section header。因为函数要查找的是某一section的name,所以第三个参数就是前面提到的secstrings,第四个参数则是安查找的section的name。函数的具体实现过程非常简单:遍历section header table中所有的entry(忽略没有SHF_ALLOC标志的section,因为这样的section最终不占有实际内存地址),对每一个entry,先找到其所对应的section name,然后和第四个参数进行比较,如果相等,就找到对应的section,返回该section在Section header table中的索引值。
在对HDR视图进行第一次改写之后,内核通过调用find_sec,分别查找以下名称的section:".gnu.linkonce.this_module”,“__versions”和“.modinfo,。查找的索引值分别保存在变量modindex、versindex和infoindex中,以备将来使用。