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段

posted on 2022-08-19 19:46  开心种树  阅读(151)  评论(0编辑  收藏  举报