# 2024q1 Homework6 (integration) contributed by < `steven523` > ```shell $ gcc --version gcc (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0 Copyright (C) 2022 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 12 On-line CPU(s) list: 0-11 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz CPU family: 6 Model: 165 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 2 CPU max MHz: 5000.0000 CPU min MHz: 800.0000 BogoMIPS: 5199.98 Caches (sum of all): L1d: 192 KiB (6 instances) L1i: 192 KiB (6 instances) L2: 1.5 MiB (6 instances) L3: 12 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-11 ``` ## 自我檢查清單 - [x] 研讀前述 ==Linux 效能分析== 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 $\to$ 從中也該理解為何不希望在虛擬機器中進行實驗; 檢查 Linux 核心版本是否大於等於 5.15.0 ```shell $ uname -r 6.5.0-26-generic ``` 確認 linux-headers 套件已正確安裝於開發環境 ```shell $ dpkg -L linux-headers-6.5.0-26-generic | grep "/lib/modules" /lib/modules /lib/modules/6.5.0-26-generic /lib/modules/6.5.0-26-generic/build ``` - [x] 閱讀〈[Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module)〉並對照 Linux 核心原始程式碼 (v6.1+),解釋 `insmod` 後,Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到 (使用 List API)、`MODULE_LICENSE` 巨集指定的授權條款又對核心有什麼影響 (GPL 與否對於可用的符號列表有關),以及藉由 [strace](https://man7.org/linux/man-pages/man1/strace.1.html) 追蹤 Linux 核心的掛載,涉及哪些系統呼叫和子系統? ### 解釋 `insmod` insmod 是一個用於將模組掛載到 Linux 核心中的命令。Linux 有許多功能是通過模組的方式,在需要時才載入核心。這樣可以使核心整體較精簡,從而提高效率。 **練習第一個 Linux 核心模組的撰寫、掛載及卸載** 建立 hello 目錄並在其中建立 Makefile 及 hello.c 後,使用以下命令編譯核心模組: ```shell $ make -C /lib/modules/`uname -r`/build M=`pwd` modules ``` 掛載核心模組並查看核心訊息: ```shell $ sudo insmod hello.ko $ sudo dmesg [ 7850.090728] Hello, world ``` 卸載核心模組並查看核心訊息: ```shell $ sudo rmmod hello $ sudo dmesg [ 7850.090728] Hello, world [ 8500.208912] Goodbye, cruel world ``` **`fibdrv`: 可輸出 Fibonacci 數列的 Linux 核心模組** ### 新建立的裝置檔案 /dev/fibonacci,注意到 236 這個數字,在你的電腦也許會有出入。試著對照 fibdrv.c,找尋彼此的關聯。 ```shell $ cat /sys/class/fibonacci/fibonacci/dev 511:0 ``` 通過查看 `/sys/class/fibonacci/fibonacci/dev` 和 `/dev/fibonacci` 來瞭解裝置的主次編號,而這兩個數字是由系統在註冊裝置時動態分配的,並且可能會因系統而異。 在 `fibdrv.c` 中,主編號是在呼叫 [`register_chrdev`](https://linux.die.net/lkmpg/x569.html#:~:text=4.1.3.%20Registering%20A%20Device) 函式時指定的。如果 `register_chrdev()` 的第一個參數為 0,系統會自動分配一個可用的主編號。如果是一個正整數,則系統會嘗試使用這個指定的主編號。 以下是 [`fibdrv.c`](https://github.com/sysprog21/fibdrv/blob/master/fibdrv.c) 中的 `init_fib_dev()`: ```cpp static int major = 0, minor = 0; static int __init init_fib_dev(void) { int rc = 0; mutex_init(&fib_mutex); // Let's register the device // This will dynamically allocate the major number rc = major = register_chrdev(major, DEV_FIBONACCI_NAME, &fib_fops); if (rc < 0) { printk(KERN_ALERT "Failed to add cdev\n"); rc = -2; goto failed_cdev; } fib_dev = MKDEV(major, minor); . . . } ``` `register_chrdev` 函式被用來註冊一個新的 Character Device,並將傳回的主編號指派給 `major` 變數。這就是為什麼看到的主編號可能是 511 或 236,兩個數字都是系統自動分配的,具體的值取決於系統在註冊裝置時可用的主編號。 不過 `register_chrdev()` 這個 function 已經算是較舊版的 api,他存在著以下缺點: 1. 會佔用主設備號下的所有次設備號,浪費了很多次設備號 2. 必須手動指定主設備號,也就是說你得先手動查詢哪個主設備號還沒被佔用,然後再去指定 所以對於撰寫 Character Device,建議使用像 [ksort/sort_mod.c](https://github.com/sysprog21/ksort/blob/master/sort_mod.c) 裡的 `alloc_chrdev_region()` 和 `register_chrdev_region()` 這兩個新版的 api,較具靈活性。 **Linux 核心模組掛載機制** ### Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到 (使用 List API) 從 [kernel/module/main.c](https://github.com/torvalds/linux/blob/master/kernel/module/main.c) 中可以看到 `load_module`,它是負責載入模組的主要函式,它會呼叫 `simplify_symbols` 來解析模組中的符號。 在 `simplify_symbols` 會根據符號的類型來進行不同的處理,對於未定義的符號(`SHN_UNDEF`)會呼叫 `resolve_symbol_wait`,然後再呼叫 `resolve_symbol` 。 最後在 `resolve_symbol` 呼叫 `find_symbol` 並使用 List API 的 `list_for_each_entry_rcu` 函式來逐一走訪核心的符號表,尋找與模組中未定義的符號名稱相匹配的符號,如果找到則將該符號的地址填充到模組的符號表中,這樣模組就可以在運行時訪問到這些符號。 ### MODULE_LICENSE 巨集指定的授權條款對核心有什麼影響 (GPL 與否對於可用的符號列表有關) 掛載核心模組時需要 `MODULE_LICENSE()` 巨集,用於指定模組的授權條款,它告訴使用者和開發者該模組的授權方式,以及他們可以如何使用和修改這個模組。 以下表格是 [Linux kernel licensing rules]() 裡提供的 `MODULE_LICENSE()` valid license strings ![image](https://hackmd.io/_uploads/Sy31OKUz0.png =80%x) * 在 [`fibdrv.c`](https://github.com/sysprog21/fibdrv/blob/master/fibdrv.c) 中使用到的 `MODULE_LICENSE("Dual MIT/GPL")`,表達這個模組採用雙重許可,能在 GPL v2 變體或 MIT 許可證中進行選擇。 * 如果模組的授權條款不與 GPL 相容,例如表中的 "Proprietary",該模組將無法看到或使用核心通過 `EXPORT_SYMBOL_GPL()` 巨集導出的服務或函式,因為 `EXPORT_SYMBOL_GPL()` 只向 GPL 相容的模組顯示符號。 * 此外,如果模組沒有使用 `MODULE_LICENSE("GPL")`,那這個核心將被視為 "tainted" (受污染)。意思是核心已經載入了非自由軟體的模組,可能無法得到維護 ### 藉由 strace 追蹤 Linux 核心的掛載,涉及哪些系統呼叫和子系統 ```shell $ sudo strace insmod fibdrv.ko execve("/usr/sbin/insmod", ["insmod", "fibdrv.ko"], 0x7ffd9e472358 /* 26 vars */) = 0 brk(NULL) = 0x645546ea2000 arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc2b466b40) = -1 EINVAL (Invalid argument) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7d7b50d26000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=69755, ...}, AT_EMPTY_PATH) = 0 mmap(NULL, 69755, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7d7b50d14000 close(3) . . . close(3) = 0 getcwd("/home/steven/linux2024/Hw6/fibdrv", 4096) = 34 newfstatat(AT_FDCWD, "/home/steven/linux2024/Hw6/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0664, st_size=274808, ...}, 0) = 0 openat(AT_FDCWD, "/home/steven/linux2024/Hw6/fibdrv/fibdrv.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|0664, st_size=274808, ...}, AT_EMPTY_PATH) = 0 mmap(NULL, 274808, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7d7b50bd4000 finit_module(3, "", 0) = -1 EEXIST (File exists) write(2, "insmod: ERROR: could not insert "..., 62insmod: ERROR: could not insert module fibdrv.ko: File exists ) = 62 munmap(0x7d7b50bd4000, 274808) = 0 close(3) = 0 exit_group(1) = ? ``` 在以上結果中可以看到: * `execve`:用於執行 `insmod` 指令 * `openat`、`read`、`close`、`finit_module`,用於開啟、讀取、關閉模組檔案以及載入模組 * `mmap`、`munmap`,用於記憶體映射和解除記憶體映射 * `brk`、`arch_prctl`、`set_tid_address`、`exit_group`,用於管理程式的堆疊空間、設定執行緒的狀態和 ID 位址、退出程式 * `prlimit64` 用於設定行程的資源量 * `getrandom` 系統呼叫用來獲取隨機數 透過不同的系統呼叫來執行不同的功能,就能夠構建 Linux 核心的相關操作。 至於 `finit_module(3, "", 0) = -1 EEXIST (File exists)`,代表先前已經有掛載過該模組,所以掛載失敗,如果是第一次掛載的話結果會顯示 `0`。 ### 請參閱 GCC 手冊,得知 __attribute__((alias( ..))) 的用法 >The alias attribute causes the declaration to be emitted as an alias for another symbol, which must have been previously declared with the same type, and for variables, also the same size and alignment. Declaring an alias with a different type than the target is undefined and may be diagnosed. 意思是說,透過 `alias` 這個屬性將宣告作為另一個符號的別名發出,該符號之前必須使用相同的類型進行宣告,對於變量也必須具有相同的大小和對齊方式。 ```cpp #define module_init(initfn) \ static inline initcall_t __maybe_unused __inittest(void) \ { return initfn; } \ int init_module(void) __attribute__((alias(#initfn))); ``` 最後一行可以看到 gcc 會在編譯過後將 `initfn` 設為 `int init_module(void)` 的別名,以下範例: ```cpp #include <linux/init.h> #include <linux/module.h> static int hello(void) { printk(KERN_INFO "Hello, world!\n"); return 0; } module_init(hello); ``` 可以看到最後一行會利用 `module_init` 巨集將 `hello` 函式指定為模組初始化函式。 所以當我們掛載這個模組時,就會呼叫 `hello` 這個函式來執行模組的初始化,並輸出 "Hello, world!"。