構建外部模組

本文件描述瞭如何構建樹外核心模組。

簡介

“kbuild”是 Linux 核心使用的構建系統。模組必須使用 kbuild 才能與構建基礎設施中的更改保持相容,並獲得正確的編譯器標誌。提供了構建樹內和樹外模組的功能。構建兩者的過程類似,並且所有模組最初都是在樹外開發和構建的。

本文件涵蓋的資訊面向有興趣構建樹外(或“外部”)模組的開發人員。外部模組的作者應提供一個 makefile,以隱藏大多數複雜性,因此只需鍵入“make”即可構建模組。這很容易實現,並且將在 為外部模組建立 Kbuild 檔案 一節中提供一個完整的示例。

如何構建外部模組

要構建外部模組,您必須有一個預構建的核心可用,其中包含構建中使用的配置和標頭檔案。此外,核心必須已啟用模組構建。如果您使用的是發行版核心,則您的發行版將提供一個適用於您正在執行的核心的軟體包。

另一種方法是使用“make”目標“modules_prepare”。這將確保核心包含所需的資訊。該目標的存在僅僅是為了簡化準備核心原始碼樹以構建外部模組的方式。

注意:“modules_prepare”即使設定了 CONFIG_MODVERSIONS 也不會構建 Module.symvers;因此,需要執行完整的核心構建才能使模組版本控制生效。

命令語法

構建外部模組的命令是

$ make -C <path_to_kernel_dir> M=$PWD

由於命令中給出的“M=<dir>”選項,kbuild 系統知道正在構建外部模組。

要針對正在執行的核心進行構建,請使用

$ 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 核心 Makefile 中。

下面的示例演示瞭如何為模組 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。

版本資訊格式

匯出的符號在 __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 節中,作為一系列連線的、以 null 結尾的字串。這些符號的 CRC 將記錄在 __version_ext_crcs 節中。

符號和外部模組

構建外部模組時,構建系統需要訪問核心中的符號,以檢查是否定義了所有外部符號。這是在 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