/*本文由真胖子同志私人定制,烦请转载保留来源http://blog.csdn.net/figtingforlove/article/details/20150957*/
概念:
通过在系统运行期间,加载内核模块来动态的扩展系统该功能而无需重启系统,更无须为这新增添的功能重新编译一个新的系统内核映像。
1. ELF文件格式解析
内核模块采用ELF(Executableand Linkable Format )格式作为文件的数据组织格式,更具体的讲,内核模块是一种普通的可重定位目标文件。
上图忽略了驱动程序模块中不会用到了program header table,可以看到静态的ELF文件视图总体分为三大部分ELF header,中间的section和尾部的section header table。
ⅰ.ELF 头
32位体系结构中,3.8内核用如下数据结构表示ELF头部。
Linux/include/uapi/linux/elf.h
203 typedef struct elf32_hdr{
204 unsigned char e_ident[EI_NIDENT]; //标识elf文件(可容纳16字节)
205 Elf32_Half e_type; //elf文件类型
206 Elf32_Half e_machine; //elf文件所需的体系结构
207 Elf32_Word e_version; //elf 版本信息
208 Elf32_Addr e_entry; //文件在虚拟内存的入口点(程序加载并映射到内存后,执行开始的位置)
209 Elf32_Off e_phoff;//程序头表在elf二进制文件中的偏移量
210 Elf32_Off e_shoff; //段头表在elf二进制文件中的偏移量
211 Elf32_Word e_flags; //特定于处理器的状态
212 Elf32_Half e_ehsize;//elf头的长度
213 Elf32_Half e_phentsize;//程序头表中一项长度
214 Elf32_Half e_phnum; //程序头表中项的数目
215 Elf32_Half e_shentsize; //段头表中一项长度
216 Elf32_Half e_shnum; //段头表中项的数目
217 Elf32_Half e_shstrndx;//段名称字符串表段在段头表的索引
218 } Elf32_Ehdr;
ⅱ.section部分
ELF 文件的主体,保存了与文件相关的各种形式的数据(符号表,实际的二进制码,数值常数等),在模块加载时不同的section会根据自身属性被搬移到新的内存区域(init和core)。
ⅲ.section header table部分
段头表组织为数组,每一项表示一个段的信息,内核定义如下数据结构表示一个sectionheader entry。299 typedef struct elf32_shdr {
300 Elf32_Word sh_name; //段名(在段名串表中的索引值)
301 Elf32_Word sh_type;//段的类型(符号表,重定位,字符串等)
302 Elf32_Word sh_flags;//段标志(可写,是否分配内存等)
303 Elf32_Addr sh_addr;//该项对应的段在虚拟内存中的地址(静态文件视图中为0,模块加载时,加载器会用该段在内存中的实际地址改写该值)
304 Elf32_Off sh_offset;//对应的段在elf文件视图中的偏移量
305 Elf32_Word sh_size;//段的长度
306 Elf32_Word sh_link;//根据段的类型有不同的解释
307 Elf32_Word sh_info; //与sh_link联用
308 Elf32_Word sh_addralign; //段数据在内存中的对齐方式
309 Elf32_Word sh_entsize; //表示固定数量entry组成的段(符号表)中entry的长度
310 } Elf32_Shdr;
在用户空间,利用“insmod demodev.ko”命令加载模块的时候,内核的操作行为是如何实现的呢?
首先,insmod命令通过init_module系统调用陷入内核,执行系统调用函数sys_init_module,在分析这个函数之前我们先来看看内核是如何表示一个模块的,这是一个非常重要的数据结构(这里重点关注蓝色注释部分)。
Linux/include/linux/module.h
229 struct module
230 {
231 enum module_state state; //模块在加载过程中的状态
232
233 /* Member of list of modules */
234 struct list_head list;//组织在内核维护的模块链表中
235
236 /* Unique handle for this module */
237 char name[MODULE_NAME_LEN];//模块名
238
239 /* Sysfs stuff. */
240 struct module_kobject mkobj;//模块的内核对象
241 struct module_attribute *modinfo_attrs;//模块属性
242 const char *version;
243 const char *srcversion;
244 struct kobject *holders_dir;
245
246 /* Exported symbols */ //导出符号
247 const struct kernel_symbol *syms;
248 const unsigned long *crcs;
249 unsigned int num_syms;
250
251 /* Kernel parameters. */ //模块参数
252 struct kernel_param *kp;
253 unsigned int num_kp;
254
255 /* GPL-only exported symbols. */
256 unsigned int num_gpl_syms;
257 const struct kernel_symbol *gpl_syms;
258 const unsigned long *gpl_crcs;
259
…
277 /* symbols that will be GPL-only in the near future. */
278 const struct kernel_symbol *gpl_future_syms;
279 const unsigned long *gpl_future_crcs;
280 unsigned int num_gpl_future_syms;
281
282 /* Exception table */ //模块异常表
283 unsigned int num_exentries;
284 struct exception_table_entry *extable;
285
286 /* Startup function. */
287 int (*init)(void); //内核模块初始化函数(最终为module__init宏指定的函数)
288
289 /* If this is non-NULL, vfree after init() returns */
290 void *module_init; //elf中init段在内核内存的地址
291
292 /* Here is the actual code + data, vfree'd on unload. */
293 void *module_core; //elf中core段在内核内存的地址
294
295 /* Here are the sizes of the init and core sections */
296 unsigned int init_size, core_size; //记录当前init和core
空间的大小
297
298 /* The size of the executable code in each section. */
299 unsigned int init_text_size, core_text_size;
300
301 /* Size of RO sections of the module (text+rodata) */
302 unsigned int init_ro_size, core_ro_size;
303
304 /* Arch-specific module values */
305 struct mod_arch_specific arch;
306
307 unsigned int taints; /* same bits as kernel:tainted */ //模块污染内核标记
…………
316 #ifdef CONFIG_KALLSYMS //模块的所有符号(包括常量,不仅是导出符号)
317 /*
318 * We keep the symbol and string tables for kallsyms.
319 * The core_* fields below are temporary, loader-only (they
320 * could really be discarded after module init).
321 */
322 Elf_Sym *symtab, *core_symtab; //符号表
323 unsigned int num_symtab, core_num_syms;
324 char *strtab, *core_strtab;
325
326 /* Section attributes */
327 struct module_sect_attrs *sect_attrs;
328
329 /* Notes attributes */
330 struct module_notes_attrs *notes_attrs;
331 #endif
332
333 /* The command line arguments (may be mangled). People like
334 keeping pointers to this stuff */
335 char *args; //装载器件传递给模块的命令行参数
…
363
364 #ifdef CONFIG_MODULE_UNLOAD //模块可卸载配置
365 /* What modules depend on me? */ //模块间依赖关系
366 struct list_head source_list;
367 /* What modules do I depend on? */
368 struct list_head target_list;
369
370 /* Destruction function. */
371 void (*exit)(void); //模块卸载函数
372
373 struct module_ref __percpu *refptr;//模块引用计数
...
381 };
了解了模块的内核表示之后,接着来分析这个函数。首先,我们来看一下sys_init_moduele的代码流程图,看看它主要完成什么功能。
Load_module作为内核模块加载器的核心函数,“几乎完成了所有艰苦的工作”,先来介绍这个函数的工作原理,最后我们再分析linxu内核源代码。
(1)从用户空间复制模块数据到内核空间的一个临时内存(vmalloc分配),从而在内核空间构造demodev.ko的ELF静态内存视图。
(2)计算段名称字符串表和符号名称字符串表的基址
定义entry为段头表首址 entry = (void*)hdr + hdr->e_shoff;
段名称字符串表:
Char *sectrings = (char*)hdr + entry[hdr->e_shstrndx].sh_offset;
hdr->e_shstrndx是段名表的索引,entry表示段头表的起始,offset表示段名表的偏移量
符号表:
遍历段头表,找到entry[i].e_type = SHT_SYMTAB的项,Entry[i].e_link是符号名称字符串表的索引
Char *strtab = (char*)hdr + entry[entry[i].e_link].sh_offset;
(3)HDR视图的第一次改写
在上述的HDR视图中,段头表中各项的sh_addr仍然为0,现在应该将该值指向每一项所对应的段在HDR视图中的实际地址了
遍历段头表的每一项修改entry[i]. sh_addr = (size_t)hdr + entry[i].sh_offset;
(4)struct module类型变量mod的初始化
首先,编译模块文件的时候,模块编译工具链会在ELF文件中生成这样一个段section".gnu.linkonce.this_module”,打开编译模块后生的目录,会发现扩展名为“.mod.c”的文件,其中有如下定义
struct module __this_module
8 __attribute__((section(".gnu.linkonce.this_module"))) = {
9 .name = KBUILD_MODNAME,
10 .init = init_module,
11 #ifdef CONFIG_MODULE_UNLOAD
12 .exit = cleanup_module,
13 #endif
14 };
可见定义了一个变量__this_module,并将该数据放到ELF文件的gnu.linkonce.this_module段保存。这里小小说明下,上述变量里模块初始化函数和析构函数的初始化有些另类,原因何在?在我们编写的模块中,使用module_init(my_init)和module_exit(my_exit)宏定义,将我们模块程序中的初始化函数my_init和析构函数my_exit函数变为init_module和cleanup_module的别名,这样就可以执行我们定义的函数了。
那么现在要做的就是利用该段初始化mod变量了。内核调用find_sec(…,name)函数遍历段头表的所有项找到名称相符的段,返回该段的段头表索引值modindex。
Mod = (void*)entry[modindex].sh_addr;
(5)HDR视图的第二次改写
临时内存映像中的段会根据各自flag搬移到新的内存中,内核调用layout_section函数遍历HDR视图中的每个段,对每个段的SHF_ALLOC标记划分为两大类:init段和core段。由于ELF文件中的符号表所在的段没有SHF_ALLOC标记,所以还要有一个对符号表的处理,很据内核配置选项CONFIG_KALLSYMS通过调用layout_symtab函数把符号表搬移到CORE section段。
随后内核调用vmalloc分配init和core对应的内存,mod变量中的module_init和module_core记录这两块的基地址,然后包对应的数据搬到新的内存空间,遍历段头表中各项修改sh_addr指向新的最终内存地址。注意,这里".gnu.linkonce.this_module”段也搬到了core section内存空间,所以要更新mod变量的值。
这样模块的数据就算进入了内核内存,下节会着重介绍模块的导出符号和加载过程中对“未解决引用”符号的处理”。