kernel——模块机制
1. 使用模块机制
测试模块hello.c
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk("Hello world\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YangXR");
1.1 在源码树内添加模块
1)将hello.c放到 driver/char/目录下
2)修改 driver/char/Kconfig,添加
config HELLO
tristate "hello module for test"
default n
help
This is a test
3)修改 driver/char/Makefile,添加
obj-$(CONFIG_HELLO) += hello.o
1.2 在内核源码外编译
添加Makefile
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
hello-objs := hello.o
else
.PHONY: all clean
KBUILD_EXTRA_SYMBOLS += /root/wlt/build/my/math/Module.symvers
export KBUILD_EXTRA_SYMBOLS
CROSS_COMPILE := arm-linux-gnueabi-
KBUILD_DIR := /root/wlt/build/linux-5.16.2
all:
make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean
rm -f hello_test
endif
解释下:
ifneq ($(KERNELRELEASE),)
# 第一部分
obj-m := hello.o
else
# 第二部分
...
endif
首先执行的是第二部分,
make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules
到kernel源码目录下,构建目标为moudles,且传变量 M,变量M的内容是 要编译模块的目录,在kernel的Makefile中会判断 M是否为空,若不为空,则根据 M变量传递的目录,读取编译模块目录的 Makefile,这时由于 KERNELRELEASE不为空(kernel的Makefile设置的),导致有效语句为 obj-m := hello.o,所以kernel 使用 hello.c 构建 hello.ko。
注意
obj-m := hello.o # 定义构建模块为 hello.ko
hello-objs := hello.o tmp.o # 定义模块的目标文件
如果不写 hello-objs,则默认只使用 hello.o
KBUILD_EXTRA_SYMBOLS += /root/wlt/build/my/math/Module.symvers
export KBUILD_EXTRA_SYMBOLS
hello模块使用了math模块导出的符号(函数或变量),使用 export 导出变量KBUILD_EXTRA_SYMBOLS,除了 自己添加的 符号文件,在 kernel编译后,可以看到kernel目录下也有一个 Module.symvers,在编译模块时,kernel的Makefile会根据这些符号文件,检查模块使用的外部符号是否定义。所以编译模块前需要先编译内核,和包含导入符号的其他模块。
match模块,使用 EXPORT_SYMBOL 导出符号。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
int math_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(math_add);
int math_sub(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL(math_sub);
static int __init math_init(void)
{
return 0;
}
static void __exit math_exit(void)
{
}
module_init(math_init);
module_exit(math_exit);
MODULE_LICENSE("GPL");
1.3 模块传参
/**
* module_param - typesafe helper for a module/cmdline parameter
* @name: the variable to alter, and exposed parameter name.
* @type: the type of the parameter
* @perm: visibility in sysfs.
*
* @name becomes the module parameter, or (prefixed by KBUILD_MODNAME and a
* ".") the kernel commandline parameter. Note that - is changed to _, so
* the user can use "foo-bar=1" even for variable "foo_bar".
*
* @perm is 0 if the variable is not to appear in sysfs, or 0444
* for world-readable, 0644 for root-writable, etc. Note that if it
* is writable, you may need to use kernel_param_lock() around
* accesses (esp. charp, which can be kfreed when it changes).
*
* The @type is simply pasted to refer to a param_ops_##type and a
* param_check_##type: for convenience many standard types are provided but
* you can create your own by defining those variables.
*
* Standard types are:
* byte, hexint, short, ushort, int, uint, long, ulong
* charp: a character pointer
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
*/
#define module_param(name, type, perm) \
module_param_named(name, name, type, perm)
注释说明了:
模块支持通过 模块加载时或sysfs传参
name: 要改变的变量名,和暴露出的参数名
type: 参数类型
perm: sysfs中的可见性
#include <linux/init.h>
#include <linux/module.h>
static int num = 11;
module_param(num, int, 0660);
static int __init hello_init(void)
{
printk("Hello world\n");
printk("num : %d\n", num);
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye\n");
printk("num : %d\n", num);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YangXR");
通过sysfs可以直接修改和查看参数,提高驱动调试效率
yangxr@vexpress:/root # cat /sys/module/hello/parameters/num
11
yangxr@vexpress:/root # echo 5 > /sys/module/hello/parameters/num
yangxr@vexpress:/root # cat /sys/module/hello/parameters/num
5
1.4 导出模块的符号
当希望导出本模块的符号给其他模块链接时,可以使用 EXPORT_SYMBOL
如下实现个 math模块
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
int math_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(math_add);
int math_sub(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL(math_sub);
static int __init math_init(void)
{
return 0;
}
static void __exit math_exit(void)
{
}
module_init(math_init);
module_exit(math_exit);
MODULE_LICENSE("GPL");
导出了 math_add math_sub,编译math模块,得到Module.symvers
root@ubuntu:~/wlt/build/my/math# cat Module.symvers
0x00000000 math_sub /root/wlt/build/my/math/math EXPORT_SYMBOL
0x00000000 math_add /root/wlt/build/my/math/math EXPORT_SYMBOL
hello模块使用math模块导出的符号
#include <linux/module.h>
#include <linux/init.h>
// 声明导入的函数
extern int math_add(int a, int b);
extern int math_sub(int a, int b);
static int __init
hello_init(void)
{
printk("1+2=%d\n", math_add(1, 2));
printk("2-1=%d\n", math_sub(2, 1));
return 0;
}
static void __exit
hello_exit(void)
{
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
hello模块的Makefile需要 导出变量KBUILD_EXTRA_SYMBOLS,其为导入符号表
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
.PHONY: all clean
KBUILD_EXTRA_SYMBOLS += /root/wlt/build/my/math/Module.symvers
export KBUILD_EXTRA_SYMBOLS
CROSS_COMPILE := arm-linux-gnueabi-
KBUILD_DIR := /root/wlt/build/linux-5.16.2
all:
make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules
$(CROSS_COMPILE)gcc ./hello_test.c -o hello_test
clean:
make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean
rm -f hello_test
endif
加载模块时,先加载 math.ko,再加载hello.ko
1.5 uboot通过内核参数传参
将hello模块链接进内核。
修改uboot启动参数
=> printenv bootcmd
bootcmd=tftp 0x60003000 uImage; tftp 0x60500000 vexpress-v2p-ca9.dtb; setenv bootargs 'root=/dev/nfs rw nfsroot=192.168.5.129:/root/wlt/rootfs ip=192.168.5.127 init=/linuxrc console=ttyAMA0 hello.num=10'; bootm 0x60003000 - 0x60500000;
yangxr@vexpress:/ # dmesg |grep Hello -A1
Hello world
num : 11
1.6 模块自动依赖
保证目录,并把模块移动到该目录下,运行 depmod -a ,生成 modules.dep.bb
yangxr@vexpress:/root # depmod -a
yangxr@vexpress:/root # ls /lib/modules/5.16.2/
hello.ko math.ko modules.dep.bb
yangxr@vexpress:/root # modprobe hello
1+2=3
2-1=1
yangxr@vexpress:/root # lsmod hello
hello 16384 0 - Live 0x7f005000 (O)
math 16384 1 hello, Live 0x7f000000 (O)
yangxr@vexpress:/root # modprobe -r hello
1.7 分析模块机制——insmod
static int __init hello_init(void)
{
dump_stack();
return 0;
}
static void __exit hello_exit(void)
{
dump_stack();
}
48 #define __init __section(".init.text")
54 #define __exit __section(".exit.text")
__init 和 __exit 将 hello_init 和 hello_exit 的代码放到 .init.text 段
130 #define module_init(initfn) \
131 static inline initcall_t __maybe_unused __inittest(void) \
132 { return initfn; } \
133 int init_module(void) __copy(initfn) \
134 __attribute__((alias(#initfn))); \
135 __CFI_ADDRESSABLE(init_module, __initdata);
module_init 对 hello_init 取了别名 init_module
138 #define module_exit(exitfn) \
139 static inline exitcall_t __maybe_unused __exittest(void) \
140 { return exitfn; } \
141 void cleanup_module(void) __copy(exitfn) \
142 __attribute__((alias(#exitfn))); \
143 __CFI_ADDRESSABLE(cleanup_module, __exitdata);
module_exit 对 hello_exit 取了别名 cleanup_module
Kbuild系统将生成 hello.mod.c
#include <linux/module.h>
#define INCLUDE_VERMAGIC
#include <linux/build-salt.h>
#include <linux/elfnote-lto.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>
BUILD_SALT;
BUILD_LTO_INFO;
MODULE_INFO(vermagic, VERMAGIC_STRING);
MODULE_INFO(name, KBUILD_MODNAME);
__visible struct module __this_module
__section(".gnu.linkonce.this_module") = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
MODULE_INFO(intree, "Y");
#ifdef CONFIG_RETPOLINE
MODULE_INFO(retpoline, "Y");
#endif
MODULE_INFO(depends, "");
定义了 struct module __this_module , 放到 .gnu.linkonce.this_module 段,绑定了 回调函数。
调用过程
yangxr@vexpress:/root # insmod ./hello.ko
CPU: 0 PID: 63 Comm: insmod Not tainted 5.16.2 #8
Hardware name: ARM-Versatile Express
[<8010f358>] (unwind_backtrace) from [<8010b254>] (show_stack+0x10/0x14)
[<8010b254>] (show_stack) from [<80836b74>] (dump_stack_lvl+0x40/0x4c)
[<80836b74>] (dump_stack_lvl) from [<7f005008>] (hello_init+0x8/0x1000 [hello])
[<7f005008>] (hello_init [hello]) from [<80101f90>] (do_one_initcall+0x48/0x1e8)
[<80101f90>] (do_one_initcall) from [<808313b0>] (do_init_module+0x54/0x208)
[<808313b0>] (do_init_module) from [<801aca54>] (load_module+0x2058/0x2670)
[<801aca54>] (load_module) from [<801ad1c4>] (sys_init_module+0x158/0x170)
[<801ad1c4>] (sys_init_module) from [<80100060>] (ret_fast_syscall+0x0/0x54)
yangxr@vexpress:/root # rmmod hello.ko
CPU: 0 PID: 65 Comm: rmmod Not tainted 5.16.2 #8
Hardware name: ARM-Versatile Express
[<8010f358>] (unwind_backtrace) from [<8010b254>] (show_stack+0x10/0x14)
[<8010b254>] (show_stack) from [<80836b74>] (dump_stack_lvl+0x40/0x4c)
[<80836b74>] (dump_stack_lvl) from [<801aa834>] (sys_delete_module+0x130/0x1f0)
[<801aa834>] (sys_delete_module) from [<80100060>] (ret_fast_syscall+0x0/0x54)
整个调用过程如下,
总结,
insmod程序首先读取模块文件,加载到内存,软中断 sys_init_module 陷入内核态,
内核态的insmod把用户空间的模块拷贝到内核空间,
进行一些检查,再重新规划和重定位模块(和动态加载库一样),
然后根据段信息,找到变量的指针,也就找到了 init函数指针,
回调init函数,最后释放 .init.text 段内存,而 .text 段的其他函数常驻内核空间,成为内核服务的一部分。
软中断返回。
1.8 模块机制——内嵌内核
[<8010f358>] (unwind_backtrace) from [<8010b254>] (show_stack+0x10/0x14)
[<8010b254>] (show_stack) from [<80836b74>] (dump_stack_lvl+0x40/0x4c)
[<80836b74>] (dump_stack_lvl) from [<80b21c48>] (hello_init+0x8/0x10)
[<80b21c48>] (hello_init) from [<80101f90>] (do_one_initcall+0x48/0x1e8)
[<80101f90>] (do_one_initcall) from [<80b01294>] (kernel_init_freeable+0x234/0x2a4)
[<80b01294>] (kernel_init_freeable) from [<80838844>] (kernel_init+0x18/0x134)
[<80838844>] (kernel_init) from [<80100148>] (ret_from_fork+0x14/0x2c)
总结
调用内嵌内核的模块的init的原理:
1) module_init(hello_init); 将 hello_init指针 放到 .initcall6.init 段
2) 链接脚本把 .initcall6.init段的内容当作数组, __initcall6_start 做数组符号。
3) kernel初始化时遍历 __initcall6_start ,把数组元素当函数指针执行
4) 完成所有模块的init后,释放所有init段