Skip to content

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

c
#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

c
#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

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

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

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

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

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

1.4 Kbuild.include

makefile
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。

  • 加载驱动

我们执行以下命令:

shell
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:

shell
rmmod module_sym_demo.ko
rmmod module_sym_math_demo.ko

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

image-20241125230153963

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

image-20241125230319154

3. 内核导出符号表

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

shell
cat /proc/kallsyms

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

shell
cat /proc/kallsyms | grep itype
cat /proc/kallsyms | grep sym_math_add
cat /proc/kallsyms | grep sym_math_sub

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

image-20241125230745840