构建外部模块

本文档介绍如何构建树外内核模块。

简介

“kbuild”是 Linux 内核使用的构建系统。模块必须使用 kbuild 才能与构建基础设施中的更改保持兼容,并选择正确的编译器标志。提供了用于构建树内和树外模块的功能。构建两者的过程类似,所有模块最初都是在树外开发和构建的。

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

如何构建外部模块

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

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

注意:“modules_prepare”即使设置了 CONFIG_MODVERSIONS 也不会构建 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

共享 Makefile

外部模块始终包含一个包装 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/ 下。

内核包含

要包含位于 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/

模块版本控制

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

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

来自内核的符号 (vmlinux + 模块)

在内核构建期间,会生成一个名为 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。

符号和外部模块

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

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

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

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

使用顶级 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

将按预期执行,并编译这两个模块,并完全了解来自任一模块的符号。

使用 “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