内核模块加载过程分析

章地址如下:

ELF文件格式解析https://blog.csdn.net/zj82448191/article/details/108441447

在用户空间,用insmod这样的命令来向内核空间安装一个内核模块,本章将详细讨论模块加载时的内核行为,当我们加载一个模块时,insmod会首先利用文件系统的接口将其数据读取到用户空间的一段内存中,然后通过系统调用sys_init_module,让内核去处理加载的整个过程。

一、sys_init_module函数分析

我们把sys_init_module函数分为两个部分,第一部分是调用load_module(),完成模块加载最核心的任务,第二部分是模块加载到系统之后的处理。在分析之前我们先介绍两个结构体,这两个结构体在后面的介绍中都用到了。

linux-3.10.70\kernel\module.c

struct load_info {
	Elf_Ehdr *hdr;         // ELF文件头
	unsigned long len;     // 文件长度,似乎除了校验的时候用了一下,再也没用过
	Elf_Shdr *sechdrs;     // 节区头部表
	char *secstrings, *strtab;    // section 名称表, 字符名称表
	unsigned long symoffs, stroffs;    // 符号表,字符串表在最终core section中的偏移
	struct _ddebug *debug;
	unsigned int num_debug;
	bool sig_ok;
#ifdef CONFIG_KALLSYMS
	unsigned long mod_kallsyms_init_off;
#endif
    /*sym 为符号表在secton headers 中的index
     *str 为字符串表在section header 中的index
     */
	struct {
		unsigned int sym, str, mod, vers, info, pcpu;
	} index;
};
linux-3.10.70\include\linux\module.h

struct module {
    // 用来记录模块加载过程中不同阶段的状态
	enum module_state state;

	/* Member of list of modules */
	// 可以看到内核使用链表来管理module
	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. */
	unsigned int num_gpl_syms;
	const struct kernel_symbol *gpl_syms;
	const unsigned long *gpl_crcs;

	/* 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;

	/* Startup function. */
	// 这就是我们用module_init(xxx)来声明的入口函数
	int (*init)(void);

	/* 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_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_MODULE_UNLOAD
	/* What modules depend on me? */
	struct list_head source_list;
	/* What modules do I depend on? */
	struct list_head target_list;

	/* Who is waiting for us to be unloaded */
	struct task_struct *waiter;

	/* Destruction function. */
	void (*exit)(void);

	struct module_ref __percpu *refptr;
#endif
}

1.1第一部分

第一部分的内容都是由load_module完成的,所以我们这里详细的介绍该函数。

1.1.1用户空间到内核空间

我们知道insmod会首先利用文件系统的接口将其数据读取到用户空间的一段内存中,然后通过系统调用sys_init_module,让内核去处理加载的整个过程。调用sys_init_module的时候,会把.ko文件的首地址及大小通过参数传入,然后load_module在内核空间中使用vmalloc开辟大小一样的空间,将文件数据都复制到内核空间。从而在内核空间构建出一个demo.ko文件的ELF静态的内存视图。接下来的操作都将以此视图为基础,我们成这个视图为HDR视图。
这部分是我截图出来的别的地方的图,供大家借鉴,从图中我们可以清楚的看到上述的过程。
在这里插入图片描述

1.1.2 HDR视图的第一次修改

我们把文件的数据拷贝到内核空间之后,需要对一些数据进行修改,视图的第一次修改只修改了节点头部表中sh_addr该变量,我们知道这个变量表示节区在进程内存中的起始地址。所以在拷贝到内核之后需要重新修改,遍历一次节区头部表,将每个entry的sh_addr改为
entry【i】.sh_addr = (size_t)hdr+entry【i】.sh_offset
因为hdr为文件在内存中的其实位置,sh_offset为节区的偏移位置。

1.1.3 mod变量初始化

load_module函数中定义有一个struct module 类型的mod变量,该变量的初始化是通过模块文件.ko中的一个节区来实现的,该节区为:
.gun.linkonce…thie_module。
在ELF文件中为什么会有这个段,其实是模块的编译工具链完成的,如果我们仔细看模块编译后的文件,会发现一个.mod.c的文件,查看该文件内容会发现如下定义:

struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
 .name = KBUILD_MODNAME,
 .init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
 .exit = cleanup_module,
#endif
 .arch = MODULE_ARCH_INIT,
};

这里我们只要找到这个段在内存中的其实地址,然后就能赋值给mod变量了,内核中find_sec函数可以根据某一节区查找到该节区在节区头部表中的标号,进而找到该节区在内存中的开始地址。
mod = (void *)sechdrs[modindex].sh_addr
于是到这里位置,在第一次修改完视图之后,mod指向了实际的该节区的初始地址,接下来会进行视图的第二次改写,会再次更新mod的地址。

1.1.4HDR视图的第二次改写

这里为什么会有第二次视图的改写,是因为很多节区是不需要加载到文件的内存映像中,所以第二次改写的时候,内核会把所有的节区遍历一遍,若是节区头部表中节区sh_flgs变量的值SHF_ALLOCK,则会根据节区名,将节区分为两部分,CORE和INIT。以.init开头的节区分到INIT部分,其余分到CORE部分。
在对所有节区进行分类的过程中,会记录下当前节区在该类中的偏移量,保存在节区头部表中sh_entsize部分。
enrty【i】.sh_entsize=mod->core_size; 或者 enrty【i】.sh_entsize=mod->init_size;
同时记录该节区分配之后,两个类的空间大小:
mod->core_size += entry[i].sh_size;或者mod->init_size +=entry[i].sh_size;
这里分好类之后搬移之前会再做一个工作,是对符号字符表的处理,这里我们介绍一个宏:CONFIG_KALLSYMS,这个宏是一个决定内核映像中是否保留所有符号的配置选项,在内核Kconfig中,如果没有打开这个宏,这里我们上面都不做,直接进行HDR视图的第二次改写,但是如果定义了,内核模块的符号都会放到ELF文件中的一个节区中,但是由于符号表的节区没有 SHF_ALLCOK标志,所以需要将这个节区也手动分到CORE类中。
所有的节区都分完类之后,内核会调用vmalloc分配两个类对应的内存空间,基地址分别记录在:
mod->module_core跟 mo->module_init,然后将视图搬移到新申请的内存上,显然,这里搬移成功之后需要再次修改节区头部表中每个节区的sh_addr的值,使其指向新的地址。
这里还需要更新一下mod变量,使其指向新的地址。
mod = (void*) entry[modindex].sh_addr;

为什么内核会进行第二次分类,这里再次说明一下,因为在模块加载过程结束时,系统会释放掉HDR所在的内存区域,在模块是初花完成之后,INIT 类的区域也会被释放掉,由此可见,当一个模块被成功记载切初始化完成后,最终留下的仅仅是CORE 类的内容,这些数据才是在系统整个运行期间存活的数据。
下面分享第二次搬移过程图:

在这里插入图片描述
到此为止,视图的搬移就结束了,但是这个时候工作还没有完成,因为我们还没有解决引用的符号问题。

1.1.5 符号问题

我们知道内核源码中存在大量的EXPORT_SYMBOL这样的宏,我们一般只是知道它是想外面导出一个符号,但是不知道其中的原理,这里我们研究一下。

如果没有独立存在的内核模块,EXPORT_SYMBOL就是去了存在的意义,因为对于静态编译了解的内核映像来说,所有的符号引用都在静态链接的时候完成了,但是内核模块不可避免的必须使用内核提供的基础设施,比如Printk函数,作为独立编译的内核模块,要解决这种问题,就必须找到引用的符号在内存的实际地址。
从全局来看,EXPORT_SYMBOL分为宏定义部分,链接脚本链接器部分和使用导出符号部分,下面来说这三部分:
第一部分
在这里插入图片描述
从上面的宏定义我们可以看到,EXPORT_SYMBOL的宏定义实际上就是定义了两个变量,这里我们使用EXPORT_SYMBOL(my_exp_function)作为一个例子。导出这个函数,相当于定义了两个变量如下:

static  const  char * _kstrtab_my_exp_function = "my_exp_function";

static  const struct kernel_symbol __kstrtab_my_exp_function = 
{(unsigned long )&my_exp_function, _kstrtab_my_exp_function }

这里介绍一下kernel_symbol结构体
  struct kernel_symbol{
			unsigned long value;
			const char* name;	
		}

​​​​​​​

通过以上定义,我们可以知道,导出符号其实就是将符号名称及其地址通过kernel_symbol这个结构体告诉外部。
从上面的宏定义中还可以看到第二个信息,就是指针变量_kstrtab_my_exp_function 将会被放到_ksymtab_strings节区中,
__kstrtab_my_exp_function变量会放到_ksymtab节区中。

第二部分
这一部分是链接脚本及链接器,我们这里粘出虚拟机的连接脚本。
在这里插入图片描述
在这里插入图片描述
从链接脚本中我们可以看到,定义了好多变量,我们这里只看EXPORT_SYMBOL类型的,即:__start__ksymtab跟__stop__ksymtab
__start__kcrctab跟__stop__kcrctab

我们这里先不讲解第三部分,先研究一下模块的导出符号。
模块导出的符号经过编译工具链生成这些导出符号的节区,这些节区都带有SHF_ALLOC标志,所以在模块加载过程中会搬移到core 类的空间中。如果没有导出符号,则不会产生写个节区。
显然,如果内核别的模块想使用一个内核模块导出的符号,就要对这些导出符号的地址有所了解,所以会记录下来这些符号的地址,这里内核通对HDR视图的查找,获取t_ksymtab节区在core 空间的地址,然后将这个地址记录到mod->syms中,这样内核通过该变量就能找到模块导出符号的所有信息。具体图如下:
在这里插入图片描述

到这里位置,我们总结一下,我们知道了不管是内核还是内核模块,对于导出符号,都有相应的指针变量保存了这些符号节区在内存空间中的地址,所以当我们需要使用某个符号的时候,是不是就是在这些区域里寻找呢?我们接着分析

第三部分

这部分是解决那些“”未解决的引用“”问题,所谓的“”“未解决的引用”就是模块工具链编译模块生成.KO文件的时候,对于模块中调用的一些函数,例如printk函数,链接工具无法在该模块的所有文件中找到该引用的指令码(因为这个函数是内核实现,指令码存在于内核的目标文件中),所以就会将这个引用定义未解决,对他的处理一直延续到内核加载时,至于怎么解决,我们接着分析。

内核中有一个查找符号的函数find_symbol,由于我这里不想过多的分析代码,所以我把这个函数的功能给大家说一下,该函数的功能就是内核与内核模块的符号节区中寻找name一样的符号,然后找到地址之后就可以引用了。
至于找到路径,是先从内核映像的导出符号表寻找 找不到则去模块链表中一个一个的模块导出符号表中寻找:
在这里插入图片描述
在内核模块的加载过程中,会有一个专门的函数解决那些“”未解决的引用“”问题,这个函数会给每个我们模块引用的符号找到它保存在节区中的对应的struct kerne_synbol变量,该变量之前我们介绍过,保存了符号名称与地址,但是找到这个地址就真的找到符号存在的地址了吗??? 我们思考一下,之前我们内核模块加载的时候,我们只是搬移了这些节区,并没有对这些节区中的地址做修改,这些符号的地址还是编译时候的地址,所以我们即使找到了,也不是采用这些地址。负责会出现大问题。linux内核对这一问题的解决引入了重定位。

1.1.6重定位

重定位的作用主要是解决静态链接时与动态加载时实际符号不一致的问题,上一节结束部分提到的模块导出的符号地址,就是需要重定位的一个典型的例子。
重定位的部分先不讲解了 。有兴趣的可以自己研究

1.2 第二部分

load_module函数完成了模块加载的所有困难的东西,接着返回到sys_init_module,之后做的操作就很简单了。

1.调用模块的是初始化函数。
这里调用mod函数中的init指向的函数,这之前已经分析过了,就不在叙述,需要知道的是,模块可以不提供初始化函数,如果提供了,则在初始化函数执行之后,需要修改模块的状态位MODULE_STATE_LIVE;

2.释放INIT 类的section占用的空间
调用vfree来释放mod->module_init,,这里再提一下,模块加载之后也会释放掉视图所在部分的内存,不过这部分是在load_module函数最后面执行的。

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核加解密流程主要涉及以下几个方面: 1. 加密算法的选择与配置:Linux内核支持多种加密算法,如AES、DES、RSA等。在编译内核时,需要根据具体需求选择相应的加密算法,并进行相应的配置。 2. 加密驱动程序的加载:加密操作通常是通过特定的硬件设备或者加密芯片来完成的,因此需要加载对应的驱动程序。Linux内核通过加载相应的加密驱动程序与硬件设备进行通信。 3. 加密API的调用:Linux内核提供了一系列的加密API,供应用程序或其他内核模块调用。这些API包括加密函数、解密函数以及相关的密钥管理函数等。应用程序或内核模块可以通过这些API来完成具体的加解密操作。 4. 加解密数据的传输:加密数据通常通过IO通道进行传输。Linux内核提供了IO通道的相关接口,包括文件IO、网络IO等。在加密数据的传输过程中,需要保证数据的完整性和安全性。 5. 加解密数据的处理:Linux内核在接收到加密数据后,通过相应的加密算法进行解密操作,将加密的数据还原为原始数据。类似地,在需要对数据进行加密时,通过加密算法对原始数据进行加密处理。这个过程需要根据具体的加密算法和密钥进行。 总的来说,Linux内核加解密流程包括选择和配置加密算法、加载加密驱动程序、调用加密API来进行加解密操作、通过IO通道进行数据传输与处理等环节。这个流程保证了Linux系统中的数据安全性和隐私保护。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值