linux驱动开发(1)-内核模块

article/2025/8/4 5:50:20

内核模块

模块最大的好处是可以动态扩展应用程序的功能而无须重新编译链接生成新的应用程序镜像,在微软的Windows系统上动态链接库DLL(Dynamic Link Library),Linux系统上的共享库so(shared object)文件的形式都属于广义上的模块。内核模块可以在linux内核运行期间动态扩展内核功能而无须重新启动系统,更无须为这些新增的功能重新编译一个新的系统内核镜像。内核模块的这个特性为内核开发者提供了极大的便利,因为编译一个新内核并重新启动将花费大量的时间。这本质上是解耦和开闭原则的实践。insmod就是向系统动态加载内核模块的命令。

 insmod demodev.ko

内核模块文件形式

以内核模块形式存在的驱动程序的数据组织形式是ELF (Executable and Linkable Format),内核模块是一种普通的可重定位目标文件。用file命令查看demodev.ko文件,可以得到类似如下的输出:

 file demodev.ko
demodev.ko: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

ELF是Linux下非常重要的一种文件格式,常见的可执行程序都是以ELF的形式存在。在这里我们结合Linux源代码中定义的ELF相关数据结构(基于32位体系架构)​,给出ELF格式的一个结构图,如图所示​:

在这里插入图片描述

静态的ELF文件视图总体上可分为三大部分:头部的ELF header,中间的Section和尾部的Section header table。在Linux环境下读取ELF文件信息的工具—readelf。

ELFheader部分大小是52字节,位于文件头部。对于驱动模块文件而言,其中一些比较重要的数据成员如下:e_type表明文件类型,对于驱动模块,这个值是1,也就是说驱动模块是一个可定位的ELF文件(relocatable file)​。e_shoff表明Section header table部分在文件中的偏移量。e_shentsize表明Section header table部分中每一个entry的大小(以字节计)​。e_shnum表明Section header table中有多少个entry。因此,Section header table的大小便为e_shentsize×e_shnum个字节。e_shstrndx与Section header entry中的sh_name一起用来指明对应的section的name。

Section部分ELF文件的主体,位于文件视图中间部分的一个连续区域中。但是当模块被内核加载时,会根据各自属性被重新分配到新的内存区域(有些section也可能只是起辅助作用,因而在运行时并不占用实际的内存空间)​。

Section header table部分该部分位于文件视图的末尾,由若干个(具体个数由ELF header中的e_shnum变量指定) Section header entry组成,每个entry具有同样的数据结构类型。对于设备驱动模块而言,一些比较重要的数据成员如下:sh_addr这个值用来表示该entry所对应的section在内存中的实际地址。在静态的文件视图中,这个值为0,当模块被内核加载时,加载器会用该section在内存中的实际地址来改写sh_addr (如果section不占用内存空间,该值为0)​。sh_offset表明对应的section在文件视图中的偏移量。sh_size表明对应的section在文件视图中的大小(以字节计)​。类型为SHT_NOBITS的section例外,这种section在文件视图中不占有空间。sh_entsize主要用于由固定数量entry组成的表所构成的section,如符号表,此种情况下用来表示表中entry的大小。

EXPORT_SYMBOL的内核实现

源码中充斥着像EXPORT_SYMBOL这样的宏,它用来向外界导出一个符号。如果没有独立存在的内核模块,作为单一的Linux内核镜像,导出符号就失去了意义。对于静态编译链接而成的内核镜像而言,所有的符号引用都将在静态链接阶段完成。然而内核模块不可避免地要使用到内核提供的功能(以调用内核函数的形式发生)​,作为独立编译链接的内核模块,必须要解决这种静态链接无法完成的符号引用问题(在内核模块所在的ELF文件中,这种引用被称为“未解决的引用”​)​。处理“未解决引用”问题的本质是在模块加载期间找到当前“未解决的引用”符号在内存中的实际目标地址。内核和内核模块通过符号表的形式向外部世界导出符号的相关信息,这种导出符号的方式在代码层面以EXPORT_SYMBOL宏定义的形式存在。从全局来看,EXPORT_SYMBOL这类宏功能的完整实现需要经过三个部分来达成:EXPORT_SYMBOL宏定义部分,链接脚本链接器部分和使用导出符号部分。

<include/linux/module.h>
#define__EXPORT_SYMBOL(sym,sec)              \extern typeof(sym)sym;                 \__CRC_SYMBOL(sym,sec)                  \static const char__kstrtab_##sym[]          \__attribute__((section("__ksymtab_strings"),aligned(1)))\=MODULE_SYMBOL_PREFIX#sym;                   \static const struct kernel_symbol__ksymtab_##sym  \__used                             \__attribute__((section("__ksymtab"sec),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宏实际上定义了两个变量:

static const char *__kstrtab_my_exp_function = "my_exp_function";//tab是table的缩写
static const struct kernel_symbol __ksymtab_my_exp_function =
{ (unsigned long)& my_exp_function, __kstrtab_my_exp_function };

第一个变量是char型指针,用来表示导出的符号名称;第二个变量类型是struct kernel_symbol数据结构,用来表示一个内核符号的实例,struct kernel_symbol的定义为:

<include/linux/module.h>
struct kernel_symbol
{unsigned long value;const char*name;
};

value是该符号在内存中的地址,name是符号名。所以,用EXPORT_SYMBOL(my_exp_function)来导出符号“my_exp_function”​,实际上是要通过struct kernel_symbol的一个对象告诉外部世界关于这个符号的两点信息:符号名称和地址。由EXPORT_SYMBOL等宏导出的符号,与一般的变量定义并没有实质性的差异,唯一的不同点在于它们被放在了特定的section中。

上面的__kstrtab_my_exp_function(symbol的名称)会被放置在一个名为“__ksymtab_strings”的section中,__ksymtab_my_exp_function(symbol实例)会放置在一个名为“__ksymtab”的section中(对于EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_GPL_FUTURE而言,其struct kernel_symbol实例所在的section名称则分别为“__ksymtab_gpl”和“__ksymtab_gpl_future”​)​。对这些section的使用需要经过一个中间环节,即链接脚本与链接器部分。链接脚本告诉链接器把所有目标文件中的名为“__ksymtab”的section放置在最终内核(或者是内核模块)镜像文件的名为“__ksymtab”的section中(对于目标文件中的名为“__ksymtab_gpl”​、​“__ksymtab_gpl_future”​、​“__kcrctab”​、​“__kcrctab_gpl”和“__kcrctab_gpl_future”的section都同样处理)​,看看下面的这个具体的链接脚本的例子就很清楚了。

<arch/x86/kernel/vmlinux.lds>
__ksymtab : AT(ADDR(__ksymtab) - 0xC0000000)
{ __start___ksymtab = .; *(__ksymtab) __stop___ksymtab = .; }
__ksymtab_gpl : AT(ADDR(__ksymtab_gpl) - 0xC0000000)
{ __start___ksymtab_gpl = .; *(__ksymtab_gpl) __stop___ksymtab_gpl = .; }
__ksymtab_gpl_future : AT(ADDR(__ksymtab_gpl_future) - 0xC0000000)
{
__start___ksymtab_gpl_future = .;
*(__ksymtab_gpl_future) __stop___ksymtab_gpl_future = .;
}
__kcrctab : AT(ADDR(__kcrctab) - 0xC0000000)
{ __start___kcrctab = .; *(__kcrctab) __stop___kcrctab = .; }
__kcrctab_gpl : AT(ADDR(__kcrctab_gpl) - 0xC0000000)
{ __start___kcrctab_gpl = .; *(__kcrctab_gpl) __stop___kcrctab_gpl = .; }
__kcrctab_gpl_future : AT(ADDR(__kcrctab_gpl_future) - 0xC0000000)
{ __start___kcrctab_gpl_future = .; *(__kcrctab_gpl_future) __stop___kcrctab_gpl_future = .; }
__ksymtab_strings : AT(ADDR(__ksymtab_strings) - 0xC0000000)
{ *(__ksymtab_strings) }

这里之所以要把所有向外界导出的符号统一放到一个特殊的section里面,是为了在加载其他模块时用来处理那些“未解决的引用”符号。注意这里由链接脚本定义的几个变量__start___ksymtab、__stop___ksymtab、__start___ksymtab_gpl、__stop___ksymtab_gpl、__start___ksymtab_gpl_future、__stop___ksymtab_gpl_future,它们会在对内核或者是某一内核模块的导出符号表进行查找时用到。内核源码中为使用这些链接器产生的变量作了如下的声明:

<kernel/module.c>
extern const struct kernel_symbol __start___ksymtab[];
extern const struct kernel_symbol __stop___ksymtab[];
extern const struct kernel_symbol __start___ksymtab_gpl[];
extern const struct kernel_symbol __stop___ksymtab_gpl[];
extern const struct kernel_symbol __start___ksymtab_gpl_future[];
extern const struct kernel_symbol __stop___ksymtab_gpl_future[];
extern const unsigned long __start___kcrctab[];
extern const unsigned long __start___kcrctab_gpl[];
extern const unsigned long __start___kcrctab_gpl_future[];

内核代码便可以直接使用这些变量而不会引起编译错误。

还是我最喜欢的老环节,总结一下:
1.内核驱动是通过模块的形式加载到内核。在软件开发中有很多相似的过程,比如linux用户态的so,windows的ddl。
2.模块的后缀是.ko,文件格式是ELF。
3.模块向其他模块或者内核导出符号是通过EXPORT_SYMBOL宏实现。
4.EXPORT_SYMBOL背后是由内核源码/链接脚本实现。

再深入思考一点,我们能得到什么,或者说更加抽象的经验是什么:
1.解耦。将驱动功能与内核的基础功能分开。内核基础功能稳定性好,而驱动则是经常变化。
2.需要通过框架实现解耦后的部件间相互依赖的问题。

忽然在脑海里想到那么几个问题:
在我们的实际开发中会遇到驱动的bug导致整个内核panic重启的情况。这种问题是否可以得到优化呢?做到类似用户态的进程隔离。驱动死了内核可以不用收到影响。

我们总会考虑用户态进程的loading。驱动的loading是否有衡量方法。但是一般驱动都是做简单业务逻辑,不会涉及复杂的算法产生巨大的CPU开销。这个问题的必要性不是那么高。


http://www.hkcw.cn/article/QqqFtgIlxK.shtml

相关文章

【ISP算法精粹】动手实战:用 Python 实现 Bayer 图像的黑电平校正

在数字成像领域&#xff0c;图像信号处理器&#xff08;ISP&#xff09;如同幕后英雄&#xff0c;默默将传感器捕获的原始数据转化为精美的图像。而黑电平校正&#xff0c;作为ISP预处理流程中的关键一环&#xff0c;直接影响着最终图像的质量。今天&#xff0c;我们就通过Pyth…

【数据结构】顺序表和链表详解(上)

前言&#xff1a;上期我们介绍了算法的复杂度&#xff0c;知道的算法的重要性同时也了解到了评判一个算法的好与坏就去看他的复杂度(主要看时间复杂度)&#xff0c;这一期我们就从顺序表和链表开始讲起。 文章目录 一&#xff0c;顺序表1&#xff0c;线性表2&#xff0c;顺序表…

【笔记】在 MSYS2(MINGW64)中安装 Python 工具链的记录

#工作记录 &#x1f4cc; 安装背景 操作系统&#xff1a;MSYS2 MINGW64当前时间&#xff1a;2025年6月1日Python 版本&#xff1a;3.12&#xff08;默认通过 pacman 安装&#xff09;目标工具链&#xff1a; pipxnumpypipsetuptoolswheel &#x1f6e0;️ 安装过程与结果记录…

sqli-labs靶场32-37关(宽字节注入)

目录 前言 less32&#xff08;宽字节注入&#xff09; less33&#xff08;宽字节注入&#xff09; less34&#xff08;POST型宽字节注入&#xff09; less35&#xff08;数字型闭合宽字节&#xff09; less36&#xff08;宽字节注入&#xff09; less37&#xff08;POST…

SRE 基础知识:在站点可靠性工程中可以期待什么

作者&#xff1a;来自 Elastic Elastic Observability Team 在过去的 20 年里&#xff0c;大多数领先企业已经采用云计算和分布式系统来开发它们的应用程序。一个意想不到的后果是&#xff1a;传统的 IT 运维&#xff08; IT operations - ITOps &#xff09;常常难以应对日益增…

day16 leetcode-hot100-31(链表10)

25. K 个一组翻转链表 - 力扣&#xff08;LeetCode&#xff09; 1.模拟法 思路 将这个过程拆解为两个步骤&#xff0c;第一步将单分组的节点反转&#xff0c;第二步将反转后的链表加入原链表。 针对节点反转很容易&#xff0c;参考之前的206. 反转链表 - 力扣&#xff08;Le…

黑马Java面试笔记之Redis篇(使用场景)

1.面试题 我看你做的项目中&#xff0c;都用到了redis&#xff0c;你在最近的项目中那些场景使用了redis呢 2.提问的底层逻辑 面试官提问你这个问题一是想验证你的项目场景的真实性&#xff0c;二是为了作为深入发问的切入点 3.延伸出来的知识点 3.1 缓存 缓存三兄弟&#x…

PyTorch -TensorBoard的使用 (一)

设置环境 新建python文件 .py 安装Tensorboard 在终端进行安装 显示安装成功 两个logs&#xff0c;出现这种情况怎么解决 所有的logs文件删掉delete&#xff0c;重新运行 add_image 不满足要求 Opencv-numpy 安装Opencv add_image 用法示例 &#xff08;500&#xff0c;375&am…

解决Ubuntu20.04上Qt串口通信 QSerialPort 打开失败的问题

运行Qt串口通信 open(QIODevice::ReadWrite) 时&#xff0c;总是失败。 1、打印失败原因 QString QSerialHelper::openSerail() {if(this->open(QIODevice::ReadWrite) true){return this->portName();}else{return "打开失败";//return this->errorStri…

[yolov11改进系列]基于yolov11引入迭代注意力特征融合iAFF的python源码+训练源码

【iAFF介绍】 1. IAFF&#xff08;迭代注意力特征融合&#xff09; iAFF通过引入多尺度通道注意力模块和迭代融合&#xff0c;更好的整合不同尺度和语义不一致的特征&#xff0c;有效解决特征融合问题&#xff0c;提高目标检测的精度。 特征融合&#xff0c;即不同层或分支的…

springboot-响应接收与ioc容器控制反转、Di依赖注入

1.想将服务器中的数据返回给客户端&#xff0c;需要在controller类上加注解&#xff1a;ResponseBody; 这个注解其实在前面已经使用过&#xff0c;RestController其实就包含两个注解&#xff1a; Controller ResponseBody 返回值如果是实体对象/集合&#xff0c;将会转换为j…

idea中springboot2.7(由于步入另一个线程,已跳过 xxx 处的断点)

idea springboot2.7 debug 问题 springboot 2.7 debug 模式时引入 spring-boot-devtools 卡在代码中不往下执行&#xff0c;提示&#xff1a;由于步入另一个线程&#xff0c;已跳过 xxx 处的断点。 原因 springboot 2.7 引入 spring-boot-devtools <!-- debug时不推荐开…

ROS应用之如何配置RTOS满足机器人系统中的实时要求

如何配置RTOS以满足机器人系统中的实时要求 前言 实时操作系统&#xff08;RTOS&#xff09;在机器人系统中的应用至关重要&#xff0c;尤其在需要对环境变化做出快速反应的高精度控制系统中。ROS2作为开源机器人操作系统&#xff0c;为机器人提供了强大的框架和工具链&#x…

03 APP 自动化-定位元素工具元素定位

文章目录 一、Appium常用元素定位工具1、U IAutomator View Android SDK 自带的定位工具2、Appium Desktop Inspector3、Weditor安装&#xff1a;Weditor工具的使用 4、uiautodev通过定位工具获取app页面元素有哪些属性 二、app 元素定位方法 一、Appium常用元素定位工具 1、U…

数学分析——一致性(均匀性)和收敛

目录 1. 连续函数 1.1 连续函数的定义 1.2 连续函数的性质 1.2.1 性质一 1.2.2 性质二 1.2.3 性质三 1.2.4 性质四 2. 一致连续函数 2.1 一致连续函数的定义 2.2 一致连续性定理(小间距定理)(一致连续函数的另一种定义) 2.3 一致连续性判定法 2.4 连…

并发执行问题 (上)

S3和S4 没法区别 i存在 父进程数据区 子进程存在数据区 所以返回值不一样 通过返回值运行不一样的代码 原来的进程镜像作废,运行的执行新的elf文件

一周学会Pandas2之Python数据处理与分析-数据重塑与透视-pivot_table() - 透视表 (长 -> 宽,支持聚合)

锋哥原创的Pandas2 Python数据处理与分析 视频教程&#xff1a; 2025版 Pandas2 Python数据处理与分析 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili pivot_table() 是 pandas 中最强大的数据透视工具&#xff0c;它不仅能重塑数据&#xff0c;还能进行复杂的数据聚合…

郑钦文抢七险胜萨姆索诺娃 首次闯进法网八强

6月1日,在2025年法国网球公开赛女单第四轮比赛中,赛会8号种子、中国选手郑钦文以2:1战胜俄罗斯选手萨姆索诺娃,首次闯进法网女单八强。这场比赛耗时2小时47分钟,双方在场上展开了激烈的争夺。首盘比赛双方陷入苦战,郑钦文通过更深的落点限制对手进攻,并且在多拍回合中展现…

robot_lab学习笔记【MDP综述】

文章目录 整体介绍第一部分第二部分第三部分总结 整体介绍 在robot_lab中的mdp文件夹下面包含6个文件&#xff1a;[init.py , commands.py , curiculums.py, events.py , observations.py , rewards.py ] 对每个部分的详细讲解在总结中会指向子链接 init.py文件的代码如下 …

Leetcode 2093. 前往目标城市的最小费用

1.题目基本信息 1.1.题目描述 一组公路连接 n 个城市&#xff0c;城市编号为从 0 到 n - 1 。 输入包含一个二维数组 highways &#xff0c;其中 highways[i] [city1i, city2i, tolli] 表示有一条连接城市 city1i 和 city2i 的双向公路&#xff0c;允许汽车缴纳值为 tolli 的…