Linux设备驱动-内核模块介绍
Linux内核模块,可以在系统运行期间动态扩展系统功能而无须重启系统,更无须为这些新增的功能重新编译一个新的系统内核映像。内核模块的这个特性为内核开发者开发验证新的功能提供了极大的便利。
内核模块的文件格式
以内核模块形式存在的驱动程序,比如 hello.ko,其在文件的数据组织形式上是 ELF(Executable and Linkable Format)格式。具体来说,内核模块是一种普通的可重定位的目标文件。用 file 命令查看 hello.ko 文件,可得到如下输出:
$ file hello.ko hello.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
ELF 是 Linux 下非常重要的一种文件格式,常见的可执行程序都是以 ELF 的形式存在。
结合 Linux 源代码中定义的 ELF相关数据结构(基于 32 位体系结构),ELF 格式的一个比较详细的结构图:
静态 ELF 文件视图总体上可分为三大部分:头部的 ELF header,中间的 Section 和 尾部的 Section Header table。
- ELF header 部分
大小是 52 字节,位于文件头部。对于驱动模块文件来说,一些比较重要的数据成员在下方进行了注释:
typedef struct elf32_hdr { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; /* 文件类型,对于驱动模块,其值为 1 */ Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ Elf32_Off e_phoff; Elf32_Off e_shoff; /* 表明 Section Header table 部分在文件中的偏移量 */ Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; /* 表明 Section Header table 部分每一个 entry 的大小(字节) */ Elf32_Half e_shnum; /* 表明 Section Header table 中有多少个 entry */ Elf32_Half e_shstrndx; /* 与 Section Header table 中的 sh_name 用来指明对应的 section 的 name */ } Elf32_Ehdr;
- Section 部分
ELF 文件的主体,位于文件视图中间部分的一个连续区域中。当模块被内核加载时,会根据各自属性被重新分配到新的内存区域。
- Section Header table 部分
该部分位于文件视图的末尾,由若干个 Section header entry 组成,每个 entry 具有相同的数据结构类型。对于驱动模块文件来说,一些比较重要的数据成员在下方进行了注释:
typedef struct elf32_shdr { Elf32_Word sh_name; Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; /* 表示该 entry 所对应的 section 在内存中的实际地址 */ Elf32_Off sh_offset; /* 表明 对应的 section 在文件视图中的偏移量 */ Elf32_Word sh_size; /* 表明对应的 setion 在文件视图中的大小(字节) */ Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; /* 表示 entry 的大小 */ } Elf32_Shdr;
EXPORT_SYMBOL 介绍
Linux 内核源码中充斥着像 EXPORT_SYMBOL 这样的宏,在我们自己的设备驱动程序中也经常会发现它的身影。大部分时间里,我们只知道它用来向外界导出一个符号,更不用说去仔细探究其背后的实现原理了。这些不起眼的宏却有大用场,如果没有他们,我们的驱动程序甚至连 printk 这样常见的内核函数都不能用。
符号导出的宏定义有以下几种:
- EXPORT_SYMBOL
- EXPORT_SYMBOL_GPL
- EXPORT_SYMBOL_GPL_FUTURE
模块在加载过程中会使用到宏定义导出符号的内核机制,导出符号这一特性在 Linux 系统中对模块的存在具有重要的意义。
对于静态编译链接而成的内核映像而言,所有的符号引用都将在静态链接阶段完成。内核模块的出现,让事情发生了变化:内核模块不可避免地要使用到内核提供地基础设施(以调用内核函数地形式发生),作为独立编译链接的内核模块,必须要解决这种静态链接无法完成地符号引用问题(“未解决的引用”)。
内核和内核模块通过符号表的形式向外部世界导出符号的相关信息,在代码层面则以 EXPORT_SYSMBOL 宏定义的形式存在。这类宏功能的完整实现需要经过三个部分来达成:
- EXPORT_SYMBOL 宏定义部分
- 链接脚本链接器部分
- 使用导出符号部分
宏定义的内核源码如下:
#define __EXPORT_SYMBOL(sym, sec) \ extern typeof(sym) sym; \ __CRC_SYMBOL(sym, sec) \ static const char __kstrtab_##sym[] \ __attribute__((section("__ksymtab_strings"), aligned(1))) \ = VMLINUX_SYMBOL_STR(sym); \ extern const struct kernel_symbol __ksymtab_##sym; \ __visible const struct kernel_symbol __ksymtab_##sym \ __used \ __attribute__((section("___ksymtab" sec "+" #sym), unused)) \ = { (unsigned long)&sym, __kstrtab_##sym } #define EXPORT_SYMBOL(sym) \ __EXPORT_SYMBOL(sym, "") #define EXPORT_SYMBOL_GPL(sym) \ __EXPORT_SYMBOL(sym, "_gpl") #define EXPORT_SYMBOL_GPL_FUTURE(sym) \ __EXPORT_SYMBOL(sym, "_gpl_future")
由 EXPORT_SYMBOL 等宏导出的符号,与一般的变量定义并没有实质性的差异,唯一不同点在于他们被放在了特定的 section 中。
对这些 section 的使用需要经过一个中间环节,即链接脚本与链接器部分。链接脚本告诉链接器,把所有目标文件中的名为 "__ksymtab
" 的 section, 放置在最终内核(或是内核模块)映像文件的名为 “__ksymtab” 的section中(其他情况类似)。
把所有导出的符号统一放在一个特殊的 section 里,为了在加载其他模块时处理 “未解决的引用” 符号。
小结
内核模块在文件格式上是一种可重定位的 ELF 文件,由 Linux 系统中的内核模块加载器负责加载和卸载。
内核和内核模块通过 EXPORT_SYMBOL 宏向外导出符号,可以被其他模块所使用,它们被放在一个特殊的 section 中。
内核和内核模块拥有各自的 section 来保存导出符号的信息。
———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
关注微信公众号【一起学嵌入式】,一起学习,一起成长
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)