Linux构建外部驱动模块

本文档介绍了如何构建out-of-tree内核模块。

介绍

“kbuild” 是 Linux 内核使用的构建系统。模块必须使用 kbuild 才能与构建基础结构的变化保持兼容,并为编译器选择正确的标志。提供了在树内和树外构建模块的功能。构建方法相似,所有模块最初都是在树外开发和构建的。

本文档涵盖的信息针对有兴趣构建树外(或“外部”)模块的开发人员。外部模块的作者应该提供一个隐藏大部分复杂性的 makefile,因此只需键入“make”即可构建模块。这很容易实现,完整的示例将在为外部模块创建 Kbuild 文件部分中提供。

如何构建外部模块

要构建外部模块,您必须有一个可用的预构建内核,其中包含构建中使用的配置和头文件。此外,内核必须在启用模块的情况下构建。如果您使用的是发行版内核,您的发行版将提供您正在运行的内核的软件包。

另一种方法是使用“make”目标“modules_prepare”。这将确保内核包含所需的信息。该目标仅作为准备内核源代码树以构建外部模块的简单方法而存在。

注意:即使设置了 CONFIG_MODVERSIONS,“modules_prepare”也不会构建 Module.symvers;因此,需要执行完整的内核构建才能使模块版本控制正常工作。

命令语法

构建外部模块的命令是:

$ make -C <path_to_kernel_dir> M=$PWD

kbuild 系统知道正在构建外部模块,因为命令中给出了“M=<dir>”选项。

要针对正在运行的内核进行构建,请使用:

$ make -C /lib/modules/`uname -r`/build M=$PWD

然后安装刚刚构建的模块,将目标“modules_install”添加到命令中:

$ make -C /lib/modules/`uname -r`/build M=$PWD modules_install

从 Linux 6.13 开始,您可以使用 -f 选项代替 -C。这将避免不必要地更改工作目录。外部模块将输出到您调用 make 的目录中。

$ make -f /lib/modules/uname -r/build/Makefile M=$PWD

选项

($KDIR 指内核源目录的路径,如果内核是在单独的构建目录中构建的,则指内核输出目录的路径。)

如果您想在单独的目录中构建模块,则可以选择传递 MO= 选项。

make -C $KDIR M=$PWD [MO=$BUILD_DIR]

-C $KDIR

  包含用于构建外部模块的内核和相关构建工件的目录。“make”在执行时实际上会改变到指定的目录,完成后会改回来。

M=$PWD

  通知 kbuild 正在构建外部模块。赋予“M”的值是外部模块(kbuild 文件)所在目录的绝对路径。

MO=$BUILD_DIR

  为外部模块指定单独的输出目录。

目标

构建外部模块时,只有“make”目标的子集可用。

make -C $KDIR M=$PWD [target]

默认会构建位于当前目录中的模块,因此无需指定目标。所有输出文件也将在此目录中生成。不会尝试更新内核源代码,前提是已成功执行内核的“make”。

modules

  外部模块的默认目标。其功能与未指定目标时相同。请参阅上文描述。

modules_install

  安装外部模块。默认位置是 /lib/modules/<kernel_release>/updates/,但可以使用 INSTALL_MOD_PATH 添加前缀(在模块安装部分讨论)。

clean

  仅删除模块目录中所有生成的文件。

help

  列出外部模块的可用目标。

构建单独的文件

可以构建作为模块一部分的单个文件。这对于内核、模块甚至外部模块都同样适用。

示例(模块 foo.ko 由 bar.o 和 baz.o 组成):

make -C $KDIR M=$PWD bar.lst
make -C $KDIR M=$PWD baz.o
make -C $KDIR M=$PWD foo.ko
make -C $KDIR M=$PWD ./

为外部模块创建 Kbuild 文件

在上一节中,我们看到了为正在运行的内核构建模块的命令。但是,模块实际上并未构建,因为需要构建文件。此文件中将包含正在构建的模块的名称以及必需的源文件列表。该文件可能只有一行:

obj-m := <module_name>.o

kbuild 系统将从 <module_name>.c 构建 <module_name>.o,并在链接后生成内核模块 <module_name>.ko。上述行可以放在“Kbuild”文件或“Makefile”中。当模块从多个源构建时,需要另外一行列出文件:

<module_name>-y := <src1>.o <src2>.o ...

注意:有关描述 kbuild 所用语法的更多文档位于 Linux 内核 Makefiles 中。

以下示例演示了如何为模块 8123.ko 创建构建文件,该文件由以下文件构建:

8123_if.c
8123_if.h
8123_pci.c

Shared Makefile

外部模块始终包含一个包装器 makefile,它支持使用不带参数的“make”构建模块。kbuild 不使用此目标;它只是为了方便。可以包含其他功能(例如测试目标),但由于可能的名称冲突,应将其从 kbuild 中过滤掉。

示例 1:

--> filename: Makefile
ifneq ($(KERNELRELEASE),)
# kbuild part of makefile
obj-m  := 8123.o
8123-y := 8123_if.o 8123_pci.o

else
# normal makefile
KDIR ?= /lib/modules/`uname -r`/build

default:
        $(MAKE) -C $(KDIR) M=$$PWD

endif

检查 KERNELRELEASE 用来区分 makefile 的两个部分。在示例中,kbuild 只会看到这两个赋值,而“make”会看到除这两个赋值之外的所有内容。这是因为对文件进行了两次传递:第一次传递由命令行上运行的“make”实例执行;第二次传递由 kbuild 系统执行,该系统由默认目标中的参数化“make”启动。

分离 Kbuild 文件和 Makefile

Kbuild 首先会查找名为“Kbuild”的文件,如果找不到,则会查找“Makefile”。利用“Kbuild”文件,我们可以将示例 1 中的“Makefile”拆分为两个文件:

示例 2:

--> filename: Kbuild
obj-m  := 8123.o
8123-y := 8123_if.o 8123_pci.o

--> filename: Makefile
KDIR ?= /lib/modules/`uname -r`/build

default:
        $(MAKE) -C $(KDIR) M=$$PWD

由于每个文件都比较简单,示例 2 中的拆分值得怀疑;但是,一些外部模块使用由几百行组成的 makefile,在这里将 kbuild 部分与其余部分分开确实很有价值。

Linux 6.13 及更高版本支持另一种方式。外部模块 Makefile 可以直接包含内核 Makefile,而不是调用子 Make。

示例 3:

--> filename: Kbuild
obj-m  := 8123.o
8123-y := 8123_if.o 8123_pci.o

--> filename: Makefile
KDIR ?= /lib/modules/$(shell uname -r)/build
export KBUILD_EXTMOD := $(realpath $(dir $(lastword $(MAKEFILE_LIST))))
include $(KDIR)/Makefile

构建多个模块

kbuild 支持使用单个构建文件构建多个模块。例如,如果您想要构建两个模块 foo.ko 和 bar.ko,则 kbuild 行将是:

obj-m := foo.o bar.o
foo-y := <foo_srcs>
bar-y := <bar_srcs>

就这么简单!

头文件包含

在内核中,头文件根据以下规则保存在标准位置:

  • 如果头文件仅描述模块的内部接口,则该文件将与源文件放在同一目录中。
  • 如果头文件描述了内核其他部分使用的接口,而这些接口位于不同的目录中,则该文件将放在include/linux/中。

注意:
此规则有两个值得注意的例外:较大的子系统在include/下有自己的目录,例如include/scsi;特定于体系结构的头文件位于arch/$(SRCARCH)/include/下。

Kernel Includes

要包含位于 include/linux/ 下的头文件,只需使用:

#include <linux/module.h>

kbuild 将向编译器添加选项,以便搜索相关目录。

单个子目录

外部模块倾向于将头文件放在其源所在的单独 include/ 目录中,尽管这不是通常的内核样式。要通知 kbuild 该目录,请使用 ccflags-y 或 CFLAGS_<filename>.o。

使用第 3 节中的示例,如果我们将 8123_if.h 移动到名为 include 的子目录中,则生成的 kbuild 文件将如下所示:

--> filename: Kbuild
obj-m := 8123.o

ccflags-y := -I $(src)/include
8123-y := 8123_if.o 8123_pci.o

多个子目录

kbuild 可以处理分布在多个目录中的文件。请考虑以下示例:

.
|__ src
|   |__ complex_main.c
|   |__ hal
|       |__ hardwareif.c
|       |__ include
|           |__ hardwareif.h
|__ include
|__ complex.h

为了构建模块 complex.ko,我们需要以下 kbuild 文件:

--> filename: Kbuild
obj-m := complex.o
complex-y := src/complex_main.o
complex-y += src/hal/hardwareif.o

ccflags-y := -I$(src)/include
ccflags-y += -I$(src)/src/hal/include

如您所见,kbuild 知道如何处理位于其他目录中的目标文件。诀窍是指定相对于 kbuild 文件位置的目录。话虽如此,这不是推荐的做法。

对于头文件,必须明确告知 kbuild 查找位置。当 kbuild 执行时,当前目录始终是内核树的根(“-C”的参数),因此需要绝对路径。$(src) 通过指向当前执行的 kbuild 文件所在的目录来提供绝对路径。

模块安装

内核包含的模块安装在以下目录中:

/lib/modules/$(KERNELRELEASE)/kernel/

外部模块安装在:

/lib/modules/$(KERNELRELEASE)/updates/

INSTALL_MOD_PATH

以上是默认目录,但一如既往,可以进行一定程度的自定义。可以使用变量 INSTALL_MOD_PATH 将前缀添加到安装路径:

$ make INSTALL_MOD_PATH=/frodo modules_install
=> Install dir: /frodo/lib/modules/$(KERNELRELEASE)/kernel/

INSTALL_MOD_PATH 可以设置为普通的 shell 变量,或者如上所示,可以在调用“make”时在命令行中指定。这在安装树内和树外模块时均有效。

INSTALL_MOD_DIR

外部模块默认安装到 /lib/modules/$(KERNELRELEASE)/updates/ 下的目录中,但您可能希望将特定功能的模块放在单独的目录中。为此,请使用 INSTALL_MOD_DIR 指定“updates”的替代名称。:

$ make INSTALL_MOD_DIR=gandalf -C $KDIR \
       M=$PWD modules_install
=> Install dir: /lib/modules/$(KERNELRELEASE)/gandalf/

Module Versioning

模块版本控制由 CONFIG_MODVERSIONS 标签启用,并用作简单的 ABI 一致性检查。将创建导出符号的完整原型的 CRC 值。加载/使用模块时,内核中包含的 CRC 值将与模块中的类似值进行比较;如果它们不相等,内核将拒绝加载模块。

Module.symvers 包含内核构建中所有导出符号的列表。

内核的符号 (vmlinux + modules)

在内核构建期间,将生成一个名为 Module.symvers 的文件。Module.symvers 包含从内核和编译模块导出的所有符号。对于每个符号,还存储了相应的 CRC 值。

Module.symvers 文件的语法为:

<CRC>       <Symbol>         <Module>                         <Export Type>     <Namespace>

0xe1cc2a05  usb_stor_suspend drivers/usb/storage/usb-storage  EXPORT_SYMBOL_GPL USB_STORAGE

字段由制表符分隔,值可能为空(例如,如果没有为导出的符号定义命名空间)。

对于未启用 CONFIG_MODVERSIONS 的内核构建,CRC 将读取 0x00000000。

Module.symvers 有两个用途:

  1. 它列出了从 vmlinux 和所有模块导出的所有符号。
  2. 如果启用了 CONFIG_MODVERSIONS,它会列出 CRC。

版本信息格式

导出的符号的信息存储在 __ksymtab 或 __ksymtab_gpl 部分中。符号名称和命名空间存储在 __ksymtab_strings 中,使用与 ELF 使用的字符串表类似的格式。如果启用了 CONFIG_MODVERSIONS,则导出符号对应的 CRC 将添加到 __kcrctab 或 __kcrctab_gpl。

如果启用了 CONFIG_BASIC_MODVERSIONS(CONFIG_MODVERSIONS 的默认设置),则导入的符号的符号名称和 CRC 将存储在导入模块的 __versions 部分中。此模式仅支持长度不超过 64 字节的符号。

如果启用了 CONFIG_EXTENDED_MODVERSIONS(需要同时启用 CONFIG_MODVERSIONS 和 CONFIG_RUST),则导入的符号的符号名称将记录在 __version_ext_names 部分中,作为一系列连接的、以空字符结尾的字符串。这些符号的 CRC 将记录在 __version_ext_crcs 部分中。

符号和外部模块

构建外部模块时,构建系统需要访问内核中的符号,以检查所有外部符号是否都已定义。这在 MODPOST 步骤中完成。modpost 通过从内核源代码树读取 Module.symvers 来获取符号。在 MODPOST 步骤期间,将写入一个新的 Module.symvers 文件,其中包含来自该外部模块的所有导出符号。

来自另一个外部模块的符号

有时,外部模块会使用从另一个外部模块导出的符号。Kbuild 需要完全了解所有符号,以避免发出有关未定义符号的警告。针对这种情况有两种解决方案。

注意:建议使用顶层 kbuild 文件的方法,但在某些情况下可能不切实际。

1. 使用顶层 kbuild 文件
如果您有两个模块,foo.ko 和 bar.ko,其中 foo.ko 需要来自 bar.ko 的符号,则可以使用通用的顶层 kbuild 文件,以便两个模块在同一个构建中编译。请考虑以下目录布局:

./foo/ <= contains foo.ko
./bar/ <= contains bar.ko

顶层 kbuild 文件看起来如下:

#./Kbuild (or ./Makefile):
        obj-m := foo/ bar/

并执行:

$ make -C $KDIR M=$PWD

然后将执行预期的操作并在充分了解任一模块的符号的情况下编译两个模块。

2. 使用“make”变量 KBUILD_EXTRA_SYMBOLS
如果添加顶层 kbuild 文件不切实际,您可以在构建文件中将一个空格分隔的文件列表分配给 KBUILD_EXTRA_SYMBOLS。这些文件将由 modpost 在其符号表初始化期间加载。

提示和技巧

测试 CONFIG_FOO_BAR
模块通常需要检查某些 CONFIG_ 选项,以确定模块中是否包含特定功能。在 kbuild 中,这通过直接引用 CONFIG_ 变量来完成:

#fs/ext2/Makefile
obj-$(CONFIG_EXT2_FS) += ext2.o

ext2-y := balloc.o bitmap.o dir.o
ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o

 

posted @ 2025-03-20 18:37  闹闹爸爸  阅读(173)  评论(0)    收藏  举报