
笔者进行了linux驱动的学习对于module_init函数比较好奇这个函数作为linux内核的入口linux是如何拿到入口参数linux模块是如何加载到内核的等等现在总结相关的知识点1. module_init的源码分析源文件位于include/linux/module.h#ifndefMODULE/** * module_init() - driver initialization entry point * x: function to be run at kernel boot time or module insertion * * module_init() will either be called during do_initcalls() (if * builtin) or at module insertion time (if a module). There can only * be one per module. */#definemodule_init(x)__initcall(x);可以发现由MODULE控制是否开启这个函数 谁来控制MODULE?可以发现在顶层文件Makefile中由KBUILD_CFLAGS_KERNEL和KBUILD_CFLAGS_MODULE这两个宏来控制在make menuconfig中把某个驱动选为[*]表示编译进入内核Kbuild 系统会使用KBUILD_CFLAGS_KERNEL这个变量。它这里是空的表示不往编译器传递特殊的宏。对应着KBUILD_CFLAGS_KERNEL:在配置中使用[M]表示编译成模块Kbuild 会在编译命令中加上-DMODULE, 等同于在源码中加入了MODULE宏。对应KBUILD_CFLAGS_MODULE:-DMODULEKBUILD_CFLAGS_KERNEL : KBUILD_CFLAGS_MODULE : -DMODULE所以说module_init有两种对应的源码第一种module_init(x) __initcall(x);第二种如下定义init_module函数并给他起名字为initfn即入口函数eg:module_init(hello_world)中,init_module hello_world#definemodule_init(initfn)\staticinlineinitcall_t__inittest(void)\{returninitfn;}\intinit_module(void)__attribute__((alias(#initfn)));#endif2. 讲解编译进内核2.1 module_init的宏这一部分比较麻烦先给结论module_init(helloworld)最终的作用是创建一个指向 helloworld 的函数指针并且把这个指针放到内核的.initcall6.init 段里。解读前提知识##代表强制连接#表示对这个变量替换后 用双引号引起来解读这个流程上图为helloworld这个函数的传递流程,我们来解析这个最复杂的最后一步先给个总结 最后的宏定义的是一个函数指针变量#define __define_initcall(fn, id) \static initcall_t __initcall_##fn##id __used \attribute((section(.initcall #id .init))) fn;initcall_t: 预定义的函数指针类型它本质是一个函数指针即一个指针只不过指代的对象是函数typedef int (*initcall_t)(void);__initcall_##fn##id 是一个变量名称 强制连接这么多是为什么防止重名而且包含变量的特征__used: 告诉内核不要把它优化掉编译器非常聪明它在把你写的 C 语言翻译成机器码时会自动帮你“搞卫生”。 如果它发现你定义了一个变量比如刚才的__initcall_my_driver_init6但是在后面的几十万行 C 代码中从来没有任何地方调用过、或者读取过这个变量编译器就会认为“这是一个无用的废变量Dead Code。”为了节省最终生成的内核镜像vmlinux的体积编译器在高级优化级别如-O2下会自动把这个变量无情地删掉。__attribute__((section(.initcall6.init))): 是一个指令将函数指针变量放到 ELF可执行文件或内核镜像 文件的.initcall6.init段中section段编译器默认将变量放到.data段之中现在内核希望放在 ELF 文件里建一个叫做.initcall6.init的位置6: 为默认驱动级别普通驱动默认 一般0-7helloworld: 为用户定义的函数除了module_init这个宏还有其他的宏只是数字优先级不同 在linux/init.h 中 本质都__define_initcall(fn, 0-7);数字越小优先级越高加载越早2.2 vmlinux.lds.h上面2.1 讲解了编译进内核的这个宏module_init现在提出疑问前面把函数指针那个变量就是函数指针存放的是helloworld放到了.initcall6.init段但段的顺序、起始 / 结束地址由谁定义先给出解答内核链接脚本include/asm-generic/vmlinux.lds.h告诉把内核代码段、数据放在内存的哪个位置按什么顺序放置下面是源码#defineINIT_CALLS_LEVEL(level)\VMLINUX_SYMBOL(__initcall##level##_start).;\KEEP(*(.initcall##level##.init))\KEEP(*(.initcall##level##s.init))\#defineINIT_CALLS\VMLINUX_SYMBOL(__initcall_start).;\KEEP(*(.initcallearly.init))\INIT_CALLS_LEVEL(0)\INIT_CALLS_LEVEL(1)\INIT_CALLS_LEVEL(2)\INIT_CALLS_LEVEL(3)\INIT_CALLS_LEVEL(4)\INIT_CALLS_LEVEL(5)\INIT_CALLS_LEVEL(rootfs)\INIT_CALLS_LEVEL(6)\INIT_CALLS_LEVEL(7)\VMLINUX_SYMBOL(__initcall_end).;INIT_CALLS_LEVEL (level) 定义单个优先级的段布局VMLINUX_SYMBOL 内核链接脚本中使用的一个宏其主要作用是抹平不同硬件架构CPU 体系结构在 C 语言符号命名上的差异。VMLINUX_SYMBOL(__initcall##level##_start).;定义该级别 initcall 段的起始符号例如 __initcall0_start并将其地址设为当前位置 (.)__initcall[level]_start 该优先级初始化调用函数段的起始位置。KEEP(*(.initcall##level##.init)): 将所有被编译到 .initcall{level}.init 段的内容通常是普通初始化函数指针放在这里__initcall[level]s_start同上但是是静态初始化KEEP(*(.initcall##level##s.init)): 将所有被编译到 .initcall{level}s.init 段的内容通常是同步初始化函数指针带 ‘s’ 结尾放在这里INIT_CALLS按优先级顺序整合所有段__initcall_start符号表示初始化调用函数段的起始位置。KEEP命令保留所有.initcallearly.init段中的内容。按照 0→7的优先级顺序调用INIT_CALLS_LEVEL定义所有段将相应优先级的初始化调用函数放置在链接器脚本中的正确位置。定义 _initcall_end符号表示初始化调用函数段的结束位置。上面的展开之后VMLINUX_SYMBOL(__initcall_start).;KEEP(*(.initcallearly.init))VMLINUX_SYMBOL(__initcall0_start).;KEEP(*(.initcall0.init))KEEP(*(.initcall0s.init))....VMLINUX_SYMBOL(__initcall7_start).;KEEP(*(.initcall7.init))KEEP(*(.initcall7s.init))VMLINUX_SYMBOL(__initcall_end).;_initcallx_start (x0,1, 2…)等以_start结尾的相关变量记录了.initcallx.init(x0,1, 2….等段的首地址综上所述根据2.1 我们写的helloworld函数存放在__initcall_helloworld_init6变量里面然后这个变量又存放在下面这个集合中(KEEP(*(.initcall6.init)))// ...INIT_CALLS_LEVEL(6)/* 展开后对应*//* VMLINUX_SYMBOL(__initcall6_start) .; *//* KEEP(*(.initcall6.init)) -- 你的 helloworld_init 函数指针就躺在这个集合里 *//* KEEP(*(.initcall6s.init)) */// ...在init/main.c中通过extern关键字进行引用这些变量将这些首地址放置在数组 initcall_levels中,源码如下initcall_levels静态指针数组/* * 1. 声明外部符号 (extern) * 这些符号并没有在任何 C 语言文件中被定义。 * 它们是由链接器Linker在最后打包内核镜像时根据 vmlinux.lds 链接脚本 * 自动计算出的内存地址。 * initcall_t 是一个函数指针类型通常定义为: typedef int (*initcall_t)(void); */externinitcall_t__initcall_start[];externinitcall_t__initcall0_start[];externinitcall_t__initcall1_start[];externinitcall_t__initcall2_start[];externinitcall_t__initcall3_start[];externinitcall_t__initcall4_start[];externinitcall_t__initcall5_start[];externinitcall_t__initcall6_start[];externinitcall_t__initcall7_start[];externinitcall_t__initcall_end[];/* * 2. 组装成数组 * 将散落的各个级别的起始地址集中管理形成一个指针数组。 * 作用使得内核可以通过一个简单的 for 循环按数组下标 0 到 7依次执行不同优先级的初始化。 * * __initdata这是一个特殊的宏指示编译器将此数组存放在内核的“初始化数据段”.init.data。 * 内核启动完毕后这块“一次性”使用的内存会被彻底释放回收节约物理内存。 */staticinitcall_t*initcall_levels[]__initdata{__initcall0_start,// Level 0: 纯粹的早期初始化 (pure_initcall)__initcall1_start,// Level 1: 核心初始化 (core_initcall)__initcall2_start,// Level 2: 核心后初始化 (postcore_initcall)__initcall3_start,// Level 3: 架构相关初始化 (arch_initcall)__initcall4_start,// Level 4: 子系统初始化 (subsys_initcall)__initcall5_start,// Level 5: 文件系统初始化 (fs_initcall)__initcall6_start,// Level 6: 设备驱动初始化 (device_initcall)__initcall7_start,// Level 7: 延迟执行的初始化 (late_initcall)__initcall_end,// 边界符: 用于配合 Level 7 计算区间告诉 for 循环何时停止};里面存不同优先级的初始化调用函数段的起始地址数组在do_one_initcall()函数中执行2.3 内核启动流程上面提到了do_one_ initcall()函数现在来解析整个内核的启动流程前面所有步骤都是准备工作宏定义→放段→定义地址→整合成数组最终系统启动时的函数调用链会执行这些函数指针这是驱动入口函数自动运行的最后一步核心调用链如下start_kernel在Linux-4.9.88/init/main.c下核心调用链如下start_kernel是内核启动的总入口所有内核初始化都从这里开始rest_init会创建内核线程kernel_init是内核初始化主线程后续的初始化工作都在这个线程中执行。关键函数do_initcalls ()初始化的总循环入口核心是按优先级遍历 initcall_levels 数组调用对应级别的初始化函数;数字小先执行do_initcalls会执行7次do_initcall_level/* * 总调度器 * 作用从 Level 0 到 Level 7 依次遍历并执行所有的初始化函数。 */staticvoid__initdo_initcalls(void){intlevel;/* * ARRAY_SIZE(initcall_levels) - 1 是因为数组最后一个元素是 __initcall_end作为边界 * 它本身不是一个有效的层级。所以这里的 level 会从 0 循环到 7。 */for(level0;levelARRAY_SIZE(initcall_levels)-1;level)do_initcall_level(level);}do_initcall_level (level)执行单个优先级的所有入口函数核心是遍历当前优先级段的所有 函数指针;这里就是将2.2 中的initcall_levels存储地址全部遍历一遍传到这里进行解析逐个传给do_one_initcall执行;/* * 单层级遍历器 * 作用处理特定的优先级比如 level 6: device_initcall * 解析该层级特有的内核启动参数并依次调用该层级内的所有函数。 */staticvoid__initdo_initcall_level(intlevel){initcall_t*fn;/* 1. 处理属于该特定级别的内核命令行参数比如某些驱动特有的启动参数 */strcpy(initcall_command_line,saved_command_line);parse_args(initcall_level_names[level],initcall_command_line,__start___param,__stop___param-__start___param,level,level,NULL,repair_env_string);/* * 2. 核心循环执行该层级的所有函数 * initcall_levels[level] 是当前层级的起始地址如 __initcall6_start * initcall_levels[level1] 是下一层级的起始地址如 __initcall7_start正好作为本层级的结束边界。 * fn 每次将指针移动一个 initcall_t 的大小逐个函数向后遍历。 */for(fninitcall_levels[level];fninitcall_levels[level1];fn)do_one_initcall(*fn);/* 解引用函数指针 fn传递给真正执行的函数 */}do_one_initcall (fn)执行单个驱动入口函数(即最终执行驱动入口函数的函数):比如helloworld会在这里被调用ret fn ();同时它还做了内核安全检查/* * 底层执行与“质检员” * 作用真正调用驱动/子系统的初始化函数并对其执行后的“系统状态”进行严格的安全审查。 * 防止有些写得糟糕的驱动把内核搞崩溃。 */int__init_or_moduledo_one_initcall(initcall_tfn){intcountpreempt_count();/* 记录执行前的“抢占计数器”状态 */intret;charmsgbuf[64];/* 如果这个驱动被加入了黑名单通过内核启动参数 initcall_blacklist则跳过不执行 */if(initcall_blacklisted(fn))return-EPERM;/* 真正执行您写的那些 module_init / device_initcall 函数 */if(initcall_debug)retdo_one_initcall_debug(fn);/* 如果开启了 initcall 调试会打印执行耗时等信息 */elseretfn();/* 正常情况直接执行函数*/msgbuf[0]0;/* * 下面是极其重要的内核级安全检查 * 防止粗心的驱动开发者在初始化函数里申请了锁却忘了释放或者关了中断忘了开。 *//* 检查 1内核抢占计数器是否失衡 * 如果驱动里用了 spin_lock 却没有 spin_unlock会导致抢占计数器发生变化。 */if(preempt_count()!count){sprintf(msgbuf,preemption imbalance );preempt_count_set(count);/* 强制恢复抢占计数器尝试擦屁股 */}/* 检查 2是否意外关闭了本地中断 * 如果驱动里 local_irq_disable() 后忘了 enable会导致 CPU 无法响应外部硬件。 */if(irqs_disabled()){strlcat(msgbuf,disabled interrupts ,sizeof(msgbuf));local_irq_enable();/* 强制重新开启中断擦屁股 */}/* 如果发现了上述糟糕行为打印出带有堆栈和函数名 (%pF) 的严厉警告公开处刑这个驱动 */WARN(msgbuf[0],initcall %pF returned with %s\n,fn,msgbuf);/* 收集一些系统运行过程中的不确定性因素用于给内核的随机数生成器熵池提供种子 */add_latent_entropy();returnret;}测试现在我们在这里加一个printk打印输出看一下内容发现在uboot阶段就已经打印了很多模块运行的信息这个是韦东山板子自带的内核模块编译进内核的dump_stack():执行者是PID: 1 Comm: swapper/0。这说明此时处于内核刚启动阶段是 1 号进程init进程的前身在内核态执行初始化。调用栈从下往上看ret_from_fork-kernel_init-kernel_init_freeable。这就是内核主干启动的经典路径。然后它调到了你修改的do_one_initcall。这证明系统正在遍历initcall_levels数组也就是 Level 0 到 7。[4.234795]CPU:0PID:1Comm:swapper/0Not tainted4.9.88#6[4.234827][80112a74](unwind_backtrace)from[8010dc6c](show_stack0x20/0x24)[4.234847][8010dc6c](show_stack)from[804699a4](dump_stack0x80/0x94)[4.234868][804699a4](dump_stack)from[80101de8](do_one_initcall0x178/0x184)[4.234890][80101de8](do_one_initcall)from[81100e9c](kernel_init_freeable0x16c/0x204)[4.234913][81100e9c](kernel_init_freeable)from[80b803c8](kernel_init0x18/0x120)[4.234937][80b803c8](kernel_init)from[80109390](ret_from_fork0x14/0x24)[4.235022]can-3v3:disabling...[4.248249]#0:wm8960-audio总结宏定义预处理编译进内核时module_init(helloworld)层层展开生成指向helloworld的函数指针__initcall_helloworld6并通过section属性把它放到内核的.initcall6.init段链接脚本布局链接器根据vmlinux.lds.h把.initcall6.init段按优先级 6 的顺序排列在内存中并定义段起始地址符号__initcall6_start地址数组整合内核在init/main.c中通过extern声明__initcall6_start并把它放到initcall_levels[6]数组中内核启动执行系统启动时内核从start_kernel开始通过调用链走到do_initcalls()循环遍历initcall_levels数组逐段执行当遍历到 level6 时do_initcall_level(6)会遍历.initcall6.init段的所有函数指针取出__initcall_helloworld6并传给do_one_initcall驱动执行do_one_initcall调用helloworld()驱动入口函数执行完成初始化。现在总算进入了我们写的helloworld函数了3. 讲解编译成模块前提需要讲解一下经常使用的insmod这个命令3.1 insmod:命令本质上是一个可执行程序; (insmod 的源码位于 busybox-1.34.1/modutils/insmod.c )源码/* * 这是一个来自 BusyBox 的轻量级 insmod 实现 (insmod_main)。 * 它是用户空间中真正接收 insmod hello.ko 命令的入口。 */intinsmod_main(intargc UNUSED_PARAM,char**argv){char*filename;intrc;/* * 1. 解析旧版内核的选项 (兼容性处理) * 如果开启了 2.4 版本内核模块支持的宏则解析命令行选项 (如 -f, -m 等)。 * 然后调整 argv 指针跳过这些选项指向真正要加载的文件名。 */IF_FEATURE_2_4_MODULES(getopt32(argv,INSMOD_OPTS INSMOD_ARGS);argvoptind-1;);/* * 2. 获取要加载的模块文件名 (例如: helloworld.ko) */filename*argv;/* 如果用户什么都没输入 (只敲了 insmod)则打印帮助文档并退出 */if(!filename)bb_show_usage();/* * 3. 核心调用将模块塞入内核 * parse_cmdline_module_options(argv, 0) 的作用是提取模块的参数 * 例如 insmod my_driver.ko debug_mode1 里的 debug_mode1。 * bb_init_module 是核心封装函数它会在底层发起真正的 init_module/finit_module 系统调用。 */rcbb_init_module(filename,parse_cmdline_module_options(argv,/*quote_spaces:*/0));/* * 4. 错误处理 * 如果返回非 0 值说明系统调用失败例如文件不存在、内核版本不匹配、符号未解析等。 * 打印出错的文件名以及对应的错误原因文本 (moderror 将错误码转换为字符串)。 */if(rc)bb_error_msg(cant insert %s: %s,filename,moderror(rc));returnrc;}insmod_main核心拿到用户输入的.ko 文件名调用真正的加载函数 bb_init_modulebb_init_module 提供两种加载.ko 模块的方式finit_module实验证明一般调用这个方法用open打开.ko文件, 调用finit_module系统调用,init_module先读文件到内存, 调用init_module系统调用上述两个函数本质是调用syscall系统调用函数3.2 insmod helloworld:关于系统调用的知识这里不在赘述这里只需要知道insmod hello world 本质上执行linux应用层的code执行的函数采用系统调用方式系统调用时,内核会执行 SYSCALL_DEFINE3 宏定义的函数这些函数最终都会调用 load_module 函数这个SYSCALL_DEFINE3 宏在kernel/module.c中/* 处理 init_module 系统调用 (接收内存指针) */SYSCALL_DEFINE3(init_module,void__user*,umod,unsignedlong,len,constchar__user*,uargs){// ... 解析参数 ...}/* 处理 finit_module 系统调用 (接收文件描述符) */SYSCALL_DEFINE3(finit_module,int,fd,constchar__user*,uargs,int,flags){// ... 解析参数 ...}load_module - do_init_module - do_one_initcalldo_one_initcall(mod-init)来执行驱动程序的入口函数但是最关键的问题mod-init到底是怎么和我们自己写的驱动入口函数比如helloworld 为开发者自己定义的初始化函数绑定的module_init宏的第二种写法:给自定义入口函数(eghelloworld)起别名init_module// 我们写的module_init(initfn) 其中initfn是自定义入口如helloworld #define module_init(initfn) static inline initcall_t __maybe_unused __inittest(void){ return initfn;} // 核心行定义init_module函数作为initfn的别名 (initfn就是helloworld) int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));__attribute__((alias(xxx))作用是给函数起别名编译 ko 时自动生成.mod.c文件创建__this_module结构体:当我们执行make编译 ko 模块时内核的 Kbuild 编译系统会自动生成一个以.mod.c为后缀的文件比如helloworld.mod.c生成__this_module结构体内核加载 ko 时首先会找到这个结构体.init init_module 把结构体的init成员函数指针赋值为init_moduleinit_module又是我们自定义入口函数的别名总结自定义入口helloworld→init_module→__this_module.initdo_one_initcall(mod-init)来执行驱动程序的入口函数mod-init __this_module.init init_module helloworld 即实现了参数的传递// 1- 自动生成的__this_module结构体类型是struct module内核定义的模块描述结构体 struct module __this_module // 2- 把这个结构体放到内核指定的内存段中内核加载 ko 文件时会专门解析这个段快速找到__this_module不会和其他代码 / 数据混淆。 __attribute__((section(.gnu.linkonce.this_module))){ .name KBUILD_MODNAME, // 模块名即KO文件名如helloworld .init init_module, // 函数指针指向上面的init_module即自定义入口的别名 #ifdef CONFIG_MODULE_UNLOAD // 内核支持模块卸载时才会有exit成员 .exit cleanup_module, // 函数指针指向出口的别名cleanup_module #endif .arch MODULE_ARCH_INIT,// 架构相关初始化如ARM64 }