module_init是linux內核提供的一個巨集, 可以用來在編寫內核模塊時註冊一個初始化函數, 當模塊被載入的時候, 內核負責執行這個初始化函數. 在編寫設備驅動程式時, 使用這個巨集看起來理所應當, 沒什麼特別的, 但畢竟我還是一個有點追求的程式員嘛:P, 這篇文章是我學習module_init... ...
開篇
module_init是linux內核提供的一個巨集, 可以用來在編寫內核模塊時註冊一個初始化函數, 當模塊被載入的時候, 內核負責執行這個初始化函數. 在編寫設備驅動程式時, 使用這個巨集看起來理所應當, 沒什麼特別的, 但畢竟我還是一個有點追求的程式員嘛:P, 這篇文章是我學習module_init相關源碼的一個記錄, 主要就回答了下麵的3個問題, 篇幅略長, 做好準備.
Q1:
內核模塊是什麼?
Q2:
內核模塊是怎麼被載入的?
Q3:
內核怎麼獲取到module_init註冊的初始化函數?
註: 以下回答是個人學習總結, 僅供參考.
A1:
編譯好內核模塊的代碼, 會得到一個".ko"文件, 這個就是內核模塊了. 實際上, ".ko"就是一個普通的ELF文件, 只不過可以使用insmod讓內核去動態載入它. 查閱ELF格式標準可知, 主要有三種類型的ELF文件, 包括:
- relocatable file
- excutable file
- shared object file
以上三種類型的ELF, 基本上可以簡單對應編譯得到的".o", "a.out", ".so". 這裡討論的".ko"模塊文件, 屬於relocatable file類型, 可以在系統里找一個內核模塊文件驗證一下.
junan@ZEN2:/lib/modules/5.19.0-50-generic/kernel/drivers/char$ ls
agp applicom.ko hangcheck-timer.ko hw_random ipmi lp.ko mwave nvram.ko pcmcia ppdev.ko tlclk.ko tpm uv_mmtimer.ko xillybus
junan@ZEN2:/lib/modules/5.19.0-50-generic/kernel/drivers/char$ file lp.ko
lp.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=98c89bd841e31b1140e61559c0bf312eb5128f5c, not stripped
使用gcc編譯一個c文件, 可以得到對應的.o文件, 前面說過.o文件屬於relocatable類型的ELF, 如果有多個.o文件, 可以使用鏈接器把它們"合併"成一個.o, 就像這樣:
junan@ZEN2:~$ ls
a.c a.o b.c b.o Desktop Documents Downloads Music Pictures Public snap Templates Videos
junan@ZEN2:~$ file a.o b.o
a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
b.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
junan@ZEN2:~$ ld -r a.o b.o -o c.o
junan@ZEN2:~$ file c.o
c.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
經過ld鏈接之後, 得到了"c.o", 這個"c.o"相當於合併了"a.o"和"b.o", 這可以從它們各自包含的符號中看出來:
junan@ZEN2:~$ nm a.o b.o c.o
a.o:
U add
0000000000000000 T call_add
b.o:
0000000000000000 T add
c.o:
000000000000001a T add
0000000000000000 T call_add
同樣的道理, 一個設備驅動, 可能包含多個源文件, 但是編譯之後最終可以合併成一個".ko"文件. 總結下來, ".ko"也是一種普通的ELF文件, 可以被內核動態載入和卸載.
A2:
一個內核模塊, 除了自己實現一些功能外, 通常還要引用其他人提供的api, 包括:
- 內核本身提供的api
- 其他模塊提供的api
內核僅僅把".ko"文件讀到自己的地址空間中, 是遠遠不夠的, 他要像鏈接器一樣, 幫我們的內核模塊正確地處理這些符號引用關係, 並且調用我們使用module_init註冊的模塊初始化函數. 下麵分析一下這部分的內核源碼. 我們的".ko"文件是使用insmod命令才載入進內核的, insmod命令實際上是一個符號鏈接. 和insmod一樣, rmmod也指向/bin/kmod, 當kmod被執行的時候, 可以通過args[0]區分出是執行insmod還是rmmod, 或者其他的功能.
junan@ZEN2:~$ which insmod | xargs ls -l
lrwxrwxrwx 1 root root 9 7月 22 23:20 /usr/sbin/insmod -> /bin/kmod
在內核代碼中, 專門為模塊的載入和卸載提供了兩個系統調用, 準確說是三個, 其中兩個用於載入模塊, 一個用於卸載模塊. linux代碼中使用SYSCALL_DEFINEx這個巨集定義一個系統調用的入口, 其中x代表系統調用的參數個數, 看一下內核代碼就可以找到和模塊的載入以及卸載相關的syscall函數, 使用正則表達式或者其他工具能很快在內核代碼中找到這三個系統調用的定義.
首先是init_module和finit_module:
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
// ...
return load_module(&info, uargs, 0);
}
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{
// ...
return load_module(&info, uargs, flags);
}
以上兩個syscall負責模塊的載入, 最終都調用了load_module去真正載入模塊.
然後是delete_module:
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
// ...
}
這三個syscall定義在"kernel/module.c"文件中, 我使用的內核版本是5.4.250, 下麵可以寫一個什麼都不做的內核模塊, 通過gdb調試的方法, 看一下這幾個syscall是怎麼被調用的. 環境的準備包括:
- qemu: 啟動編譯好的內核鏡像, 以及gdb server, 等待gdb的連接
- rootfs: 內核正常啟動, 需要一個根文件系統, 使用busybox製作
- kernel Image: 編譯好的內核鏡像
- ko文件: 編譯好的沒有實際功能的內核模塊
關於怎麼建立內核的調試環境, 會在其他文章中說明, 這裡僅通過調試內核的方法, 記錄一下以上三個syscall的調用過程. 當你還不知道insmod和rmmod需要使用到這三個系統調用, 怎麼樣才能知道這兩個命令依賴什麼系統調用呢? 答案是可以使用strace去定位一個進程運行過程中用到了哪些syscall, 比如, 我的系統里有一個名字叫做lp的ko模塊:
junan@ZEN:~$ lsmod | grep lp
lp 28672 0
drm_display_helper 184320 1 i915
cec 81920 2 drm_display_helper,i915
drm_kms_helper 200704 2 drm_display_helper,i915
先把這個模塊卸載, 看看使用了什麼syscall:
junan@ZEN:~$ sudo strace rmmod lp
...
...
close(3) = 0
openat(AT_FDCWD, "/sys/module/lp/refcnt", O_RDONLY|O_CLOEXEC) = 3
read(3, "0\n", 31) = 2
read(3, "", 29) = 0
close(3) = 0
delete_module("lp", O_NONBLOCK) = 0
exit_group(0) = ?
+++ exited with 0 +++
能看到倒數第3行, 調用了delete_module.
再重新載入這個lp模塊:
junan@ZEN:/usr/lib/modules/5.19.0-50-generic/kernel/drivers/char$ sudo strace insmod lp.ko
...
...
getcwd("/usr/lib/modules/5.19.0-50-generic/kernel/drivers/char", 4096) = 55
newfstatat(AT_FDCWD, "/usr/lib/modules/5.19.0-50-generic/kernel/drivers/char/lp.ko", {st_mode=S_IFREG|0644, st_size=72553, ...}, 0) = 0
openat(AT_FDCWD, "/usr/lib/modules/5.19.0-50-generic/kernel/drivers/char/lp.ko", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1", 6) = 6
lseek(3, 0, SEEK_SET) = 0
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=72553, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 72553, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4675dac000
finit_module(3, "", 0) = 0
munmap(0x7f4675dac000, 72553) = 0
close(3) = 0
exit_group(0) = ?
+++ exited with 0 +++
倒數第5行, finit_module被調用, 用來載入lp模塊. 使用這種方式, 先分析一下執行過程, 能夠幫助你大致確定它是怎麼實現的. 接下來開始調試內核, 看一下syscall的調用過程:
以下是對視頻中調試過程的詳細記錄:
-
使用qemu載入了一個編譯好的內核鏡像.
- 使用arm平臺
- 掛載本機目錄到qemu虛擬機, 目錄下包含一個等待測試的ko模塊
- 開啟調試選項, qemu啟動之後等待外部gdb的連接
-
啟動vscode遠程調試的配置, 開始調試內核代碼
- 先關閉所有斷點, 載入和卸載testko模塊, 得到正確的輸出
- 之後開啟斷點, 分別break在init_module和finit_module兩個syscall上
-
重新插入testko模塊
- init_module系統調用上的斷點命中
- 調用load_module函數
- 載入模塊到內核
- 完成鏈接, 處理符號的引用關係
- 調用testko註冊的初始化函數, 得到初始化函數的輸出
所以, 內核模塊在載入時, 需要使用init_module/finit_module系統調用, 經syscall進入內核之後, 內核會把我們的模塊載入到自己的地址空間中, 然後完成原本鏈接器需要做的工作, 這時, 模塊中引用的其他符號, 已經得到了真實的地址, 在模塊載入的最後階段, 內核調用do_one_initcall去調用我們註冊的模塊初始化函數. 以上就是內核模塊載入的基本過程, 模塊卸載的過程類似.
A3:
先找到module_init巨集的實現代碼, 在include/linux/module.h文件中, 能找到這個巨集的定義:
#ifndef MODULE
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
#else /* MODULE */
/*
* In most cases loadable modules do not need custom
* initcall levels. There are still some valid cases where
* a driver may be needed early if built in, and does not
* matter when built as a loadable module. Like bus
* snooping debug drivers.
*/
#define early_initcall(fn) module_init(fn)
#define core_initcall(fn) module_init(fn)
#define core_initcall_sync(fn) module_init(fn)
#define postcore_initcall(fn) module_init(fn)
#define postcore_initcall_sync(fn) module_init(fn)
#define arch_initcall(fn) module_init(fn)
#define subsys_initcall(fn) module_init(fn)
#define subsys_initcall_sync(fn) module_init(fn)
#define fs_initcall(fn) module_init(fn)
#define fs_initcall_sync(fn) module_init(fn)
#define rootfs_initcall(fn) module_init(fn)
#define device_initcall(fn) module_init(fn)
#define device_initcall_sync(fn) module_init(fn)
#define late_initcall(fn) module_init(fn)
#define late_initcall_sync(fn) module_init(fn)
#define console_initcall(fn) module_init(fn)
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
#endif
可以看到根據是否定義了MODULE, module_init有兩個不同的實現, 對於驅動程式的內核模塊來說, 走到的是#else分支的定義, 這一點能夠從編譯ko時詳細的編譯命令中確認:
junan@ZEN:~/Documents/github/blogcodes/01-testko$ ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE KDIR=$KDIR make -nB V=1
...
arm-linux-gnueabihf-gcc -Wp,-MD,/home/junan/Documents/github/blogcodes/01-testko/.testko.o.d -nostdinc -isystem /usr/lib/gcc-cross/arm-linux-gnueabihf/11/include -I../arch/arm/include -I./arch/arm/include/generated -I../include -I./include -I../arch/arm/include/uapi -I./arch/arm/include/generated/uapi -I../include/uapi -I./include/generated/uapi -include ../include/linux/kconfig.h -include ../include/linux/compiler_types.h -D__KERNEL__ -mlittle-endian -Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration -Werror=implicit-int -Werror=return-type -Wno-format-security -std=gnu89 -fno-dwarf2-cfi-asm -fno-ipa-sra -mabi=aapcs-linux -mfpu=vfp -funwind-tables -marm -Wa,-mno-warn-deprecated -D__LINUX_ARM_ARCH__=7 -march=armv7-a -msoft-float -Uarm -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -Wno-address-of-packed-member -O2 -fno-allow-store-data-races -Wframe-larger-than=1024 -fstack-protector-strong -Wimplicit-fallthrough -Wno-unused-but-set-variable -Wno-unused-const-variable -fomit-frame-pointer -fno-var-tracking-assignments -g -gdwarf-4 -Wdeclaration-after-statement -Wvla -Wno-pointer-sign -Wno-stringop-truncation -Wno-zero-length-bounds -Wno-array-bounds -Wno-stringop-overflow -Wno-restrict -Wno-maybe-uninitialized -fno-strict-overflow -fno-merge-all-constants -fmerge-constants -fno-stack-check -fconserve-stack -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -fmacro-prefix-map=../= -Wno-packed-not-aligned -DMODULE -DKBUILD_BASENAME='\''"testko"'\'' -DKBUILD_MODNAME='\''"testko"'\'' -c -o /home/junan/Documents/github/blogcodes/01-testko/testko.o /home/junan/Documents/github/blogcodes/01-testko/testko.c
...
從上面截取的編譯命令中, 應該能找到"-DMODULE", 雖然有點多, 但仔細看還是能找到的哈, 實在沒看到可以Ctrl+F搜索一下. gcc的-D參數相當於幫你在代碼里#define了一個巨集, 所以, 在包含module.h頭文件時, module_init確實會走到#else那個分支, 在這個分支里, module_init定義了一個static inline的函數, 並且, 聲明瞭一個名字叫做init_module的函數, 千萬不要把這個函數名和之前視頻里調試的syscall名字搞混了:
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
註意, 這個函數帶了一個alias屬性(詳細介紹, 參考官方文檔), 這樣init_module函數就變成了我們傳遞進來的initfn函數的別名, 也就是說如果調用init_module, 實際上會調用initfn. 接下來的問題是, 載入模塊的時候, 內核如何能夠得到模塊初始化函數的地址呢? 你如果自己編譯一個內核模塊代碼, 會發現編譯完成之後, 除了".ko"還會生成很多其他東西, 比如: xxx.mod.c, 內核的構建系統給你生成了一個c文件, 看一下裡面的內容:
junan@ZEN:~/Documents/github/blogcodes/01-testko$ cat testko.mod.c
#include <linux/build-salt.h>
#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>
BUILD_SALT;
MODULE_INFO(vermagic, VERMAGIC_STRING);
MODULE_INFO(name, KBUILD_MODNAME);
__visible struct module __this_module
__section(.gnu.linkonce.this_module) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
#ifdef CONFIG_RETPOLINE
MODULE_INFO(retpoline, "Y");
#endif
MODULE_INFO(depends, "");
這個文件中定義了一個struct module類型的變數__this_module, 並且.init成員已經被填上了模塊初始化函數的並名"init_module", .exit成員也是這樣. 這樣當內核把模塊載入自己的地址空間, 完成鏈接器的工作之後, 這個.init欄位就指向真實的模塊初始化函數地址了, 之前的調試視頻里可以看到有這樣的判斷:
...
if(mod->init != NULL)
ret = do_one_initcall(mod->init);
...
這裡的"mod"是一個struct module*, 它實際上就是xxx.mod.c中的__this_module, 看一下代碼:
static struct module *layout_and_allocate(struct load_info *info, int flags)
{
struct module *mod;
unsigned int ndx;
int err;
...
/* Determine total sizes, and put offsets in sh_entsize. For now
this is done generically; there doesn't appear to be any
special cases for the architectures. */
layout_sections(info->mod, info);
layout_symtab(info->mod, info);
/* Allocate and move to the final place */
err = move_module(info->mod, info);
if (err)
return ERR_PTR(err);
/* Module has been copied to its final place now: return it. */
mod = (void *)info->sechdrs[info->index.mod].sh_addr;
kmemleak_load_module(mod, info);
return mod;
}
總結下來, 關於內核怎麼獲取到我們註冊的初始化函數這個問題:
- 實現模塊初始化函數
- 使用module_init巨集傳遞上述初始化函數
- 這個巨集會聲明一個名字叫做"init_module"的函數, 這個函數是真實的模塊初始化函數的別名
- 編譯模塊時, 內核的構建系統生成xxx.mod.c文件, 在這個文件里
- 定義一個struct module類型的變數, __this_module
- 用"init_module"填充.init成員
- 順便說一下, THIS_MODULE巨集實際上就會展開成__this_module
- 內核載入模塊
- 做一些鏈接器的工作(這個地方實際上有點複雜, 之後有需要可以仔細研究, 暫時就理解為內核幫你把引用的一些符號的地址都算好了, 併在引用的地方填上正確的值)
- 獲取到__this_module的地址, 判斷.init成員是否為空, 不為空就調用模塊初始化函數
註: 當module_init巨集的定義走到第一個分支時, 之後再寫一篇討論一下.
結尾
這篇文章主要討論了3個問題:
- 內核模塊是什麼?
- 模塊是怎麼被內核載入的?
- 內核是怎麼找到模塊初始化函數的?
讀完之後, 你應該對這3個問題有了自己的體會. 除此之外, 你還應該解到一些的技術, 它們能夠幫你找到一些問題的答案:
- strace跟蹤系統調用
- 使用qemu+gdb調試內核源碼
- gcc的一些擴展語法, 比如給函數或者變數加屬性
第1點, 比較簡單, 就是一個命令的使用問題, 但是背後的原理應該不簡單;
第2點, 是一個環境搭建的問題, 有時間可以發個視頻詳細介紹一下;
第3點, 讀源碼的過程中碰到了, 不懂的話查一下官方手冊, 寫一點代碼驗證一下, 就知道怎麼用了. 實際上, 除了attribute, 還有很多方式能夠告訴編譯器, 你要做什麼.
建了個QQ群: 838923389. 有想法的老鐵可以加一下, 一起交流linux內核的使用和學習經驗. 後續也會在b站發一些技術視頻, 老鐵們覺得有需要可以先關註一下, 視頻和文章肯定會給各位帶來一些啟發和幫助.
本文來自博客園,作者:kfggww,轉載請註明原文鏈接:https://www.cnblogs.com/kfggww/p/17598809.html