LV065-符号导出
内核模块编译生成的 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:
#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:
#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
#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
#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
# 模块名和模块测试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这里我们需要编译两个驱动,所以这里需要这样写:
obj-m += $(MODULE_NAME1).o
obj-m += $(MODULE_NAME2).o1.4 Kbuild.include
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。
- 加载驱动
我们执行以下命令:
insmod module_sym_math_demo.ko itype=123 btype=1 ctype=200 stype=abc
insmod module_sym_demo.ko可以看到如下输出信息:

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

- 卸载驱动
卸载驱动的时候,我们要先卸载使用符号的 module_sym_demo.ko,再卸载提供导出符号的 module_sym_math_demo.ko:
rmmod module_sym_demo.ko
rmmod module_sym_math_demo.ko我们会看到以下打印信息:

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

3. 内核导出符号表
我们在驱动中导出的符号是可以在根文件系统中直接搜索到的,内核导出的符号表在这个 /proc/kallsyms 文件中,我们可以使用 cat 命令查看:
cat /proc/kallsyms但是这个文件含有大量的符号,我们的根文件系统支持 grep 命令的话我们可以配合 grep 命令使用:
cat /proc/kallsyms | grep itype
cat /proc/kallsyms | grep sym_math_add
cat /proc/kallsyms | grep sym_math_sub我们会看到如下打印信息:
