contributed by < flawless0714
>
檔案 fibdrv.c
裡頭的 MODULE_LICENSE
, MODULE_AUTHOR
, MODULE_DESCRIPTION
, MODULE_VERSION
等巨集做了什麼事,可以讓核心知曉呢? insmod
這命令背後,對應 Linux 核心內部有什麼操作呢?請舉出相關 Linux 核心原始碼並解讀
##
為連接子(concatenator)),會有兩層的原因是第一層的輸入有可能是巨集字串,假如沒有兩層巨集包裝的話巨集名稱會被當作字串。__UNIQUE_ID_
做連接,之後在與 __COUNTER__
做連接,最後輸出 __UNIQUE_ID_license__COUNTER
。此巨集在 v3.8-rc1 之前為 __module_cat
,此外,這個 commit 沒有註明替換原因,找了兩個 kernel 的版控系統都沒看到,不知道是不是只在 mailing list 裡討論。__attribute__
後的 section("")
為提示編譯器要將此資訊存放在名為 .modinfo
的 section,unused
為告知編譯器不需跳警告 (編譯器會對沒有使用的變數提出警告),aligned(1)
為告知編譯器此變數在 allocation 時是以每單位(字元)一個 byte 來存放。MODULE_INFO
時會由第一個參數告知此為模組的 license 資訊,並由第二個參數告知 license 名稱。insmod
insmod
首先使用模組中的 init_module()
巨集去提示 kernel 有模組要求載入,並將控制權移交給 kernel。sys_init_module
,之後首先檢查使用者是否有權限載入模組。load_module()
,此函數會配置一段暫時性記憶體空間,並使用 copy_from_user
將模組的 elf 複製到這段記憶體。load_module
會開始解譯 elf 檔,並且根據剛才配置的記憶體空間來產生 offset,這個 offset 名為 convenience variable。insmod
時給的參數也複製進 kernel space 的記憶體。MODULE_STATE_COMING
。SHF_ALLOC
配置)。load_module
的任務已經完成,回傳值為此模組記憶體位置的 reference。MODULE_STATE_LIVE
。init_fib_dev
,此參數為模組初始化函數之記憶體位置。module_init()
展開後的程式碼如下:
程式碼第 2 行的 initcall_t
為 function type,此處為 pointer of int。__maybe_unused
這個巨集即為前面提過的 gcc attribute,unused
。此函數功用為回傳函數的記憶體位置(initfn
)。接下來宣告的函數(init_module()
)為巨集帶入的參數(initfn
)的函數別名(當呼叫 init_module()
時其實是呼叫 initfn
)。sys_init_module()
。其實這邊接不太起來,沒有看到哪邊執行這個系統呼叫的。以下為此系統呼叫的程式碼:
開頭看到的巨集 SYSCALL_DEFINE3
在這段程式碼可以把它替換成 asmlinkage long sys_init_module(void __user *umod, unsigned long len, const char __user *uargs);
,中間會包那麼多層巨集主要是因為 CVE-2009-2009,這漏洞簡單來說就是系統呼叫時假如有32位元參數要放到64位元暫存器來做傳遞的話,programmer 要手動做 sign extension (ABI 規定的),假如沒做的話,當 MSB 為1時就有可能拿到非法地址。may_init_module()
),通過驗證後模組(elf)將被複製到 kernel memory (copy_module_from_user()
),接下來 kernel 將驗證此 elf、完成 symbol resolution…(load_module()
),load_module()
的最後一步 (回傳前),會呼叫 do_init_module()
做模組啟動的相關作業,至此模組已完成載入並啟動。另外 insmod
用的是 finit_module()
系統呼叫,與 init_module()
做的事情雷同,只是 finit_module()
是用 file descriptor 來載入模組,而 init_module()
用的是帶入參數中的 buffer 來載入。當我們透過 insmod
去載入一個核心模組時,為何 module_init
所設定的函式得以執行呢?Linux 核心做了什麼事呢?
試著執行 $ readelf -a fibdrv.ko
, 觀察裡頭的資訊和原始程式碼及 modinfo
的關聯,搭配上述提問,解釋像 fibdrv.ko
這樣的 ELF 執行檔案是如何「植入」到 Linux 核心
這個 fibdrv
名稱取自 Fibonacci driver 的簡稱,儘管在這裡顯然是為了展示和教學用途而存在,但針對若干關鍵的應用場景,特別去撰寫 Linux 核心模組,仍有其意義,請找出 Linux 核心的案例並解讀。提示: 可參閱 Random numbers from CPU execution time jitter
查閱 ktime 相關的 API,並找出使用案例 (需要有核心模組和簡化的程式碼來解說)
clock_gettime 和 High Resolution TImers (HRT) 的關聯為何?請參閱 POSIX 文件並搭配程式碼解說
fibdrv
如何透過 Linux Virtual File System 介面,讓計算出來的 Fibonacci 數列得以讓 userspace (使用者層級) 程式 (本例就是 client.c
程式) 得以存取呢?解釋原理,並撰寫或找出相似的 Linux 核心模組範例
注意到 fibdrv.c
存在著 DEFINE_MUTEX
, mutex_trylock
, mutex_init
, mutex_unlock
, mutex_destroy
等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題
許多現代處理器提供了 clz / ctz 一類的指令,你知道如何透過演算法的調整,去加速 費氏數列 運算嗎?請列出關鍵程式碼並解說
client.c
中上數執行於下數後,應該是跟快取有關。此版本使用 tiny-bignum-c
作為大數運算的 API,此 API 基本上無最大輸入限制,只要提供適量記憶體(128 bytes 可表示約150位數的輸出)即可使用(根據使用的演算法而定,像是會用到 VLA 宣告 struct bn
的演算法就須注意。過多(kernel process stack 只有 16KB (x86_64))會導致 process 發生 stack overflow)。只是未優化的 fabonacci
效能實在勘憂。
下圖為大數運算與原始版本之效能分析圖,其中可以發現兩個版本進行 copy_to_user()
(分別為8 bytes 與128 bytes)的花費時間是幾乎相同的,而 fabonacci 計算效能的部份我們可以看到兩者差距之大,同樣都是使用 DP,大數運算版本的時間複雜度是 O(n),原始版本的時間複雜度則為 O(logn)。
已知問題
bn_kernel_space.h
中的 bn.array
大小大於128 bytes 時(目前僅測試256 bytes)會造成 kernel panic。
找到問題了,是我的資料結構大到讓 kernel space 的 process 發生 stack overflow (kernel space 的 stack 只有 8KB。在 2.6.37
前,有些 kernel 只有 4KB 的 stack,這樣有好也有壞,但壞大於好,所以最後全部改用 8KB 的 stack,詳見 Kernel Small Stacks)。話說 kdump 剛開始開的時候都捕捉不到 kernel panic,推測是我在設定的時候自己手動去調到 kernel image 的載入 cmd,而污染到其他檔案(因為有試過改回去,但問題依舊),後來想說都 panic 了,原本開著的東西都被關了,就順便升級一下(4.15.0-46-generic
-> 4.15.0-45-generic
),結果問題就解決了…,以下是 kernel 被 kexec
替換前最後一口氣(其中 fibonacci 的訊息是 debug 時插入的):
以下是 module 中出問題的程式碼,"<3>"
是多加的,原本以為要這樣設 log level 才能在 dmesg 中有發言權,但用錯方法了。其中大家可以發現到 panic 是發生在第117行之後,所以可以推測在配置 local variable 時讓 stack overflow 了。但奇怪的地方是假如 stack 只有 8KB,那我在 fibonacci input 輸入110時也應該會 panic 阿(一個 struct bn 大小為 128bytes,假設參數 k 為110,則共有 110 + 2 + 1
個 struct bn,總計使用記憶體為 113 * 128bytes = 14464bytes
,這明顯已經超過 8KB 了,思考原因中…)。
Wed, Mar 20, 2019 9:15 PM 目前有找到 gcc 的靜態分析功能,
-Wframe-larger-than=8192
。這個 option 可以監測靜態的 stack 大小,目前設定8192再配上宣告112個 struct bn,gcc 確實有吐出警告。這樣就可以猜測目前 kernel space 的 stack size 大於 8KB,或許我應該試試在執行時期吐出 stack size 到 dmseg。
Thu, Mar 21, 2019 11:23 PM 找到問題了…,x86_64 的 kernel process stack 是兩個 page (16KB),所以當我使用了 14464bytes 時,其實還有剩下 1.5KB 左右,而 fibonacci 輸入為120時,使用的記憶體大小至少是 15744bytes,這數字已經 stack frame 的邊界,而且 stack frame 中不只有 stack 而已,還有 thread_struct 等等元件,所以會爆掉不太意外…,目前想到的解法就是換演算法了。
還以為是區域變數
fib
沒有初始化而造成算到後面出現誤差,結果白高興了,問題依舊,而且假如是這原因的話那誤差應該不會很規律的發生在 80 左右,還有 0~70 這區間應該也會出問題才對..
Sun, Mar 31, 2019 5:32 PM
tiny-bignum-c
出問題的點。套用 tiny-bignum-c
的 fibonacci 演算法
不管使用的演算法是 DP 還是 fast doubling,只要輸入超過80左右後輸出會與正確輸出產生指數成長的差距,試過把 tiny-bignum-c
使用的資料結構加大與研究其 API 都還沒發現問題點…
read()
系統呼叫
模組中有用到這個系統呼叫,這個系統呼叫的第三個參數 size_t count
代表欲讀取的大小(單位為 byte)。但是 client 呼叫 read()
時回傳的結果往往大於呼叫時傳入的大小(1 byte),這已經不符合 read()
在 manpage 裡面的說明。
打問題的時候想到解答…,對模組的 fd 的
read()
已經被包裝成fib_read()
,也就是它不用照規定走,只要 caller 跟 calle 互相有協調好就好。這邊說錯了,只有內部實做(將
read()
syscall 實做成 fabonacci 數列運算)才能自己定義,回傳型態需要照原本 syscall 的走。譬如說read()
回傳型態為ssize_t
,那就不能使用ssize_t
以為的資料型態。原本想直接回傳一個自定義 struct,裡面包含計算時間跟計算結果,看來除非是自己做 syscall,不然就只能乖乖直接從 kernel space 丟出相關數據或對回傳值動手腳了。
Kernel Module
main()
開始執行。kernel module 在初始化的時候會註冊他將 handle 的 request (request 類型定義於 module code 中),之後只會在收到特定的 request 時才有事做,類似 event-driven 的方式。printf()
printk()
來輸出 user space 可以看到的資訊。巨集 __stringify(x...)
此巨集用途為將輸入替換為字串輸出。此巨集有個夥伴名為 _stringify_1(x...)
,這種巨集叫做 two levels macro,他可以接受輸入參數同樣也是 macro,如此就可以直接將字串 macro 當做輸入參數,而不使 macro 名稱直接轉為字串。
GCC aligned()
指定空間配置時的最小單位。e.g. 使用 int x __attribute__ ((aligned (16))) = 0;
宣告一 int
變數,那麼這個變數大小為 16 bytes。增加 boundary 可以改善 copy 時的效能(編譯器會使用其他複製 instruction)。值得注意的是此 attribute 只能增加不能減少,e.g. int
定義為 4 bytes,那你就不能 align 小於 4 bytes。若真有需要,請使用另個 attribute,packed
。
pointer of array, array of pointers 宣告方式
module_exit()
Kernel 的內建模組會直接忽略此呼叫,因為編進核心的模組規定是不會被卸載的。
copy_to_user()
時間開銷 (TODO)
根據此研究,從 kernel space 複製資料到 user space 的花費時間為 22ns/byte,所以在做效能分析時假如有包含到此函數的時間開銷,須注意資料大小成長後造成的量測誤差。另外根據不同架構、kernel 版本等等環境因素,可能會有不一樣的花費時間,因此此時間開銷待測試。
fopen()
and open()
linux2019