linux内核加载阶段,转载--Linux可加载内核模块剖析

剖析内核模块

LKM

与直接编译到内核或典型程序的元素有根本区别。典型的程序有一个 main 函数,其中 LKM 包含 entry 和 exit 函数(在 2.6

版本,您可以任意命名这些函数)。当向内核插入模块时,调用 entry 函数,从内核删除模块时则调用 exit 函数。因为 entry 和

exit 函数是用户定义的,所以存在 module_init 和 module_exit 宏,用于定义这些函数属于哪种函数。LKM 还包含一组必要的宏和一组可选的宏,用于定义模块的许可证、模块的作者、模块的描述等等。图 1 提供了一个非常简单的 LKM 的视图。

图 1. 简单 LKM 的源代码视图

1fb57292c608fefb5b740d77afb95342.png

2.6 版本的 Linux 内核提供了一个新的更简单的方法,用于构建 LKM。构建 LKM 时,可以使用典型的用户工具管理模块(尽管内部已经改变):标准 insmod(安装 LKM),rmmod (删除 LKM),modprobe(insmod 和 rmmod 的包装器),depmod(用于创建模块依赖项),以及 modinfo(用于为模块宏查找值)。

剖析内核模块对象

LKM

只不过是一个特殊的可执行可链接格式(Executable and Linkable

Format,ELF)对象文件。通常,必须链接对象文件才能在可执行文件中解析它们的符号和结果。由于必须将 LKM 加载到内核后 LKM

才能解析符号,所以 LKM 仍然是一个 ELF 对象。您可以在 LKM 上使用标准对象工具(在 2.6 版本中,内核对象带有后缀 .ko,)。例如,如果在 LKM 上使用 objdump 实用工具,您将发现一些熟悉的区段(section),比如 .text(说明)、.data(已初始化数据)和 .bss(块开始符号或未初始化数据)。

您还可以在模块中找到其他支持动态特性的区段。.init.text 区段包含 module_init 代码,.exit.text 区段包含 module_exit 代码(参见图 2)。.modinfo 区段包含各种表示模块许可证、作者和描述等的宏文本。

图 2. 具有各种 ELF 区段的 LKM 的示例

16ae43c23424d606f8ccd4333bfbd63f.png

了解 LKM 的基础知识之后,现在我们进一步探索模块是如何进入内核的,以及在内核内部是如何管理模块的。

c6ccbc96c20bebb0d73e8e6368158ef5.png

LKM 的生命周期

在用户空间中,insmod(插入模块)启动模块加载过程。insmod 命令定义需要加载的模块,并调用 init_module 用户空间系统调用,开始加载过程。2.6 版本内核的 insmod 命令经过修改后变得非常简单(70 行代码),可以在内核中执行更多工作。insmod 并不进行所有必要的符号解析(处理 kerneld),它只是通过 init_module 函数将模块二进制文件复制到内核,然后由内核完成剩余的任务。

init_module 函数通过系统调用层,进入内核到达内核函数 sys_init_module(参见图 3)。这是加载模块的主要函数,它利用许多其他函数完成困难的工作。类似地,rmmod 命令会使 delete_module 执行 system call 调用,而 delete_module 最终会进入内核,并调用 sys_delete_module 将模块从内核删除。

图 3. 加载和卸载模块时用到的主要命令和函数

e659a0862f554a6384ed19a9dcb3bea1.png

在模块的加载和卸载期间,模块子系统维护了一组简单的状态变量,用于表示模块的操作。加载模块时,状态为 MODULE_STATE_COMING。如果模块已经加载并且可用,状态为 MODULE_STATE_LIVE。此外,卸载模块时,状态为 MODULE_STATE_GOING。

c6ccbc96c20bebb0d73e8e6368158ef5.png

模块加载细节

现在,我们看看加载模块时的内部函数(参见图 4)。当调用内核函数 sys_init_module 时,会开始一个许可检查,查明调用者是否有权执行这个操作(通过 capable 函数完成)。然后,调用 load_module 函数,这个函数负责将模块加载到内核并执行必要的调试(后面还会讨论这点)。load_module 函数返回一个指向最新加载模块的模块引用。这个模块加载到系统内具有双重链接的所有模块的列表上,并且通过 notifier 列表通知正在等待模块状态改变的线程。最后,调用模块的 init() 函数,更新模块状态,表明模块已经加载并且可用。

图 4. 内部(简化的)模块加载过程

d07f8e60f6d02db75ae9c007f4be7566.png

加载模块的内部细节是 ELF 模块解析和操作。load_module 函数(位于 ./linux/kernel/module.c)首先分配一块用于容纳整个 ELF 模块的临时内存。然后,通过 copy_from_user 函数将 ELF 模块从用户空间读入到临时内存。作为一个 ELF 对象,这个文件的结构非常独特,易于解析和验证。

一步是对加载的 ELF 映像执行一组健康检查(它是有效的 ELF 文件吗?它适合当前的架构吗?等等)。完成健康检查后,就会解析 ELF

映像,然后会为每个区段头创建一组方便变量,简化随后的访问。因为 ELF 对象的偏移量是基于 0

的(除非重新分配),所以这些方便变量将相对偏移量包含到临时内存块中。在创建方便变量的过程中还会验证 ELF 区段头,确保加载的是有效模块。

任何可选的模块参数都从用户空间加载到另一个已分配的内核内存块(第 4 步),并且更新模块状态,表明模块已加载(MODULE_STATE_COMING)。如果需要 per-CPU 数据(这在检查区段头时确定),那么就分配 per-CPU 块。

在前面的步骤,模块区段被加载到内核(临时)内存,并且知道哪个区段应该保持,哪个可以删除。步骤 7 为内存中的模块分配最终的位置,并移动必要的区段(ELF 头中的 SHF_ALLOC,

或在执行期间占用内存的区段)。然后执行另一个分配,大小是模块必要区段所需的大小。迭代临时 ELF

块中的每个区段,并将需要执行的区段复制到新的块中。接下来要进行一些额外的维护。同时还进行符号解析,可以解析位于内核中的符号(被编译成内核映象),

或临时的符号(从其他模块导出)。

然后为每个剩余的区段迭代新的模块并执行重新定位。这个步骤与架构有关,因

此依赖于为架构(./linux/arch//kernel/module.c)定义的 helper

函数。最后,刷新指令缓存(因为使用了临时 .text 区段),执行一些额外的维护(释放临时模块内存,设置系统文件),并将模块最终返回到 load_module。

在 ./kernel/module.c 定义sys_init_module如下:

(void

*

,

unsigned long

,

const char

*

)

{

struct

*

;

int

= 0;

/* Must have permission */

if (!

(

))

return -

;

/* Only one module load at a time, please */

if (

(&

) != 0)

return -

;

/* Do all the hard work */

=

(

,

,

);

if (

(

)) {

(&

);

return

(

);

}

/* Drop lock so they can recurse */

(&

);

(&

,

,

);

/* Start the module */

if (

->

!=

)

=

->

();

if (

/* Init routine failed: abort. Try to protect us from

buggy refcounters. */

->

=

;

();

(

);

(&

,

,

);

(&

);

(

);

(&

);

(&

);

return

;

}

if (

> 0) {

(

"%s: '%s'->init suspiciously returned %d, "

"it should follow 0/-E convention\n"

"%s: loading module anyway...\n",

,

->

,

,

);

();

}

/* Now it's a first class citizen! Wake up anyone waiting for it. */

->

=

;

(&

);

(&

);

/* Drop initial reference. */

(

);

(

->

, 1);

(

,

->

);

->

=

;

->

= 0;

->

= 0;

(&

);

return 0;

}

模块卸载细节

卸载模块的过程和加载模块基本一样,除了必须进行几个健康检查外(确保安全删除模块)。卸载模块过程首先在用户空间调用 rmmod(删除模块)命令。在 rmmod 命令内部,对 delete_module 执行系统调用,它最终会导致在内核内部调用 sys_delete_module(查看

)。图 5 演示了删除模块的基本操作过程。

图 5. 内部(简化的)模块卸载过程

1bea5ac458af3f300e8423b1bcd864c3.png

当调用内核函数 sys_delete_module(将要删除的模块的名称作为参数传入)之后,第一步便是确保调用方具有权限。接下来会检查一个列表,查看是否存在依赖于这个模块的其他模块。这里有一个名为 modules_which_use_me 的列表,它包含每个依赖模块的一个元素。如果这个列表为空,就不存在任何模块依赖项,因此这个模块就是要删除的模块(否则会返回一个错误)。接下来还要测试模块是否加载。用户可以在当前安装的模块上调用 rmmod,因此这个检查确保模块已经加载。在几个维护检查之后,倒数第二个步骤是调用模块的 exit 函数(模块内部自带)。最后,调用 free_module 函数。

调用 free_module

函数之后,您将发现模块将被安全删除。该模块不存在依赖项,因此可以开始模块的内核清理过程。首先,从安装期间添加的各种列表中(系统文件、模块列表等)

&

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值