目录
.ko文件格式
"K0"文件通常是指Linux内核模块文件,这些文件包含了可以在运行中的Linux内核中加载和卸载的代码。
内核模块允许你在不重新编译整个内核的情况下,向Linux内核添加新功能、驱动程序或其他功能。
K0文件的文件格式实际上就是编译后的二进制文件,其结构和格式取决于所加载的内核模块的编译和源代码。
一般情况下, Ko文件是一种ELF (Executable and Linkable Format,可执行与可链接格式)文件,这也是Linux 上常见的可执行文件格式之一。
文件开始处是一个ELF头部(ELF Header ),用来描述整个文件的组织,这些信息独立于处理器,也独立于文件中的其余内容。我们可以使用 readelf 工具查看 elf文件的头部详细信息。
下面是KO模块ELF头部信息的一些重要字段和描述:e_ident: ELF文件的标识信息,它包含了文件的一些基本属性,如文件类型、字节序、文件版本等。
e_type:描述ELF文件类型的字段。
对于内核模块来说,这个字段的值通常是 ET_REL,表示可重定位文件。
e_machine:指定了目标机器的体系结构,如x86、 ARM等。
e_version: ELF 版本号。
e_entry:对于可执行文件来说,这是程序的入口点地址。
对于可重定位文件(如内核模块) ,这个字段一般是0。
e_phoff:指定了程序头表(Program Header Table)的偏移量。
内核模块一般没有程序头表。
e-shoff:指定了节头表(Section Header Table)的偏移量。
节头表包含了关于文件中各个节的描述信息。
e_flags:一些标志位,用于指示文件的特性。
e_ehsize: ELF 头部的大小,以字节为单位。
e_phentsize和e_phnum:描述程序头表条目的大小和数量。
内核模块通常没有程序头表。
e_shentsize和e_shnum:描述节头表条目的大小和数量。
e_shstrndx: 指定了包含节名称的字符串表在节头表中的索引。
对于内核模块(KO 文件)来说,节头表中的各个节包含了模块的重要信息,如代码段、数据段、符号表、字符串表等。
KO模块的结构可能因模块的功能和实现方式而有所不同,因此具体的节和字段可能会有所变化。
内核模块加载过程
在前面我们了解了 ko 内核模块文件的一些格式内容之后, 我们可以知道内核模块其实也是一段经过特殊加工的代码,那么既然是加工过的代码,内核就可以利用到加工时留在内核模块里的信息,对内核模块进行利用。所以我们就可以接着了解内核模块的加载过程了。
首先 insmod 会通过文件系统将.ko 模块 读到用户空间的一块内存中,然后执行系统调用 sys_init module()解析模组,这时,内核在 vmalloc 区分配与 ko文件大小相同的内存来暂存 ko 文件,暂存好之后解析 ko 文件,将文件中的各个section分配到init段和core段,在modules 区为 init 段和 core段分配内存,并把对应的 sectioncopy 到 modules 区最终的运行地址,经过 relocate 函数地址等操作后,就可以执行ko的 init 操作了,这样一个ko 的加载流程就结束了。同时,init 段会被释放掉,仅留下 core 段来运
SYSCALL DEFINE3(init module,void __user *, umod, unsigned long, len, const char,uargs)
{
int err;
struct load info info ={};
err = may init module();if(err)
return err;
pr debug("init module: umod=%p,len=%lu, uargs=%p\n",umod,len,uargs);
err =copy_module_fromuser(umod,len,&info);//<-if(err)
return err;
return load module(&info,uargs,0);//
}
第14行:通过vmalloc在vmalloc区分配内存空间,将内核模块copy到此空间,info->hdr 直接指向此空间首地址,也就是ko的elf header 。
第18行:然后通过load_module()进行模块加载的核心处理,在这里完成了模块的搬移,重定向等艰苦的过程。
下面是load_module()的详细过程,代码已经被我简化,主要包含setup_load_info()和layout_and_allocate()。
/* 分配并加载模块 */
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
struct module *mod;
long err = 0;
char *after_dashes;
...
err = setup_load_info(info, flags);//<---------
...
mod = layout_and_allocate(info, flags);//<--------------
...
}
第9行:setup_load_info()加载struct load_info 和 struct module, rewrite_section_headers,将每个section的sh_addr修改为当前镜像所在的内存地址, section 名称字符串表地址的获取方式是从ELF头中的e_shstrndx获取到节区头部字符串表的标号,找到对应section在ELF文件中的偏移,再加上ELF文件起始地址就得到了字符串表在内存中的地址。
第11行:在layout_and_allocate()中,layout_sections() 负责将section 归类为core和init这两大类,为ko的第二次搬移做准备。move_module()把ko搬移到最终的运行地址。内核模块加载代码搬运过程到此就结束了。
但此时内核模块要工作起来还得进行符号导出。
内核模块卸载过程
卸载过程相对加载比较简单,我们输入指令rmmod,最终在系统内核中需要调用 sys_delete_module 进行实现。
具体过程如下:先从用户空间传入需要卸载的模块名称,根据名称找到要卸载的模块指针, 确保我们要卸载的模块没有被其他模块依赖,然后找到模块本身的exit函数实现卸载。 代码如下。
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
struct module *mod;
char name[MODULE_NAME_LEN];
int ret, forced = 0;
if (!capable(CAP_SYS_MODULE) || modules_disabled)
return -EPERM;
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
return -EFAULT;
name[MODULE_NAME_LEN-1] = '\0';
audit_log_kern_module(name);
if (mutex_lock_interruptible(&module_mutex) != 0)
return -EINTR;
mod = find_module(name);
if (!mod) {
ret = -ENOENT;
goto out;
}
if (!list_empty(&mod->source_list)) {
ret = -EWOULDBLOCK;
goto out;
}
/* Doing init or already dying? */
if (mod->state != MODULE_STATE_LIVE) {
/* FIXME: if (force), slam module count damn the torpedoes */
pr_debug("%s already dying\n", mod->name);
ret = -EBUSY;
goto out;
}
if (mod->init && !mod->exit) {
forced = try_force_unload(flags);
if (!forced) {
/* This module can't be removed */
ret = -EBUSY;
goto out;
return ret;
}
第8行:确保有插入和删除模块不受限制的权利,并且模块没有被禁止插入或删除
第11行:获得模块名字
第20行:找到要卸载的模块指针
第26行:有依赖的模块,需要先卸载它们
第39行:检查模块的退出函数
第48行:停止机器,使参考计数不能移动并禁用模块
第56行:告诉通知链module_notify_list上的监听者,模块状态变 为MODULE_STATE_GOING。
第60行:等待所有异步函数调用完成