LV06-02-内核模块-08-内核模块符号导出

本文主要是内核模块——内核模块符号导出的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

内核模块编译生成的 ko 文件是相互独立的, 即模块之间变量或者函数在正常情况下无法进行互相访问。 而一些复杂的驱动模块需要分层进行设计, 这时候就需要用到内核模块符号导出(也可以叫符号共享)。

一、内核符号导出

1. 内核符号

内核符号指的就是在内核模块中导出函数和变量, 在加载模块时被记录在公共内核符号表中,以供其他模块调用。 这个机制,允许我们使用分层的思想解决一些复杂的模块设计。我们在编写一个驱动的时候, 可以把驱动按照功能分成几个内核模块,借助符号共享去实现模块与模块之间的接口调用,变量共享。

2. 符号导出使用的宏

符号导出所使用的宏为 EXPORT_SYMBOL(sym)和 EXPORT_SYMBOL_GPL(sym)。

2.1 EXPORT_SYMBOL(sym)

EXPORT_SYMBOL(sym)定义在export.h - include/linux/export.h - EXPORT_SYMBOL

1
2
#define EXPORT_SYMBOL(sym)					\
__EXPORT_SYMBOL(sym, "")

sym 表示要导出的函数或变量名称。

2.2 EXPORT_SYMBOL_GPL(sym)

EXPORT_SYMBOL_GPL(sym)定义在export.h - include/linux/export.h - EXPORT_SYMBOL_GPL

1
2
#define EXPORT_SYMBOL_GPL(sym)					\
__EXPORT_SYMBOL(sym, "_gpl")

EXPORT_SYMBOL(sym)和 EXPORT_SYMBOL_GPL(sym)两个宏使用方法相同, 而 EXPORT_SYMBOL_GPL(sym)导出的模块只能被 GPL 许可的模块使用, 所以绝大多数的情况都使用 EXPORT_SYMBOL(sym)进行符号导出。 sym表示要导出的函数或变量名称。

二、符号导出实例

1. 代码实例

1.1 module_sym_math_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <linux/kernel.h>
#include <linux/init.h> /* module_init module_exit */
#include <linux/module.h> /* MODULE_LICENSE */
#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

int itype = 0; // 定义int类型变量
static bool btype = 0; // 定义bool类型变量
static char ctype = 0; // 定义char类型变量
static char *stype = 0; // 定义char *类型指针变量

module_param(itype, int, 0);
module_param(btype, bool, S_IRUGO);
module_param(ctype, byte, 0);
module_param(stype, charp, S_IRUGO);

// 模块入口函数
static int __init module_sym_math_demo_init(void)
{
printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
printk("module_sym_math_demo module init!\n");
printk(KERN_ALERT "itype=%d\n", itype);
printk(KERN_ALERT "btype=%d\n", btype);
printk(KERN_ALERT "ctype=%d\n", ctype);
printk(KERN_ALERT "stype=%s\n", stype);
return 0;
}

// 模块出口函数
static void __exit module_sym_math_demo_exit(void)
{
printk("module_sym_math_demo exit!\n");
}

// 自定义加法函数
int sym_math_add(int a, int b)
{
return a + b;
}

// 自定义减法函数
int sym_math_sub(int a, int b)
{
return a - b;
}

EXPORT_SYMBOL(itype); // 导出参数num
EXPORT_SYMBOL(sym_math_add);
EXPORT_SYMBOL(sym_math_sub);

module_init(module_sym_math_demo_init); // 将__init定义的函数指定为驱动的入口函数
module_exit(module_sym_math_demo_exit); // 将__exit定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo module_sym_math_demo 查看) */
MODULE_LICENSE("GPL V2"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

在这个内核模块中,我们导出三个符号:itype、sym_math_add和sym_math_sub,第一个为变量,后面两个为函数。另外该模块可以传入四个参数。

1.2 module_sym_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <linux/kernel.h>
#include <linux/init.h> /* module_init module_exit */
#include <linux/module.h> /* MODULE_LICENSE */
#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

extern int itype;
int sym_math_add(int a, int b);
int sym_math_sub(int a, int b);

// 模块入口函数
static int __init module_sym_demo_init(void)
{
printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
printk("module_sym_demo module init!\n");

printk(KERN_ALERT "itype+1=%d, itype-1=%d\n", sym_math_add(itype, 1), sym_math_sub(itype, 1));
return 0;
}

// 模块出口函数
static void __exit module_sym_demo_exit(void)
{
printk("module_sym_demo exit!\n");
}

module_init(module_sym_demo_init); // 将__init定义的函数指定为驱动的入口函数
module_exit(module_sym_demo_exit); // 将__exit定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo module_sym_demo 查看) */
MODULE_LICENSE("GPL V2"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

该内核模块使用module_sym_math_demo.ko中导出的变量和符号进行运算。

1.3 Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# 模块名和模块测试APP名称
MODULE_NAME1 := module_sym_demo
MODULE_NAME2 := module_sym_math_demo

ARCH ?= arm
MAKE_PARAM :=
CURRENT_PATH := $(shell pwd)
KERNEL_KO_RELEASE = $(shell git rev-parse --verify --short HEAD)
ifeq ("$(origin V)", "command line")
KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
KBUILD_VERBOSE = 0
endif

ifeq ($(KBUILD_VERBOSE),1)
quiet =
Q =
else
quiet=quiet_
Q = @
endif

#=====================================================
INCDIRS := ./
SRCDIRS := ./

INCLUDE := $(patsubst %, -I %, $(INCDIRS))
#=====================================================
# NFS 共享目录
TFTP_SERVER ?= ~/3tftp
NFS_SERVER ?= ~/4nfs

TFTP_DIR ?= $(TFTP_SERVER)
ROOTFS_ROOT_DIR ?= $(NFS_SERVER)/imx6ull_rootfs
#ROOTFS_MODULE_DIR ?= $(ROOTFS_ROOT_DIR)/lib/modules/4.19.71-00007-g51f3cd8ec-dirty
ROOTFS_MODULE_DIR ?= $(ROOTFS_ROOT_DIR)/drivers_demo
#=====================================================
ifeq ($(KERNELRELEASE),)

# 选择可执行文件运行的平台
ifeq ($(ARCH), arm)
KERNELDIR ?= ~/7Linux/imx6ull-kernel
MAKE_PARAM += ARCH=arm
MAKE_PARAM += CROSS_COMPILE=arm-linux-gnueabihf-
CROSS_COMPILE_PREFIX ?= arm-linux-gnueabihf-
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
CROSS_COMPILE_PREFIX ?=
endif

CC := $(CROSS_COMPILE_PREFIX)gcc
LD := $(CROSS_COMPILE_PREFIX)ld

include Kbuild.include

srctree := .
timestamp_h := timestamp_autogenerated.h
version_h := version_autogenerated.h

export CC LD srctree

# 编译模块和测试程序
all: $(timestamp_h) $(version_h) modules

modules:
$(MAKE) $(MAKE_PARAM) -C $(KERNELDIR) M=$(CURRENT_PATH) modules $(INCLUDE)

modules_install:
$(MAKE) $(MAKE_PARAM) -C $(KERNELDIR) M=$(CURRENT_PATH) modules INSTALL_MOD_PATH=$(ROOTFS_MODULE_DIR) modules_install

$(timestamp_h): $(srctree)/Makefile FORCE
$(call filechk,timestamp_autogenerated.h)

$(version_h): $(srctree)/Makefile FORCE
$(call filechk,version_autogenerated.h)

# 拷贝相关文件到nfs共享目录
install:
$(Q)sudo cp -v *.ko $(ROOTFS_MODULE_DIR)

uninstall:
$(Q)sudo rm -vf $(ROOTFS_MODULE_DIR)/$(MODULE_NAME1).ko
$(Q)sudo rm -vf $(ROOTFS_MODULE_DIR)/$(MODULE_NAME2).ko

PHONY += FORCE
FORCE:

PHONY += clean
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
$(Q)rm -rf $(timestamp_h)
$(Q)rm -rf $(version_h)

.PHONY: $(PHONY)

help:
@echo "\033[1;32m================================ Help ================================\033[0m"
@echo "Ubuntu may need to add sudo:"
@echo "insmod <module_name>.ko # Load module"
@echo "rmmod <module_name> # Uninstall the module"
@echo "dmesg -C # Clear the kernel print information"
@echo "lsmod # Check the kernel modules that have been inserted"
@echo "dmesg # View information printed by the kernel"
@echo "file <module_name>.ko # View \".Ko\" file information"
@echo ""
@echo "make ARCH=x86_64 # x86_64 platform"
@echo "make # arm platform"
@echo "\033[1;32m======================================================================\033[0m"

print:
@echo "KERNELDIR = $(KERNELDIR)"
@echo "INCLUDE = $(INCLUDE)"
else
CONFIG_MODULE_SIG = n
obj-m += $(MODULE_NAME1).o
obj-m += $(MODULE_NAME2).o
endif

这里我们需要编译两个驱动,所以这里需要这样写:

1
2
obj-m            += $(MODULE_NAME1).o
obj-m += $(MODULE_NAME2).o

1.4 Kbuild.include

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
define filechk_timestamp_autogenerated.h
(if test -n "$${SOURCE_DATE_EPOCH}"; then \
SOURCE_DATE="@$${SOURCE_DATE_EPOCH}"; \
DATE=""; \
for date in gdate date.gnu date; do \
$${date} -u -d "$${SOURCE_DATE}" >/dev/null 2>&1 && DATE="$${date}"; \
done; \
if test -n "$${DATE}"; then \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_DATE "%b %d %C%y"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_TIME "%T"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_TZ "%z"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_DMI_DATE "%m/%d/%Y"'; \
LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define KERNEL_KO_BUILD_DATE 0x%Y%m%d'; \
else \
return 42; \
fi; \
else \
LC_ALL=C date +'#define KERNEL_KO_DATE "%b %d %C%y"'; \
LC_ALL=C date +'#define KERNEL_KO_TIME "%T"'; \
LC_ALL=C date +'#define KERNEL_KO_TZ "%z"'; \
LC_ALL=C date +'#define KERNEL_KO_DMI_DATE "%m/%d/%Y"'; \
LC_ALL=C date +'#define KERNEL_KO_BUILD_DATE 0x%Y%m%d'; \
fi)
endef

define filechk_version_autogenerated.h
(echo \#define PLAIN_VERSION \"$(KERNEL_KO_RELEASE)\"; \
echo \#define KERNEL_KO_VERSION \"\" PLAIN_VERSION; \
echo \#define CC_VERSION_STRING \"$$(LC_ALL=C $(CC) --version | head -n 1)\"; \
echo \#define LD_VERSION_STRING \"$$(LC_ALL=C $(LD) --version | head -n 1)\"; )
endef

define filechk
$(Q)set -e; \
: ' CHK $@'; \
mkdir -p $(dir $@); \
$(filechk_$(1)) < $< > $@.tmp; \
if [ -r $@ ] && cmp -s $@ $@.tmp; then \
rm -f $@.tmp; \
else \
: ' UPD $@'; \
mv -f $@.tmp $@; \
fi
endef

这个文件用于生成编译时间和git版本号。

2. 开发板测试

我们编译完毕后将其放到开发板的根文件系统中。然后就可以加载了,由于module_sym_demo.ko要使用module_sym_math_demo.ko中的符号,我们需要先加载module_sym_math_demo.ko。

  • 加载驱动

我们执行以下命令:

1
2
insmod module_sym_math_demo.ko itype=123 btype=1 ctype=200 stype=abc
insmod module_sym_demo.ko

可以看到如下输出信息:

image-20241125225945338

若是反过来的话就会报如下错误:

image-20241125230218230
  • 卸载驱动

卸载驱动的时候,我们要先卸载使用符号的module_sym_demo.ko,再卸载提供导出符号的module_sym_math_demo.ko:

1
2
rmmod module_sym_demo.ko
rmmod module_sym_math_demo.ko

我们会看到以下打印信息:

image-20241125230153963

若是反过来,则会报以下错误:

image-20241125230319154

3. 内核导出符号表

我们在驱动中导出的符号是可以在根文件系统中直接搜索到的,内核导出的符号表在这个/proc/kallsyms文件中,我们可以使用cat命令查看:

1
cat /proc/kallsyms

但是这个文件含有大量的符号,我们的根文件系统支持grep命令的话我们可以配合grep命令使用:

1
2
3
cat /proc/kallsyms | grep itype
cat /proc/kallsyms | grep sym_math_add
cat /proc/kallsyms | grep sym_math_sub

我们会看到如下打印信息:

image-20241125230745840