开篇
本文引用的内核代码参考来自版本linux-5.15.4。
在用户空间,用指令insmod来向内核空间安装一个内核模块,其使用方式如下:
insmod xx.ko /* 向内核空间安装模块 xx */
注意,加载内核模块须要具有root权限,否则会加载失败。
当调用“insmodxx.ko”来安装“xx.ko”内核模块时,insmod会首先借助文件系统的插口,将模块文件的数据读取到用户空间的一段显存中,之后通过系统调用sys_init_module让内核去处理模块加载的整个过程。
系统调用sys_init_module
sys_init_module()函数的原型为:
long sys_init_module(void __user *umod, unsigned long len, const char __user *uargs);
参数umod,是指向用户空间内核模块文件映像数据的显存地址。参数len,是该文件的数据大小。第三个参数uargs,是传给模块的参数在用户空间下的显存地址。
函数的具体代码如下(早已将函数名称替换为实际展开后的方式):
/* */
long sys_init_module(void __user *umod, unsigned long len, const char __user *uargs);
{
int err;
struct load_info info = { };
err = may_init_module(); /* 判断是否有加载模块的权限 */
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%pn", umod, len, uargs);
/* 将模块文件数据复制到内核空间 */
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
/* 加载模块 */
return load_module(&info, uargs, 0);
}
由以上代码可知,加载模块的工作主要是通过load_module函数完成的。
该函数完成模块加载的全部任务,原型为:
static int load_module(struct load_info *info, const char __user *uargs, int flags)
参数info为结构表针,指向储存模块文件数据的结构。参数uargs,与函数sys_init_module的参数uargs相同。参数flags为加载标志。
函数的主要功能为:分配模块须要的显存资源,之后将模块加载到内核中。
模块加载成功,返回值为0。加载失败,则返回错误码(负值)。
关键数据结构
结构体structload_info
在加载过程中会用到一个类型为load_info的结构体变量info,此变量在模块加载过程中临时记录一些参数。结构体load_info的定义如下:
/* */
struct load_info {
const char *name;
/* pointer to module in temporary copy, freed at end of load_module() */
struct module *mod;
Elf_Ehdr *hdr; /* 模块文件内容指针 */
unsigned long len; /* 模块文件大小(字节数) */
Elf_Shdr *sechdrs;
char *secstrings, *strtab;
unsigned long symoffs, stroffs, init_typeoffs, core_typeoffs;
struct _ddebug *debug;
unsigned int num_debug;
bool sig_ok;
#ifdef CONFIG_KALLSYMS
unsigned long mod_kallsyms_init_off;
#endif
struct {
unsigned int sym, str, mod, vers, info, pcpu;
} index;
};
结构体structmodule
此结构体拿来管理系统中加载的模块,是一个十分重要的数据结构。
一个structmodule对象代表着一个内核模块在Linux系统的具象。因为该结构成员变量非常多linux压缩命令,只列举了关键的几个成员变量,并做了注释说明,如下:
/* */
struct module {
enum module_state state; /* 记录模块加载过程中的不同阶段状态 */
struct list_head list; /* 用来将模块链接到系统维护的内核模块链表中 */
char name[MODULE_NAME_LEN]; /* 模块名称 */
const struct kernel_symbol *syms; /* 内核模块导出的符号所在起始地址 */
const s32 *crcs; /* 内核模块导出符号的校验码存放地址 */
struct kernel_param *kp; /* 内核模块参数所在地址 */
int (*init)(void); /* 内核模块初始化函数的指针 */
struct list_head source_list; /* 用来在内核模块之间建立依赖关系 */
struct list_head target_list;
void (*exit)(void); /* 内核模块退出函数指针 */
};
模块加载过程不同阶段的状态,module_state定义如下:
enum module_state {
MODULE_STATE_LIVE, /* 模块成功加载进系统时的状态 */
MODULE_STATE_COMING, /* 配置完成,开始加载模块 */
MODULE_STATE_GOING, /* 加载过程出错,退出加载 */
MODULE_STATE_UNFORMED, /* 正在建立加载配置 */
};
加载函数load_module
此函数主要分两部份功能:一部份完成模块加载最核心的任务;第二部份是,模块被加载到系统的后续处理。
load_module第一部份
通过调用copy_module_from_user()函数,将用户空间的模块文件数据复制到内核空间中,因而在内核空间构造出模块的一个ELF静态显存视图。也就是HDR视图,加载完成后会将其释放。
字符串表是ELF文件中的一个section,拿来保存ELF文件中各个section的名称或符号。通过调用setup_load_info(info,flags);会创建这个字符串表,并得到section名称字符串表的基地址secstrings。
通过调用此函数,内核找寻某一个section在sectionheadertable中的索引值。分别查找以下section:“.modinfo”、“__versions”、“.gnu.linkonce.this_module”,保存查到的索引值,以备将来使用。
第一次遍历sectionheadertable中的所有entry,更改entry中的sh_addr,估算句子如下:
shdr->sh_addr = (size_t)info->hdr + shdr->sh_offset;
这样每位entry中的sh_addr指向该entry所对应的section在HDR视图中的实际储存地址。
结构体structmodule是一个十分重要的数据结构,内核拿来表示一个模块。在load_module函数中定义了一个structmodule类型的变量mod。调用mod=layout_and_allocate(info,flags);分配须要的显存,并初始化。
此次改写中,HDR视图中绝大多数的section会被搬动到新的显存空间中,致使其中sectionheadertable中各个entry的sh_addr指向最终的显存地址。
模块可以向外部导入自己的符号。假如一个内核向外界导入了自己的符号,这么模块编译工具链负责生成那些导入的符号section。而这种section都带有SHF_ALLOC标志,模块在加载过程中会被搬动到COREsection区域中。
第一部份,在内核导入的符号表中查找指定的符号。第二部份linux加载服务命令,在系统早已加载的模块导入的符号表中查找符号。
“未解决的引用符号“,就是模块编译链接生产.ko文件时,对于模块中调用的一些函数,链接工具难以在所有的目标文件中找到某个函数的指令码,链接工具会将这个符号标记为”未解决的引用符号“。模块被加载时linux加载服务命令,内核会解决那些符号。
主要拿来解决静态链接时的符号引用,与动态加载时的实际符号地址不一致的问题。
在用insmod加载模块时,有时须要向模块传递一些参数,内核模块本身在源代码中必须用宏module_param申明模块可以接收的参数。内核加载器可以得到从命令行传过来的实际参数。
内核能跟踪模块间的依赖关系,在模块加载过程中,构建模块之间的依赖关系。
版本控制主要拿来解决内核模块和内核之间的插口一致性问题。防止模块使用内核早已改变或废弃的插口,而造成加载失败或则存在风险的问题。
模块最终的ELF文件中就会有一个名为”.modinfo“的section,以文本方式保留着模块的一些信息。加载过程中,内核须要获得”.modinfo“section中的相关信息,便于进一步处理。包括:模块的license、模块的vermagic。
load_module第二部份
第二部份通过调用do_init_module()函数完成。
在这个函数中,首先调用模块的构造函数do_mod_ctors()。之后调用模块的初始化函数,也就是mod中init表针指向的函数linux运维博客,初始化函数通过do_one_initcall()完成调用。
模块完成加载以后,HDR视图和INITsection所占的显存空间不再使用,须要释放它们。
HDR视图在调用do_init_module()之前完成释放(调用free_copy(info))。INITsection在do_init_module()结尾完成释放。
模块加载进系统以后,链接到内核维护的模块数组modules中,该数组记录着系统中所有已加载的模块。