SylixOS动态加载器系列文章(4) 内核模块加载原理

       通过前面ELF格式章节我们已经知道内核模块文件的特点,它只有节区视图,很多链接工作尚未完成,但也给内核模块加载器带来了更大的操作空间。加载器可以精确控制每个节区的地址以及每个符号的位置。本系列文件我们虽然着重介绍SylixOS加载器,但linux的加载流程和SylixOS大部分相似,但是也有不同点,本文也将在一些不同的地方加以说明。内核模块的加载流程分以下几个步骤:

1、  读取ELF文件到内存。

2、  符号重定位。

3、  导出符号到内核符号表。

4、  调用模块初始化函数。

下面将逐一介绍上述流程。

1.1     读取ELF文件到内存

我们不能简单的将ELF文件已流的形式全部加载到内存,而要在加载的过程中需要完成节的组装,将各个节按顺序拼接成一个可执行的连续内存块。内存块由哪几部分组成呢?

ELF文件中存在很多不需要加载的节,这些节中的信息在程序调试等时候会被使用到,而在执行时确是无用的。此外还有ELF的组织结构信息如重定位表、符号表、符号字符串表、以及ELF头在加载完成后这些信息会被保存到系统更加高层次的结构体中,这些节会被加载到临时内存,加载完成后释放。加载器只需要加载带SHF_ALLOC标记的节。

一般情况下一个可执行块包含ELF由text节、data节、bss节以及common节组合而成。其中text节和data节从ELF文件中读出,也就是带SHF_ALLOC标记的节。而bss节和common节则是在加载时生成。bss节保存未初始化的全局变量,所以无需浪费磁盘空间。Common节一般用于保存弱符号,这里必须介绍一下弱符号的概念,弱符号是模块依赖的外部符号,但不是必须的,如果加载器能够在模块外部(其它模块或内核中)找到该外部符号,则将本模块对该符号的引用重定位到外部符号,如果找不到,则使用common区中为其与分配的地址,该地址上一般填写0。

模块对外部符号的引用一般都使用相对跳转指令,而在大部分体系结构中,相对跳转指令的范围是有限的,如果我们将一个模块加载到了距离其引用的外部符号很远的位置,则会超出相对跳转指令范围。SylixOS内核一般加载在低端地址,而模块可能会被分配到高端地址,这种情况下跳转指令无法重定位。为解决这个问题,我们生成一个跳转表,这个表的作用是将相对地址跳转转换为绝对地址跳转。具体实现方法将在后续章节介绍。

总结一下,到目前为止,我们知道一个内核模块的可执行内存块中至少应该包含以下几段数据:

l  ELF文件中所有带SHF_ALLOC标记的节,一般是data节和text节。

l  加载器生成的BBS节和common节。

l  外部符号跳转表。

如下图:


在内核模块文件加载过程中,内存的计算和初始化是最为复杂的一部分,不但要考虑内存分配,文件读取,还要考虑符号对齐属性,指令跳转范围等因素。

1.2     符号重定位

读取ELF文件到内存后,加载器会记录每个节在内存段中的位置,同时前面也有说到,重定位表、符号表、字符串表都会被读取到临时内存区域。符号重定位主要处理重定位表,重定位表表头保存在节信息表中(ELF起始有一张保存有所节信息的节信息表),表头包含如下信息。

l  本重定位节在文件中的偏移

l  重定位表大小,可以由此计算表项数。

l  相关目标节,表示本重定位表影响该节的内容。

l  符号节,表示本重定位表重定位该符号节中的符号。

l  符号字符串节,表示符号名称字符串在该节查找。

每个重定位表项中包含如下信息:

l  符号,配合上面的符号节,表面本项处理的符号。

l  目标地址,配合上面的目标节,表面本项影响的地址。

l  重定位类型,编译器会根据不同的指令生成不同的重定位类型。如函数调用指令生成R_ARM_CALL,绝对地址重定位指令生成R_ARM_ABS32类型。

加载器获取到上述信息就可以根据符号名称在内核符号表中查找符号位置,再根据重定位类型做对应的处理,填写到对应的目标内存位置。

如果一个模块的重定位过程中需要用到另外一个模块中的符号,我们说这两个模块间存在依赖关系,Linux 下moprobe和depmod命令可以用来检测和自动加载相互依赖的模块,SylixOS下目前还没有类似工具,需要用户手动解决。

1.3     导出符号

前面介绍了怎样引用内核符号,现在介绍本模块的符号如何被外部符号引用。与linux类似,如果想要一个模块的功能被外部模块和系统内核引用到,有两种办法,一种是将函数注册到内核或其它模块提供的接口中作为回调函数,另一种方法就是导出符号到内核符号表,这样其它模块重定位的时候便可以查找到该符号,这种方法更为灵活。

加载器为每个模块建立自己的符号表,这样在模块卸载的时候可以统一销毁。加载器同时建立便于查找的hash表。在linux内核源码定义符号时使用EXPORT_SYMBOL宏表示本符号是否被导出,SylixOS也不需要导出所有全局符号,所以使用了类似机制,SylixOS使用宏LW_SYMBOL_EXPORT标识符号为导出符号。该宏将要导出的符号抽取到一个特定的节中,加载器在导出符号时根据节名称过滤,只导出名称为“.__sylixos_kernel”的节中的符号。宏LW_SYMBOL_EXPORT的定义有利于我们理解这一原理,如下:

#define LW_SYMBOL_EXPORT  __attribute__((section(.__sylixos_kernel)))

1.4     调用初始化函数

在完成上面的步骤后,加载工作只剩下最后一步,就是初始化函数调用。SylixOS规定,固定使用module_init和module_exit函数名作为模块的初始化函数和卸载函数。也就是说这两个函数分别需要在模块加载完成后和模块卸载前执行。加载器在加载过程中的符号导出阶段会识别这两个函数名并将其记录在模块管理结构体中。只要有记录,调用便只是时机选择问题。

       需要说明的是,与linux不同,SylixOS内核模块加载器支持C++程序,C++的全局变量构造函数和析构函数也是需要在加载后和卸载前执行的。另外编译器还能识别一些特殊关键字或属性,这些关键字或属性规矩某些函数必须在加载完成后调用(如:__attribute__ (constructor))。所幸编译器为我们做了很多工作,它将这些函数的指针汇集成一张张表保存在以特定名称命名的节区中,我们只需要查找这些节区并记录节中的指针信息即可。以下是一些已经确定的节名称:

".preinit_array", ".init_array" ----编译器生成的初始化函数表。
".ctors"----C++构造函数表。
".fini_array"----编译器生成的销毁函数表。
".dtors"----C++析构函数表。
加载器在内部为每个模块建立初始化函数和析构函数表,在加载过程中将上述节的内容依次拷贝到内部表中,加载完成后依次调用表中的初始化函数,最后调用module_init函数。

1.5     总结

至此,内核模块加载介绍完毕,下一节将介绍应用程序模块加载,值得一提的是,应用程序模块加载在细节上和内核模块加载是一致的,这些内容我不会再重复介绍,如:重定位方法,符号表导出。我只会着重介绍不同的地方。


SylixOS官网:www.sylixos.com

SylixOS源码下载:git.sylixos.com

SylixOS百科:wiki.sylixos.com


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值