###1. linux 内核模块入口

在编写linux内核模块时(无论是built-in, 还是built module), 通常使用“module_init()”宏来指定模块的入口函数,使用“module_exit()”宏来指定模块的出口函数, 例如

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
		printk(KERN_INFO "sven : init\n");
		return 0;
}

static void __exit hello_exit(void)
{
		printk(KERN_INFO "sven : exit\n");
}

module_init(hello_init);
module_exit(hello_exit);

使用module_init(hello_init)指明了模块的入口函数为 hello_init(), (函数前的_init修饰符用于指定将函数的代码放置在 “.init.text” section中, 注意, 只是存放在对应的中间文件的“.init.text” section中, 而不是最终的vmlinux的“.init.text” section中,因为在链接时, 可能会合并一些section, 在调用完后,将释放该section以释放内存), 使用module_exit()宏指定模块的出口函数为hello_exit()

###2. built-in时内核模块更高级别的入口函数

built module 的内核模块只能够使用 module_init()宏来声明入口函数, 在insmod模块时, 入口函数被调用, 但是, 对于built-in的模块, 某一些功能模块需要更早的加载, 以便给后续的模块提供支持, 例如文件系统, 网络协议栈等, 对于built-in的内核模块, linux kernel 提供了不同的入口等级, 在”linux/init.h” 中有

#define early_initcall(fn)		__define_initcall(fn, early)

#define pure_initcall(fn)		__define_initcall(fn, 0)

#define core_initcall(fn)		__define_initcall(fn, 1)
#define core_initcall_sync(fn)	__define_initcall(fn, 1s)
#define postcore_initcall(fn)	__define_initcall(fn, 2)
#define postcore_initcall_sync(fn)	__define_initcall(fn, 2s)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define arch_initcall_sync(fn)	__define_initcall(fn, 3s)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define subsys_initcall_sync(fn)	__define_initcall(fn, 4s)
#define fs_initcall(fn)		__define_initcall(fn, 5)
#define fs_initcall_sync(fn)	__define_initcall(fn, 5s)
#define rootfs_initcall(fn)		__define_initcall(fn, rootfs)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define device_initcall_sync(fn)	__define_initcall(fn, 6s)
#define late_initcall(fn)		__define_initcall(fn, 7)
#define late_initcall_sync(fn)	__define_initcall(fn, 7s)

按照从上往下的顺序,越往下, 调用的时间越靠后, 注意其中的rootfs_initcall(), 用于初始化initramfs等, 为初始化根文件系统做准备, 对于module_init(), 有

#define module_init(x)	__initcall(x);
#define __initcall(fn) device_initcall(fn)

可见, module_init 的级别为6

另外还有两个声明入口的宏, 这两种入口的调用时机比以上所有的入口都要早, 用于初始化console和安全机制(例如smack, 类似于SELinux),因为此时系统中大部分功能都未ready, 一般的模块请勿使用

console_initcall(fn)
security_initcall(fn)

###3. built-in时内核模块不同的等级入口的实现

以module_init()为例来说明

#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id __used \
	__attribute__((__section__(".initcall" #id ".init"))) = fn

#define device_initcall(fn)		__define_initcall(fn, 6)
#define __initcall(fn) device_initcall(fn)

#define module_init(x)	__initcall(x);

宏定义展开可得

#define module_init(fn)	\
	static initcall_t __initcall##fn##6 __used	\
	__attribute__((__section__(".initcall6.init))) = fn

又有 typedef int (*initcall_t)(void);

因此, module_init() 宏最终生成了一个静态的函数指针变量, 并且存储在 “.initcall6.init” section 中 (module_init的等级为6, 所以是“.initcall6.init”)

例如展开module_init(hello_init)可得

static initcall_t __initcallhello_init6 __used __attribute__((__section__(".initcall6.init“))) = hello_init;

即定义了一个函数指针__initcallhello_init6, 指向hello_init函数, 该函数指针存放在 “.initcall6.init“ section中, 而“__used__”修饰符起始是一个gcc属性

#define __used			__attribute__((__used__))

__used__ 告诉编译器无论 GCC 是否发现这个函数的调用实例,都要使用这个函数

对于使用其它等级来声明的入口函数的指针,, 则会被存放在对应的section中, 例如 “.initcallearly.init”, “.initcall4s.init”, “.initcallrootfs.init” 等

对于console_initcall() 和 security_initcall(), 有

#define console_initcall(fn) \
	static initcall_t __initcall_##fn \
	__used __section(.con_initcall.init) = fn

#define security_initcall(fn) \
	static initcall_t __initcall_##fn \
	__used __section(.security_initcall.init) = fn

即分别在 “.con_initcall.init” 和 “.security_initcall.init” section中存放入口函数指针

####3.1 built-in时内核模块入口函数指针的调用时机

上面的分析过程可以看出, 最终在 “.initcall6.init“ section 中保存了kernel module的入口函数的指针, 以x86为例, 在

在”include/asm-generic/vmlinux.lds.h”中

#define INIT_CALLS_LEVEL(level)						\
	VMLINUX_SYMBOL(__initcall##level##_start) = .;		\
	*(.initcall##level##.init)				\
	*(.initcall##level##s.init)		

#define INIT_CALLS							\
	VMLINUX_SYMBOL(__initcall_start) = .;			\
	*(.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) = .;

#define INIT_DATA_SECTION(initsetup_align)				\
	.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {		\
	......
	INIT_CALLS						\
	......
}

“source/arch/x86/kernel/vmlinux.lds.S”中有

SECTIONS {
	......
	INIT_DATA_SECTION(16)
	......
}

适当的展开后可得

SECTIONS {
	......
	.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {

		......

		VMLINUX_SYMBOL(__initcall_start) = .;
		*(.initcallearly.init)
	
		VMLINUX_SYMBOL(__initcall0_start) = .;
		*(.initcall##level0.init)
		*(.initcall##level0s.init)

		VMLINUX_SYMBOL(__initcall1_start) = .;
		*(.initcall##level1.init)
		*(.initcall##level1s.init)

		VMLINUX_SYMBOL(__initcall2_start) = .;
		*(.initcall##level2.init)
		*(.initcall##level2s.init)

		......

		VMLINUX_SYMBOL(__initcall7_start) = .;
		*(.initcall##level7.init)
		*(.initcall##level7s.init)

		VMLINUX_SYMBOL(__initcall_end) = .;

		......
	}
	......
}

这一段ld链接脚本定义了如何生成vmlinux 中的 “init.data” section, 先来看VMLINUX_SYMBOL宏

#define __VMLINUX_SYMBOL(x) x
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)

那么 VMLINUX_SYMBOL(__initcall_start) = .;展开后为

__initcall_start = .;

在ld脚本语法中, 这一行的意义是:定义一个符号 __initcall_start, 并且和地址“.”绑定 (“.”是一个特殊的符号, 代表当前位置加载到内存中后的地址),这个符号在程序中可以声明为extern变量, 直接使用, 再来看接下来的一句

*(.initcallearly.init)

意义是将 “.initcallearly.init” section添加到最终的 “init.data” section中

最终, 在vmlinux的 “init.data” section中, 一次包含了如下的section (不同等级的入口函数指针放置在不同的section中):

  • “.initcallearly.init”
  • ”.initcall0.init“
  • ”.initcall0s.init“
  • ”.initcall1.init“
  • ”.initcall1s.init“
  • ”.initcall2.init“
  • ”.initcall2s.init“
  • ”.initcall3.init“
  • ”.initcall3s.init“
  • ”.initcall4.init“
  • ”.initcall4s.init“
  • ”.initcall5.init“
  • ”.initcall5s.init“
  • ”.initcall6.init“
  • ”.initcall6s.init“
  • ”.initcall7.init“
  • ”.initcall7s.init“

并且还定义了如下的变量

  • __initcall_start : 存储了“.initcallearly.init” section加载进内存后的地址
  • __initcall0_start : 存储了“.initcall0.init” section加载进内存后的地址
  • __initcall1_start : 存储了“.initcall1.init” section加载进内存后的地址
  • __initcall2_start : 存储了“.initcall2.init” section加载进内存后的地址
  • __initcall3_start : 存储了“.initcall3.init” section加载进内存后的地址
  • __initcall4_start : 存储了“.initcall4.init” section加载进内存后的地址
  • __initcall5_start : 存储了“.initcall5.init” section加载进内存后的地址
  • __initcall6_start : 存储了“.initcall6.init” section加载进内存后的地址
  • __initcall7_start : 存储了“.initcall7.init” section加载进内存后的地址
  • __initcall_end : 存储了“.initcall7s.init” section加载进内存后的结尾的地址

通过上述的这些变量, 可以获取所有的built-in的 module的入口函数的指针

在linux源码的 “init/main.c” 声明了这些变量

extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];

static initcall_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

而在linux kernel的初始化过程中

start_kernel()
	rest_init()
		kernel_thread(kernel_init)
			kernel_init_freeable()
				do_basic_setup()
					do_initcalls()

static void __init do_initcalls(void)
{
	int level;

	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);
}

static void __init do_initcall_level(int level)
{
	......
	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(*fn);
}

即依次遍历每一个level中的每一个函数指针, 分别调用这些函数, 也即分别调用了不同的模块的入口函数

####3.2 onsole_initcall() 和 security_initcall() 的函数指针调用的时机

再来看console_initcall() 和 security_initcall(), 它们的函数指针的分别存储在“.con_initcall.init”和“.security_initcall.init” section中, 而在”include/asm-generic/vmlinux.lds.h”中

#define CON_INITCALL							\
	VMLINUX_SYMBOL(__con_initcall_start) = .;		\
	*(.con_initcall.init)					\
	VMLINUX_SYMBOL(__con_initcall_end) = .;

#define SECURITY_INITCALL						\
	VMLINUX_SYMBOL(__security_initcall_start) = .;		\
	*(.security_initcall.init)				\
	VMLINUX_SYMBOL(__security_initcall_end) = .;

#define INIT_DATA_SECTION(initsetup_align)				\
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {		\
	......
	INIT_CALLS						\
	CON_INITCALL						\
	SECURITY_INITCALL					\
	......
}

而在 “source/arch/x86/kernel/vmlinux.lds.S”中有

SECTIONS {
	......
	INIT_DATA_SECTION(16)
	......
}

适当的展开后有

SECTIONS {
	......
	.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {
		......

		__con_initcall_start = .;
		*(.con_initcall.init)
		__con_initcall_end = .;

		__security_initcall_start =. ;
		*(.security_initcall.init)
		__security_initcall_end = .;

		......
	}
	......
}

即在linux kerne中通过变量 __con_initcall_start 和 __con_initcall_end 可以访问 “.con_initcall.init” section中的函数指针, 而通过变量 __security_initcall_start 和 __security_initcall_end 可以访问 “.security_initcall.init” section中的函数指针, 在“linux/init.h”中声明了这几个外部变量

extern initcall_t __con_initcall_start[], __con_initcall_end[];
extern initcall_t __security_initcall_start[], __security_initcall_end[];

在 linux kernel的初始化过程中, 有

start_kernel()
	......
	console_init()
	......
	security_init()
	......
	rest_init()

在console_init()中会依次调用 ”.con_initcall.init“ section中的函数指针, security_init()中会依次调用 “.security_initcall.init” section中的函数指针, 最后的 rest_init()才完成调用其它等级的入口函数

###4. built-in时内核模块出口函数的实现

直接编译进内核的模块的出口函数(清理函数)会被忽略掉, 因为编译进内核的模块不许要做清理, 但是我们还是要来了解一下它的实现

在 “linux/init.h” 中有

#define __exit_call	__used __section(.exitcall.exit)

typedef void (*exitcall_t)(void);
#define __exitcall(fn) 	static exitcall_t __exitcall_##fn __exit_call = fn
#define module_exit(x)	__exitcall(x);

即根据出口函数的名称, 定义了一个静态的函数指针变量, 指向出口函数, 例如, 出口函数为 hello_exit()时有

static void (*__exitcall_hello_exit)(void)  __used __section(.exitcall.exit) = hello_exit;

该函数指针变量被放置在 “.exitcall.exit” section中, “__used” 修饰符指出该符号可能不会被使用, 编译器不要给出警告

而在 “include/asm-generic/vmlinux.lds.h” 中有

#define EXIT_TEXT							\
	*(.exit.text)							\
	DEV_DISCARD(exit.text)						\
	CPU_DISCARD(exit.text)						\
	MEM_DISCARD(exit.text)

#define DISCARDS							\
	/DISCARD/ : {							\
	EXIT_TEXT							\
	EXIT_DATA							\
	EXIT_CALL							\
	*(.discard)							\
	*(.discard.*)							\
}

在对应的vmlinux链接脚本文件 vmlinux.lds.S 中有

SECTIONS		{

	......

	/* Sections to be discarded */
		DISCARDS
		/DISCARD/ : { *(.eh_frame) }
}

适当展开后有

SECTIONS		{

	......

	/* Sections to be discarded */
		/DISCARD/ : {
		*(.exit.text)
		......
	}
		/DISCARD/ : { *(.eh_frame) }
}

在ld链接脚本的语法中,有一个特殊的输出section,名为/DISCARD/,被该section引用的任何输入section将不会出现在输出文件内, 即在最终链接生成的vmlinux中, “.exit.text” section直接被丢弃掉

###5. built module时内核模块入口的实现

需要注意的是,在build为模块时, 不论以何种其它的等级来声明模块的入口参数, 最终都会被替换为 module_init()

#ifndef	MODULE
	#define early_initcall(fn)		__define_initcall(fn, early)
	#define pure_initcall(fn)		__define_initcall(fn, 0)
	....
#else
	#define early_initcall(fn)		module_init(fn)
	#define core_initcall(fn)		module_init(fn)
	#define postcore_initcall(fn)	module_init(fn)
	#define arch_initcall(fn)		module_init(fn)
	#define subsys_initcall(fn)		module_init(fn)
	#define fs_initcall(fn)		module_init(fn)
	#define device_initcall(fn)		module_init(fn)
	#define late_initcall(fn)		module_init(fn)
	......
	#define module_init(initfn)					\
		static inline initcall_t __inittest(void)		\
		{ return initfn; }					\
		int init_module(void) __attribute__((alias(#initfn)));
	......
#endif

#define security_initcall(fn)		module_init(fn)

可以看到, 在编译为模块时, module_init() 宏会将模块的入口函数声明为 函数指针 “init_module” 的一个别名(即两个符号等价)

linux kernel中使用 struct module来描述一个模块的相关信息, 其中 module.init 指向模块的入口函数, 在编译模块的 MODPOST阶段(即modpost命令中的add_header()函数中), 会生成一个struct module __this_module, 并且附加在模块的 “.gnu.linkonce.this_module” section中

在编写内核模块时常用的宏 THIS_MODULE 即获取MODPOST阶段生成的 struct module __this_module的地址, 无论是built-in 还是built module

static void add_header(struct buffer *b, struct module *mod)
{
	buf_printf(b, "#include <linux/module.h>\n");
	buf_printf(b, "#include <linux/vermagic.h>\n");
	buf_printf(b, "#include <linux/compiler.h>\n");
	buf_printf(b, "\n");
	buf_printf(b, "MODULE_INFO(vermagic, VERMAGIC_STRING);\n");
	buf_printf(b, "\n");
	buf_printf(b, "struct module __this_module\n");
	buf_printf(b, "__attribute__((section(\".gnu.linkonce.this_module\"))) = {\n");
	buf_printf(b, "\t.name = KBUILD_MODNAME,\n");
	if (mod->has_init)
		buf_printf(b, "\t.init = init_module,\n");
	if (mod->has_cleanup)
		(b, "#ifdef CONFIG_MODULE_UNLOAD\n"
		      "\t.exit = cleanup_module,\n"
		      "#endif\n");
	buf_printf(b, "\t.arch = MODULE_ARCH_INIT,\n");
	buf_printf(b, "};\n");
}

在MODPOST阶段, 会为每一个模块添加一个 xxx.mod.c 文件, 例如, 若模块名为 hello, 则会添加一个 hello.mod.c 文件, 其中包含了 VERMAGIC, __this_module, __module_depends 等信息, 而_this_module.init 会被初始化为 init_module (即对应的模块入口函数的别名), 这一文件最终会被编译链接到模块中去

在用户空间调用系统调用 init_module() 来加载模块时, 内核中的执行流程如下:

SYSCALL_DEFINE3(init_module)
	load_module()
		layout_and_allocate()
			setup_load_info()	
				info->index.mod = find_sec(info, ".gnu.linkonce.this_module");
		do_init_module()
			do_one_initcall(mod->init);

在 setup_load_info() 中, 使用 “.gnu.linkonce.this_module” section的内容来初始化一个 struct module, 然后在 do_one_initcall() 中调用 module.init 函数指针, 即调用了模块的初始化函数

###6. built module时内核模块的出口的实现

built module时内核模块的出口的实现与模块的入口的实现相类似, 请先阅读 “built module时内核模块入口的实现”

在将内核模块编译成module时, 在 “linux/init.h” 中有

#define module_exit(exitfn)					\
	static inline exitcall_t __exittest(void)		\
	{ return exitfn; }					\
	void cleanup_module(void) __attribute__((alias(#exitfn)))

即将对应的模块的出口函数声明为一个函数指针变量 cleanup_module 的别名(即两个符号等价), 与上一节中的module形式的内核模块的入口函数的实现类似, 在在MODPOST阶段, 在模块的 “.gnu.linkonce.this_module” section中附加的 struct module __this_module中, module.exit 指向 cleanup_module 符号

在 用户空间调用系统调用 delete_module() 卸载内核模块时, kernel中的调用流程为

SYSCALL_DEFINE2(delete_module)
	mod->exit()

即调用了模块的出口函数