# Rust for Linux 研究 - 成功建構 [Rust for linux](https://github.com/Rust-for-Linux) 專案 - 成功在 Linux 中以 busybox 掛載由 rust 撰寫的核心模組 ## 安全性 ### Use After Free ```c char *ptr = malloc(SIZE); if (err) { abrt = 1; free(ptr); /* Free ptr here */ } ... if (abrt) { logError("operation aborted before commit", ptr); /* Use ptr after free */ } ``` ### Memory Leak ```c /* First allocated 16 bytes are not freed */ int *data = (int*) malloc(sizeof(int) * 4); data = (int*) malloc(sizeof(int) * 4); free(data); ``` ### Double Free ```c int *data = (int*) malloc(sizeof(int) * 4); free(data); free(data);/* Free the data twice */ ``` ### 關於 Rust 中所有權概念 (Ownership) 以下為所有權的規則 1. Rust 中每一個物件都擁有一個擁有者 (owner),每一個物件只能被綁定到一個變數上,這時候我們會稱該變數擁有該物件的所有權,或是該變數為該物件的擁有者 2. 一個物件在同一時間只能被一個擁有者擁有 3. 當擁有者離開作用域 (scope) 時,物件就會被擁有者丟棄 ### 解決 Use after free ```rust let a = Box::new(10); std::mem::drop(a); println!("{}", *a); ``` output ``` Line 4, Char 16: borrow of moved value: `a` (solution.rs) | 2 | let a = Box::new(10); | - move occurs because `a` has type `Box<i32>`, which does not implement the `Copy` trait 3 | std::mem::drop(a); | - value moved here 4 | println!("{}", *a); | ^^ value borrowed here after move ``` 這裡通過 `std::mem::drop` 讓 `a` 離開目前的作用域,這時候 `a` 在 Heap 上所擁有的記憶體空間被記憶體分配器回收,接著值被銷毀。 這時候我們後面要反參考去存取 `a` 的值,便會得到錯誤,原因為 `a` 在 drop 時被 `move` 了,這時候發生了所有權轉移,`a` 這時候已經不再是 `Box::new(10)` 的擁有者,喪失了 `Box::new(10)` 的所有權。 再後面我們試圖去反參考 `a`,也就是試著進行借用,由於 `a` 在上一行已經 `move`,因此發生錯誤。 ### 解決 Double free ```rust let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); ``` output: ``` Line 5, Char 24: borrow of moved value: `s1` (solution.rs) | 2 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait 3 | let s2 = s1; | -- value moved here 4 | 5 | println!("{}, world!", s1); | ^^ value borrowed here after move ``` 在這個例子中,s2 和 s1 都指向了相同的資料,如果 s1 和 s2 同時離開作用域,則 drop 會嘗試釋放相同的記憶體 (因為 s2 和 s1 指向到同一塊記憶體),這時候便會產生 Double free 的情況,在上面可以看到這個錯誤在編譯期間被 Rust 編譯器指出。 為了保障記憶體安全,在 `let s2 = s1;` 後,s1 便無效,這裡所謂的無效是指 s1 便不再指向到資料存在的記憶體地址,資料的記憶體地址被 s2 所指向,因此下面 `println!("{}, world!", s1);` 無法執行,原因為這是一個無效的引用,引用關係已經被解除。 對於 `let s2 = s1`,s2 指向到 s1 所指向的資料,之後 s1 便不再指向到 s2 所指向的資料,這個動作稱為 move,s1 被 move 到 s2。 ### C 語言範例,列舉出常見的記憶體錯誤 以下為使用 C 語言實現一個可變長度的陣列 (或是稱為 Vector),在這個實做中包含了 7 個記憶體錯誤,以下將列舉出這一些錯誤。 ```c #include <stdio.h> #include <stdlib.h> #include <assert.h> // There are at least 7 bugs relating to memory on this snippet. // Find them all! // Vec is short for "vector", a common term for a resizable array. // For simplicity, our vector type can only hold ints. typedef struct { int* data; // Pointer to our array on the heap int length; // How many elements are in our array int capacity; // How many elements our array can hold } Vec; Vec* vec_new() { Vec vec; vec.data = NULL; vec.length = 0; vec.capacity = 0; return &vec; } void vec_push(Vec* vec, int n) { if (vec->length == vec->capacity) { int new_capacity = vec->capacity * 2; int* new_data = (int*) malloc(new_capacity); assert(new_data != NULL); for (int i = 0; i < vec->length; ++i) { new_data[i] = vec->data[i]; } vec->data = new_data; vec->capacity = new_capacity; } vec->data[vec->length] = n; ++vec->length; } void vec_free(Vec* vec) { free(vec); free(vec->data); } int main() { Vec* vec = vec_new(); vec_push(vec, 107); int* n = &vec->data[0]; vec_push(vec, 110); printf("%d\n", *n); free(vec->data); vec_free(vec); return 0; } ``` 1. `vec_new`: `Vec vec` 分配在 `vec_new` 的 stack frame 上,接著我們會回傳 `Vec vec` 在 stack 上的記憶體位置,也就是指向該結構的指標。但是在 `vec_new` 結束時,stack frame 也會跟著釋放,而指向到 `Vec vec` 記憶體區域的指標變數將不再有效,這時候便產生了懸置指標 (dangling pointer)。 2. `vec_new`: 我們將 `Vec` 中成員 `capacity` 初始化成 0,這會在我們接下來呼叫 `vec_push` 時產生問題,當 `vec_push` 被呼叫時,會將 `capacity` 放大兩倍,如果初始化為 0 將會導致沒有額外的記憶體空間被分配,正確的初始化行為應該將 `capacity` 初始化成 1。 3. `vec_push`: 錯誤的呼叫 `malloc` 分配記憶體間,`malloc` 的參數為所需要分配的記憶體位元數,正確的作法為 `malloc(sizeof(int) * new_capacity)`。 4. `vec_push`: 當我們重新分配記憶體大小時,我們沒有將 `vec->data` 原先的指標所指向的區域進行記憶體釋放 (`free`),這會導致記憶體洩漏 (memory leak)。 5. `vec_free`: 錯誤 `free` 呼叫的順序,當我們釋放 `vec` 的記憶體位置,`vec->data` 指標將不再有效,這將會有記憶體洩漏 (memory leak) 的問題,正確的作法為先將 `vec->data` 指向的記憶體區域進行釋放,接著將 `vec` 指向的記憶體區域釋放。 6. `main`: 在 `main` 函式中我們將 `vec->data` 所指向的記憶體空間進行釋放,接著我們呼叫了 `vec_free`,會將 `vec->data` 再次進行釋放,這會有 double free 的問題。 7. `main`: `n` 指向到 Vector 的第一個元素,接著我們呼叫了 `vec_push` 將新的元素放入到 Vector 中,但是在 `vec_push` 中將會重新調整 Vector 的大小,這時候 `n` 可能指向到的就不再是 Vector 的第一個元素,`n` 變成了懸置指標 (dangling pointer),接著我們在通過 `printf` 進行反參考就會是一個危險的行為了,這樣的問題又稱為無效的迭代器使用 (iterator invalidation)。 以上這個 C 語言程式雖然有許多關於記憶體的錯誤,但是卻能夠成功編譯,我們嘗試使用以下指令進行編譯 ```shell $ gcc test.c -Wall -o test ``` 產生以下輸出 ``` test.c: In function ‘vec_new’: test.c:21:10: warning: function returns address of local variable [-Wreturn-local-addr] 21 | return &vec; | ^~~~ ``` 加上 enable all warning 的選項,僅發現了錯誤 1。 我們試著使用 cppcheck 這個靜態記憶體分析工具對這個程式進行分析 ```shell $ cppcheck --enable=all --suppress=missingIncludeSystem test.c ``` 產生以下輸出 ``` Checking test.c ... test.c:21:10: error: Returning pointer to local variable 'vec' that will be invalid when returning. [returnDanglingLifetime] return &vec; ^ test.c:21:10: note: Address of variable taken here. return &vec; ^ test.c:17:7: note: Variable created here. Vec vec; ^ test.c:21:10: note: Returning pointer to local variable 'vec' that will be invalid when returning. return &vec; ^ ``` 也是發現到錯誤 1。 上面我們看到在 C 語言實做的 Vector 中存在著許多不當的記憶體管理錯誤,在 C 語言中我們需要手動的管理這一些記憶體,但隨之而來也伴隨許多錯誤發生。 在除了 C, C++, Rust 程式語言中如 Java 等等具備自動管理記憶體的功能,能夠自動回收記憶體,也就是所謂垃圾回收 (garbade collector, aka GC),可以在花費一定的執行時間開銷下,換取更加安全的程式,但是垃圾回收並不是萬靈丹,在某一些場景下垃圾回收可能會有一些問題。 - 對於具有垃圾回收的語言,使用記憶體具有很大的限制,但是對於一些只有 64 KB 等大小的嵌入式裝置,我們更偏好於手動管理記憶體已達到最佳效率的記憶體使用 - 如果程式對於實時 (real time) 有所要求,在具有垃圾回收的語言會涉及到某些停止所有程式運作的開銷,執行到程式的某一個部份,突然停止所有運作,讓垃圾回收偵測記憶體並且做出相對應的處理。[延伸閱讀: why-discord-is-switching-from-go-to-rust](https://discord.com/blog/why-discord-is-switching-from-go-to-rust) - 存在不只一個垃圾回收器的情況 如果我們能夠做到自動回收記憶體且是靜態執行的,不需要停止所有程式的開銷,一個在編譯時自動確認所有記憶體的分配以及釋放的語言,那該有多好?而這個概念正是在上方提及的 Rust 所有權概念。 ### C2Rust,一個用於將 C 語言遷移到 Rust 的工具 C2Rust 可以將符合 C99 規範的 C 語言使用 `c2rust transpile` 翻譯成等效的 Rust 語言實做,這個工具主要目的為保留原本程式功能,因此在產生出的 Rust 中會包含許多 `unsafe` 標籤,後續仍需要將 unsafe Rust 遷移成 safe Rust。 下圖為 C2Rust 工具的概念視圖 ![](https://hackmd.io/_uploads/rJU1Rkdo3.png) [ref](https://github.com/immunant/c2rust/blob/master/docs/c2rust-overview.png) 以下為嘗試將上方 Vector 的 C 語言實做通過 C2Rust 翻譯成 Rust。 根據官方說明文件,我們可以知道 C2Rust 依賴於 `compile_commands.json` 這個檔案,這個檔案會記錄我們編譯一個 C 語言原始碼時所傳遞的參數,這邊使用 python 的 intercept-build 工具來產生 `compile_commands.json`。 安裝 intercept-build ```shell $ pip install scan-build ``` 使用 intercept-build 產生 `compile_commands.json` ```shell $ intercept-build sh -c "gcc test.c -o test" ``` 以下為產生出的 `compile_commands.json` 內容 ```json [ { "arguments": [ "cc", "-c", "-o", "test", "test.c" ], "directory": "/home/ubuntu/Desktop/workspace/rust/rust_vs_c", "file": "test.c" } ] ``` 接著使用 C2Rust 產生出 `test.c` 對應的 Rust 實做 ```shell $ c2rust transpile compile_commands.json ``` 得到以下 `test.rs` ```rust #![allow(dead_code, mutable_transmutes, non_camel_case_types, non_snake_case, non_upper_case_globals, unused_assignments, unused_mut)] #![feature(label_break_value)] extern "C" { fn printf(_: *const libc::c_char, _: ...) -> libc::c_int; fn malloc(_: libc::c_ulong) -> *mut libc::c_void; fn free(_: *mut libc::c_void); fn __assert_fail( __assertion: *const libc::c_char, __file: *const libc::c_char, __line: libc::c_uint, __function: *const libc::c_char, ) -> !; } #[derive(Copy, Clone)] #[repr(C)] pub struct Vec_0 { pub data: *mut libc::c_int, pub length: libc::c_int, pub capacity: libc::c_int, } #[no_mangle] pub unsafe extern "C" fn vec_new() -> *mut Vec_0 { let mut vec: Vec_0 = Vec_0 { data: 0 as *mut libc::c_int, length: 0, capacity: 0, }; vec.data = 0 as *mut libc::c_int; vec.length = 0 as libc::c_int; vec.capacity = 0 as libc::c_int; return &mut vec; } #[no_mangle] pub unsafe extern "C" fn vec_push(mut vec: *mut Vec_0, mut n: libc::c_int) { if (*vec).length == (*vec).capacity { let mut new_capacity: libc::c_int = (*vec).capacity * 2 as libc::c_int; let mut new_data: *mut libc::c_int = malloc(new_capacity as libc::c_ulong) as *mut libc::c_int; if !new_data.is_null() {} else { __assert_fail( b"new_data != NULL\0" as *const u8 as *const libc::c_char, b"test.c\0" as *const u8 as *const libc::c_char, 28 as libc::c_int as libc::c_uint, (*::core::mem::transmute::< &[u8; 26], &[libc::c_char; 26], >(b"void vec_push(Vec *, int)\0")) .as_ptr(), ); } 'c_1586: { if !new_data.is_null() {} else { __assert_fail( b"new_data != NULL\0" as *const u8 as *const libc::c_char, b"test.c\0" as *const u8 as *const libc::c_char, 28 as libc::c_int as libc::c_uint, (*::core::mem::transmute::< &[u8; 26], &[libc::c_char; 26], >(b"void vec_push(Vec *, int)\0")) .as_ptr(), ); } }; let mut i: libc::c_int = 0 as libc::c_int; while i < (*vec).length { *new_data.offset(i as isize) = *((*vec).data).offset(i as isize); i += 1; i; } (*vec).data = new_data; (*vec).capacity = new_capacity; } *((*vec).data).offset((*vec).length as isize) = n; (*vec).length += 1; (*vec).length; } #[no_mangle] pub unsafe extern "C" fn vec_free(mut vec: *mut Vec_0) { free(vec as *mut libc::c_void); free((*vec).data as *mut libc::c_void); } unsafe fn main_0() -> libc::c_int { let mut vec: *mut Vec_0 = vec_new(); vec_push(vec, 107 as libc::c_int); let mut n: *mut libc::c_int = &mut *((*vec).data).offset(0 as libc::c_int as isize) as *mut libc::c_int; vec_push(vec, 110 as libc::c_int); printf(b"This is dereference of %d\n\0" as *const u8 as *const libc::c_char, *n); free((*vec).data as *mut libc::c_void); vec_free(vec); return 0; } pub fn main() { unsafe { ::std::process::exit(main_0() as i32) } } ``` 觀察翻譯出來的結果,可以看到使用到許多 libc 的 crate,以及許多 `unsafe` 標籤,翻譯出來的程式碼正如同官方宣稱的,需要將該程式碼遷移成 safe Rust 的實做,我們試著將上方的 `test.rs` 執行起來,我們需要在 `Cargo.toml` 中加入 libc 的依賴項,便能順利執行,以下為執行結果 ```shell $ cargo run Compiling c2rust_out v0.1.0 (/home/ubuntu/Desktop/workspace/rust/rust_vs_c/c2rust_out) warning: unused label --> src/main.rs:51:9 | 51 | 'c_1586: { | ^^^^^^^ | = note: `#[warn(unused_labels)]` on by default warning: the feature `label_break_value` has been stable since 1.65.0 and no longer requires an attribute to enable --> src/main.rs:2:12 | 2 | #![feature(label_break_value)] | ^^^^^^^^^^^^^^^^^ | = note: `#[warn(stable_features)]` on by default warning: path statement with no effect --> src/main.rs:69:13 | 69 | i; | ^^ | = note: `#[warn(path_statements)]` on by default warning: `c2rust_out` (bin "c2rust_out") generated 3 warnings Finished dev [unoptimized + debuginfo] target(s) in 0.15s Running `target/debug/c2rust_out` This is dereference of 107 free(): invalid pointer [1] 145992 IOT instruction (core dumped) cargo run ``` 可以看到執行結果正確的印出了 Vector 中第一個元素的值,但輸出也伴隨許多 warning 以及錯誤,這一些將需要在後期進行修正。 :::info 關於 Rust 中 `unsafe` 如果在 Rust 中進行以下行為,都應需要使用 `unsafe` 進行標注 1. 反參考一個裸指標 (raw pointer),如暫存器 2. 讀取或是寫入可變的全域變數 (static) 或是 external 變數 3. 讀取 C 語言風格中的 union 4. 呼叫已經標記為 `unsafe` 的函式 5. 實現的特徵 (trait) 本身被標記為 `unsafe` ::: ==TODO: 關於 C2Rust 的前身,Citrus 與 Corrode== ## Rust-for-Linux 首先,在本地端複製 `Rust-for-Linux` 的工作區: ```shell $ git clone https://github.com/Rust-for-Linux/linux.git ``` 接著檢視 `Documentation/rust/quick-start.rst` 完成環境配置 環境依賴於 `rustc`, `rust-src`, `rust-bindgen` 等等,首先安裝 `rustup` ```shell $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` `rustc` 的安裝 ```shell $ rustup override set $(scripts/min-tool-version.sh rustc) ``` 接著需要 `rust-src`,也就是 Rust 的標準函式庫程式碼 ```shell $ rustup component add rust-src ``` `bindgen` 使用以下進行安裝 ```shell $ cargo install --locked --version $(scripts/min-tool-version.sh bindgen) bindgen ``` 接著我們需要設定 `.config`,使用以下命令進行設定 (如果沒有正確設定,可能會遇到編譯 `.rs` 檔案時無法滿足 Linux 核心的相依性,如使用 `rust_echo_server` 時找不到 net) ```shell $ make LLVM=1 allnoconfig qemu-busybox-min.config rust.config ``` 接著安裝 busybox,使用 `git clone --depth=1 https://github.com/mirror/busybox.git` 將其 git clone 到 linux 目錄下 接著進行以下命令 ```shell $ make menuconfig ``` 並在 Settings 中,找到 Build Options 中的 Build static binary (no shared libs) 按下 `Y` 將其 include。 ![](https://i.imgur.com/SSf8K8G.png) 接著執行以下 ```shell $ make -j$(nproc) $ make install ``` 切換到 `_install` 目錄以下,建立 `ramdisk.img` ```shell $ find . | cpio -H newc -o | gzip > ../ramdisk.img ``` 接著編譯整個核心,我們依賴於 LLVM 工具鏈 (使用 clang 編譯器進行編譯),使用以下命令: ```shell $ make LLVM=1 -j$(nproc) ``` 在編譯完成之後,會產生 `vmlinux` 和 `bzImage`,及個別 Linux 核心模組,以下說明 - vmlinux: 編譯出來的核心 ELF 檔,沒有經過壓縮,該檔案不能用於 Linux 的系統啟動,但可用以定位核心中的錯誤,如使用 `gdb vmlinux` 進行除錯 - bzImage: 使用以下命令,以 qemu 啟動 Linux 核心 ```shell $ qemu-system-x86_64 -nographic \ -kernel arch/x86/boot/bzImage \ -initrd busybox/ramdisk.img -M pc -m 4G -cpu Cascadelake-Server \ -vga none -no-reboot -smp $(nproc) ``` 這邊解釋參數 (TODO) - kernel: ### Busybox ### Rust Hello World 下面我們將嘗試在 [Rust for linux](https://github.com/Rust-for-Linux) 中加入使用 Rust 撰寫的 Hello World 核心模組,在 `make menuconfig` 中,由 `Kernel hacking` $\to$ `Sample kernel code` $\to$ `Rust sample` 中我們可以看到有若干 Rust 撰寫的核心模組,這些核心模組位於 `sample/rust` 底下,要加入我們自己撰寫的 Hello World 核心模組。首先我們觀察 `Makefile` ``` obj-$(CONFIG_SAMPLE_RUST_MINIMAL) += rust_minimal.o obj-$(CONFIG_SAMPLE_RUST_PRINT) += rust_print.o obj-$(CONFIG_SAMPLE_RUST_MODULE_PARAMETERS) += rust_module_parameters.o obj-$(CONFIG_SAMPLE_RUST_SYNC) += rust_sync.o obj-$(CONFIG_SAMPLE_RUST_CHRDEV) += rust_chrdev.o obj-$(CONFIG_SAMPLE_RUST_MISCDEV) += rust_miscdev.o obj-$(CONFIG_SAMPLE_RUST_STACK_PROBING) += rust_stack_probing.o obj-$(CONFIG_SAMPLE_RUST_SEMAPHORE) += rust_semaphore.o obj-$(CONFIG_SAMPLE_RUST_SEMAPHORE_C) += rust_semaphore_c.o obj-$(CONFIG_SAMPLE_RUST_RANDOM) += rust_random.o obj-$(CONFIG_SAMPLE_RUST_PLATFORM) += rust_platform.o obj-$(CONFIG_SAMPLE_RUST_NETFILTER) += rust_netfilter.o obj-$(CONFIG_SAMPLE_RUST_ECHO_SERVER) += rust_echo_server.o obj-$(CONFIG_SAMPLE_RUST_FS) += rust_fs.o obj-$(CONFIG_SAMPLE_RUST_SELFTESTS) += rust_selftests.o ``` 觀察以上,我們可將 `rust_hello` 核心模組加入到編譯目標中 ```diff obj-$(CONFIG_SAMPLE_RUST_MINIMAL) += rust_minimal.o obj-$(CONFIG_SAMPLE_RUST_PRINT) += rust_print.o obj-$(CONFIG_SAMPLE_RUST_MODULE_PARAMETERS) += rust_module_parameters.o obj-$(CONFIG_SAMPLE_RUST_SYNC) += rust_sync.o obj-$(CONFIG_SAMPLE_RUST_CHRDEV) += rust_chrdev.o obj-$(CONFIG_SAMPLE_RUST_MISCDEV) += rust_miscdev.o obj-$(CONFIG_SAMPLE_RUST_STACK_PROBING) += rust_stack_probing.o obj-$(CONFIG_SAMPLE_RUST_SEMAPHORE) += rust_semaphore.o obj-$(CONFIG_SAMPLE_RUST_SEMAPHORE_C) += rust_semaphore_c.o obj-$(CONFIG_SAMPLE_RUST_RANDOM) += rust_random.o obj-$(CONFIG_SAMPLE_RUST_PLATFORM) += rust_platform.o obj-$(CONFIG_SAMPLE_RUST_NETFILTER) += rust_netfilter.o obj-$(CONFIG_SAMPLE_RUST_ECHO_SERVER) += rust_echo_server.o obj-$(CONFIG_SAMPLE_RUST_FS) += rust_fs.o obj-$(CONFIG_SAMPLE_RUST_SELFTESTS) += rust_selftests.o +obj-$(CONFIG_SAMPLE_RUST_HELLO) += rust_hello.o ``` 接著看到 `Kbuild`,在 `Kbuild` 檔案中看到的敘述,便是我們在 `make menuconfig` 中看到的選項,我們可以在 `menuconfig` 中選擇要編譯的目標,完成 `make menuconfig` 後,會產生出 `.config` 檔案,這個檔案描述 Linux 核心中需要編譯哪一些目標,如果我們將 `Rust sample -> Printing macros` 加入到編譯目標中,我們執行 `make LLVM=1 -j$(proc)` 後,啟動核心後便可以看到其相關輸出。 以下為在核心啟動過程中,`rust_print` 產生的輸出資訊 ``` ... [ 0.238533] usbhid: USB HID core driver [ 0.238835] rust_print: Rust printing macros sample (init) [ 0.239155] rust_print: Emergency message (level 0) without args [ 0.239504] rust_print: Alert message (level 1) without args [ 0.239827] rust_print: Critical message (level 2) without args [ 0.240165] rust_print: Error message (level 3) without args [ 0.240492] rust_print: Warning message (level 4) without args [ 0.240823] rust_print: Notice message (level 5) without args [ 0.241150] rust_print: Info message (level 6) without args [ 0.241473] rust_print: A line that is continued without args [ 0.241799] rust_print: Emergency message (level 0) with args [ 0.242139] rust_print: Alert message (level 1) with args [ 0.242456] rust_print: Critical message (level 2) with args [ 0.242785] rust_print: Error message (level 3) with args [ 0.243108] rust_print: Warning message (level 4) with args [ 0.243434] rust_print: Notice message (level 5) with args [ 0.243751] rust_print: Info message (level 6) with args [ 0.244056] rust_print: A line that is continued with args ``` 要將 Hello World核心模組加入到編譯目標中,我們需要修改 `Kbuild`,加入後 `Kbuild` 內容如下 ```diff config SAMPLE_RUST_HOSTPROGS bool "Host programs" help This option builds the Rust host program samples. If unsure, say N. config SAMPLE_RUST_SELFTESTS tristate "Self tests" help This option builds the self test cases for Rust. If unsure, say N. +config SAMPLE_RUST_HELLO + tristate "Rust Hello" + help + This option builds the Hello for Rust. + + If unsure, say N. ``` 接著是 `rust_hello.rs` 程式碼本身 ```rust //! Hello world example for Rust. use kernel::prelude::*; module! { type: RustHello, name: "rust_hello", author: "Rust for Linux Contributors", description: "Rust hello sample", license: "GPL v2", } struct RustHello { message: String, } impl kernel::Module for RustHello { fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> { pr_info!("Hello World From Rust Hello.\n"); pr_info!("Am I built-in? {}\n", !cfg!(MODULE)); Ok(RustHello { message: "on the heap!".try_to_owned()?, }) } } impl Drop for RustHello { fn drop(&mut self) { pr_info!("My message is {}\n", self.message); pr_info!("Rust hello sample (exit)\n"); } } ``` 接著再次執行 `make menuconfig`,將 `Rust Hello` 加入到編譯目標中,並且編譯核心,接著啟動核心,便可看到相關輸出 ``` [ 0.244386] rust_hello: Hello World From Rust Hello. [ 0.244671] rust_hello: Am I built-in? true [ 0.245056] Initializing XFRM netlink socket [ 0.245332] NET: Registered PF_INET6 protocol family [ 0.245786] Segment Routing with IPv6 [ 0.246011] In-situ OAM (IOAM) with IPv6 [ 0.246261] sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver [ 0.246689] NET: Registered PF_PACKET protocol family [ 0.247017] 9pnet: Installing 9P2000 support [ 0.247945] Key type dns_resolver registered [ 0.248266] IPI shorthand broadcast: enabled ``` ### [Virtme](https://hackmd.io/@sysprog/linux-virtme) virtme 是 Linux 核心開發者利用 QEMU 所建立一個輕量級的 Linux 核心測試環境,和 Linux 核心原始程式碼有很好的整合。 以下我們將利用 virtme 掛載我們使用 Rust 撰寫的 Hello World 核心模組。 以下安裝 virtme 流程 ```shell $ pip3 install --user \ git+https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git ``` 更新環境變數,讓我們能夠找到 `virtme-kernelconfig` 以及 `virtme-run` ```shell $ export PATH=$HOME/.local/bin:$PATH ``` 使用 virtme 選取預設核心組態 ```shell $ virtme-configkernel --defconfig ``` 設定完畢核心組態,我們需要加入 Rust 的相關設定,包含 `Rust support` 以及我們要載入的 `Rust sample` :::info 在完成以上設定並編譯,可能會產生以下錯誤 ```shell error: missing documentation for the crate --> samples/rust/rust_hello.rs:1:1 | 1 | / use kernel::prelude::*; 2 | | 3 | | module! { 4 | | type: RustHello, ... | 30 | | } 31 | | } | |_^ | = note: `-D missing-docs` implied by `-D warnings` error: aborting due to previous error ``` 只要在我們撰寫的 Rust 核心模組加入 `//!` 作為檔案註解即可解決 ```rust //! Hello world example for Rust. use kernel::prelude::*; module! { type: RustHello, ... ``` > 參見: [How can I write crate-wide documentation?](https://stackoverflow.com/questions/36184407/how-can-i-write-crate-wide-documentation) ::: 使用 virtme 的 kimg 參數啟動核心映像檔後,若我們想要使用 modprobe 載入與核心一同編譯的核心模組時,會因為與 Ubuntu Linux 共用檔案系統,而無法從預設路徑 `/lib/modules/$(uname -r)` 中讀取相關設定檔。可以透過以下的方式來進行設定: 我們需要將核心模組安裝到一個暫存目錄中 ```shell $ make modules_install INSTALL_MOD_PATH=/home/ubuntu/test-kmod ``` 接著我們在任一目錄建立名為 `hello_kernel_module_rust` 的子目錄,其目錄結構如下 ``` hello_kernel_module_rust ├── hello.rs ├── Kbuild └── Makefile ``` - [ ] `Makefile` ``` all: make -C /tmp/linux_rust LLVM=1 M=$(PWD) modules clean: make -C /tmp/linux_rust M=$(PWD) clean ``` - [ ] `Kbuild` ``` obj-m := hello.o ``` 執行完畢 `make` 後,預期看到以下輸出,並得到 `hello.ko` ```shell $ make make -C /tmp/linux_rust LLVM=1 M=/tmp//hello_kernel_module_rust modules make[1]: Entering directory '/tmp/linux_rust' RUSTC [M] /tmp/hello_kernel_module_rust/hello.o MODPOST /tmp/hello_kernel_module_rust/Module.symvers CC [M] /tmp/hello_kernel_module_rust/hello.mod.o LD [M] /tmp/hello_kernel_module_rust/hello.ko make[1]: Leaving directory '/tmp/linux_rust' ``` 我們可用 `modinfo` 檢視 `hello.ko` 資訊 ```shell $ modinfo hello.ko filename: hello.ko author: Rust for Linux Contributors description: Rust hello sample license: GPL v2 vermagic: 6.2.0-g12860deed1d6-dirty SMP preempt mod_unload name: hello retpoline: Y depends: ``` 接著我們將 `hello.ko` 複製到 `/tmp/test-kmod/lib/modules` 底下,這時候我們回到虛擬機器中,將放置核心模組的目錄掛載到 `/lib/modules` ```shell # mount --bind /home/ubuntu/test-kmod/lib/modules /lib/modules ``` 接著到虛擬機器中 `/lib/modules` 目錄底下,我們預期會看到 `hello.ko` 這個核心模組 ```shell root@(none):/lib/modules# ls 6.1.24 6.2.0-g12860deed1d6-dirty hello.ko ``` 接著嘗試掛載模組,得到以下輸出訊息 ```shell root@(none):/lib/modules# insmod hello.ko [ 6281.053467] rust_hello: Hello World From Rust Hello. [ 6281.053694] rust_hello: Am I built-in? false root@(none):/lib/modules# rmmod hello.ko [ 6286.071590] rust_hello: My message is on the heap! [ 6286.071807] rust_hello: Rust hello sample (exit) ``` 到這裡,我們完成在 host 端編譯完成核心模組,並且在 guest 中執行。 #### virtme 配合 busybox 使用 (通過 rcS 在 Linunx 啟動時將放置核心的目錄掛載到 `/lib/modules`) 在建構 Rust-for-linux 過程中我們使用到 busybox,並在 busybox 目錄底下建立 `ramdisk.img`,我們可用以下命令,將 Virtme 與 busybox 一同使用 ```shell $ virtme-run --kdir . --mods=auto --busybox busybox ``` TODO ### 在 C 語言撰寫的核心模組中呼叫 Rust 語言撰寫的函式庫 參考以下專案進行實做 [Rust-Kernel-Mod](https://github.com/blueOkiris/Rust-Kernel-Mod) 使用以下命令取得專案程式碼 ```shell $ git clone "https://github.com/blueOkiris/Rust-Kernel-Mod" ``` 取得專案程式碼後,觀察 `Makefile` 後得知專案根目錄底下的 `Makefile` 會相依於 `c/src` 中的 `Makefile`,將進入的內核目錄更改成 `Rust-for-linux` 的目錄 ```diff # Author: Dylan Turner # Description: # Sub-makefile for building the kernel module from the same dir as C files MODULE_NAME := hello_world obj-m := $(MODULE_NAME).o $(MODULE_NAME)-objs:= $(MODULE_NAME)_main.o $(RUST_LIB) EXTRA_CFLAGS := -I$(PWD)/../include .PHONY : all all : $(MODULE_NAME).ko .PHONY : clean clean : + $(MAKE) -C /tmp/linux_rust M=$(PWD) clean $(MODULE_NAME).ko : $(RUST_LIB) $(wildcard *.c) + $(MAKE) -C /tmp/linux_rust M=$(PWD) modules ``` 接著回到專案根目錄,使用以下命令進行編譯 ```shell $ make CC=clang ``` 發現出現以下錯誤 ``` ERROR: modpost: "printk" [/tmp/Rust-Kernel-Mod/c/src/hello_world.ko] undefined! make[3]: *** [scripts/Makefile.modpost:138: /tmp/Rust-Kernel-Mod/c/src/Module.symvers] Error 1 make[2]: *** [Makefile:1973: modpost] Error 2 make[2]: Leaving directory '/tmp/linux_rust' make[1]: *** [Makefile:19: hello_world.ko] Error 2 make[1]: Leaving directory '/tmp/Rust-Kernel-Mod/c/src' make: *** [Makefile:26: hello_world.ko] Error 2 ``` 錯誤為找不到 `printk` 定義,這邊進行修復,為了方便測試,我們將 `kernel.rs` 刪除,並根據 [A little Rust with your C](https://docs.rust-embedded.org/book/interoperability/rust-with-c.html) 將 `lib.rs` 修改如下 ```rust /* lib.rs */ #[no_mangle] pub extern "C" fn hello_from_rust(x: i32, y: i32) -> i32 { x + y } ``` 這裡修改了原先 `hello_from_rust` 的函式原型,因此我們需要修改 `c/include/hello_rust` ```c /* hello_rust.h */ int hello_from_rust(int x, int y); ``` 修改完成後,接著在核心模組主要程式碼中新增以下程式碼用於觀察 C 語言撰寫的核心模組是否成功呼叫 Rust 撰寫的函式庫 ```diff /* * Author: Dylan Turner * Description: Kernel Module entry point for "Hello, world!" kernel module */ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <hello_rust.h> MODULE_AUTHOR("Dylan Turner"); MODULE_DESCRIPTION("Hello World From Rust"); MODULE_LICENSE("GPL"); // On module insertion static int __init rust_loader_init(void) { pr_info("Loading code from Rust library!\n"); + pr_info("From Rust: hello_from_rust()\n"); return 0; } // On module removal static void __exit rust_loader_cleanup(void) { pr_info("Cleaning up Rust library code!\n"); } module_init(rust_loader_init); module_exit(rust_loader_cleanup); ``` 接著回到專案根目錄,使用以下命令編譯模組,將會得到 `hello_world.ko` 於專案根目錄中 ```shell $ make CC=clang ``` 使用 `modinfo` 檢視編譯完成的模組資訊 ```shell $ modinfo hello_world.ko filename: temp/Rust_Kernel_MOD/hello_world.ko author: Dylan Turner description: Hello World From Rust license: GPL vermagic: 6.2.0-g12860deed1d6-dirty SMP preempt mod_unload name: hello_world retpoline: Y depends ``` 接著使用 virtme 測試核心模組 (`#` 開頭表示位於虛擬機器測試環境中) ```shell root@(none):/lib/modules# insmod hello_world.ko [ 45.787316] Loading code from Rust library! [ 45.787827] From Rust: 3 root@(none):/lib/modules# rmmod hello_world.ko [ 55.027507] Cleaning up Rust library code! ``` 由上方輸出結果顯示,我們成功在 C 語言撰寫的核心模組中呼叫 Rust 撰寫的函式庫 TODO: 在 Rust 撰寫的函式庫中使用 `kernel::prelude::*` ### 在 Rust 語言撰寫的核心模組中呼叫 C 語言撰寫的函式庫 目標: 在 C 語言中撰寫 `hello_from_c` 函式,此函式接收一個參數 `message` 作為將被印出的訊息,`hello_from_c` 將會在 Rust 中被呼叫,同時在 Rust 中將參數傳入 `hello_from_c`。 以下為 `hello.c` 程式碼 ```c /* hello.c */ #include <linux/module.h> #include <linux/init.h> #include <linux/kernel.h> void hello_from_c(const char *message) { pr_info("Rust Send: %s, This msg From C", message); } ``` 以下為 Rust 核心模組程式碼,此程式碼與上方 Rust Hello World 核心模組程式碼相同 ```rust //! hello_world.rs use kernel::prelude::*; module! { type: RustHello, name: "rust_hello", author: "Rust for Linux Contributors", description: "Rust hello sample", license: "GPL v2", } struct RustHello { message: String, } impl kernel::Module for RustHello { fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> { pr_info!("Hello World From Rust Hello.\n"); pr_info!("Am I built-in? {}\n", !cfg!(MODULE)); Ok(RustHello { message: "on the heap!".try_to_owned()?, }) } } impl Drop for RustHello { fn drop(&mut self) { pr_info!("My message is {}\n", self.message); pr_info!("Rust hello sample (exit)\n"); } } ``` 在 [LKMPG 4.6 Modules Spanning Multiple Files](https://sysprog21.github.io/lkmpg/#modules-spanning-multiple-files) 中提及依賴於多個原始檔的內核模組 Makefile 的撰寫,根據書上的範例,我們將上方程式碼相依關係撰寫為 Kbuild 可以表示成以下 - [ ] `Kbuild` ``` obj-m := call_c_from_rust.o call_c_from_rust-objs += hello_world.o hello.o ``` 我們為 `hello_world.o` 和 `hello.o` 組合成的物件建立一個名稱,稱為 `call_c_from_rust`,接著我們告訴 `make` 哪一些目的檔為核心模組所構成的部份。 以下為 Makefile - [ ] `Makefile` ``` all: make -C /tmp/linux_rust LLVM=1 M=$(PWD) modules clean: make -C /tmp/linux_rust M=$(PWD) clean ``` 下面我們修改 `hello_world.rs` 的內容,參考 [A little C with your Rust](https://docs.rust-embedded.org/book/interoperability/c-with-rust.html),得知呼叫外部 C 語言函式,我們需要在 Rust 中定義 C 語言 ABI 的函式原型,如以下所示 ```rust extern "C" { pub fn cool_function( ... ); } ``` `cool_function` 的實做位於外部,可能為一個靜態函式庫或是二進位檔等等,在我們的例子中,實做部份位於 `hello.c` 中,這裡我們可以在 Rust 中將 `hello_from_c` 定義函式原型為以下 ```rust extern "C" { fn hello_from_c(message: ???); } ``` 在定義原型時有一個問題,我們要傳參數到 `hello_from_c` 中,這裡 message 的型別應該為何?直關上的作法為使用 `use std::ffi::CStr;`,但是在核心模組開發中我們不能使用 `std` 相關函式庫,這點可從 `#[no_std]` 標籤中得知,因此,我們應該在 [Crate kernel](https://rust-for-linux.github.io/docs/kernel/) 中找尋解決方案。 在查詢文件後,找到 [Macro kernel::c_str](https://rust-for-linux.github.io/docs/kernel/macro.c_str.html) 可以從一字串常數建立 `CStr` 型別,接著向下尋找,發現 [`CStr`](https://rust-for-linux.github.io/docs/kernel/str/struct.CStr.html) 為一結構,包含許多方法,其中 `as_char_ptr` 方法可以回傳一個 `*const c_char`,到這邊,我們有了在 Rust 中建立 `CStr` 的方法,以及如何將 `CStr` 轉換成 `*const c_char` 的方法,我們可以使用以下程式碼建構一字串常數並將該常數作為 `hello_from_c` 的參數呼叫 ```rust const C_MSG: &CStr = c_str!("Hello"); unsafe{hello_from_c(C_MSG.as_char_ptr())}; ``` 接著是 Rust 中函式原型的部份,`as_char_ptr` 回傳型別為 `*const c_char`,而 `c_char` 定義於 `core::ffi::c_char` 中,因此函式原型可以定義為以下 ```rust use core::ffi::c_char; extern "C" { fn hello_from_c(message: *const c_char); } ``` :::info 為什麼我們可以引用 [`core`](https://rust-for-linux.github.io/docs/core/index.html)? Rust 的 `core` 函式庫為 Rust 標準函式庫中,去除任何外部依賴 (dependency-free) 的版本,如 libc 或是一些系統函式庫等等,用於函式庫與程式語言之間的鏈接,並具有可移植性。 由於 `core` 不具有平台相依性以及不依賴於其他函式庫,因此我們可以引入他使用。 ::: 以下為完整 `hello_world.rs` 程式碼 ```rust //! hello use kernel::prelude::*; use core::ffi::c_char; use kernel::c_str; module! { type: RustHello, name: "rust_hello", author: "Rust for Linux Contributors", description: "Rust hello sample", license: "GPL v2", } struct RustHello { message: String, } extern "C" { fn hello_from_c(message: *const c_char); } impl kernel::Module for RustHello { fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> { const C_MSG: &CStr = c_str!("Hello"); pr_info!("Hello World From Rust Hello.\n"); pr_info!("Am I built-in? {}\n", !cfg!(MODULE)); unsafe{hello_from_c(C_MSG.as_char_ptr())}; Ok(RustHello { message: "on the heap!".try_to_owned()?, }) } } impl Drop for RustHello { fn drop(&mut self) { pr_info!("My message is {}\n", self.message); pr_info!("Rust hello sample (exit)\n"); } } ``` 使用 virtme 測試如下 (`#` 開頭表示於虛擬機器環境中) ```shell root@(none):/lib/modules# insmod call_c_from_rust.ko [13855.808153] rust_hello: Hello World From Rust Hello. [13855.808634] rust_hello: Am I built-in? false root@(none):/lib/modules# rmmod call_c_from_rust.ko [13855.809034] Rust Send: Hello, This msg From C [13860.136710] rust_hello: My message is on the heap! [13860.137640] rust_hello: Rust hello sample (exit) ``` TODO: 為什麼 `hello_from_rust` 在核心模組移除時才被呼叫並輸出結果 ### TODO: [Rust in the Linux kernel - DevConf.CZ 2020](https://www.youtube.com/watch?v=oacmnKlWZT8&t=32s&ab_channel=DevConf) ### Scull: C vs. Rust Scull (Simple Character Utility for Loading Localities),Scull 為 Character Driver,將一塊記憶體區塊當作是一個硬體設備進行讀寫。 Scull 只是操作一些由核心所分配的記憶體區塊,並不需要依賴於特定的硬體設備,這個特性方便我們編譯並執行。 ### 實作 Character Driver - 1. 指定 device number (major/ minor),這一步驟可以通過 `register_chrdev_region()`, `alloc_chrdev_region()` 或是 `register_chrdev()` 完成 - 2. 實作對應的檔案操作,如 `open`, `read`, `write`, `ioctl` 等等 - 3. 在 Linux 核心中註冊 Character driver,可以通過 `cdev_init()` 以及 `cdev_add()` 完成 ### 關於 Device Driver 當我們在 `/dev` 中輸入 `ls -l`,可能得到類似以下輸出 ``` brw-rw---- 1 root disk 259, 10 六 20 20:31 nvme1n1p4 brw-rw---- 1 root disk 259, 11 六 20 20:30 nvme1n1p5 crw------- 1 root root 10, 144 六 20 20:30 nvram crw-r----- 1 root kmem 1, 4 六 20 20:30 port crw-rw-rw- 1 root tty 5, 0 六 20 20:30 tty crw--w---- 1 root tty 4, 0 六 20 20:30 tty0 crw--w---- 1 root tty 4, 1 六 20 20:30 tty1 crw--w---- 1 root tty 4, 10 六 20 20:30 tty10 ``` 在上面的輸出,我們可以看到有兩個數字以逗號進行分割,如 `1, 4`,第一個數字表示為主編號,在上面有 1, 4, 5, 10, 259 這五個主編號。第二個數字為副編號,在上面有 0, 1, 4, 10, 11, 144,可以看到 `tty0`, `tty1`, `tty10` 主編號皆為 4,但副編號分別為 0, 1, 10。 主編號表示我們目前使用哪一個驅動程式來存取裝置,以上面的例子,我們可以看到 `tty0`, `tty1`, `tty10` 是由相同的驅動程式進行存取與控制的。 對於驅動程式而言,使用副編號來辨識我們目前使用哪一個裝置,以上面的例子為例,`tty0`, `tty1`, `tty10` 都是由同一個驅動程式所控制,而驅動程式通過副編號來辨識目前控制的是哪一個裝置。 主編號是讓核心知道我們是使用哪一個驅動程式去控制裝置,副編號是讓驅動程式知道我們控制的是哪一個裝置。 TODO: `fops` #### Scull: Register C version 以下為使用 C 語言實作 `chrdev_reg` 程式碼 ```c #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/printk.h> #include <linux/device.h> #define DRIVER_NAME "chrdev_reg" static unsigned int chrdev_reg_major = 0; static int chrdev_reg_open(struct inode *inode, struct file *file) { pr_info("Call chrdev_reg_open\n"); return 0; } static int chrdev_reg_close(struct inode *inode, struct file *file) { pr_info("Call chrdev_reg_close\n"); return 0; } struct file_operations fops = { .open = chrdev_reg_open, .release = chrdev_reg_close, }; static int chrdev_reg_init(void) { int major; major = register_chrdev(chrdev_reg_major, DRIVER_NAME, &fops); if ((chrdev_reg_major == 0 && major < 0) || (chrdev_reg_major > 0 && major != 0)) { pr_err("%s Driver registration error\n", DRIVER_NAME); return major; } else { chrdev_reg_major = major; } pr_info(KERN_ALERT "%s driver(major %d) installed.\n", DRIVER_NAME, chrdev_reg_major); return 0; } static void chrdev_reg_exit(void) { unregister_chrdev(chrdev_reg_major, DRIVER_NAME); pr_info("%s driver removed\n", DRIVER_NAME); } module_init(chrdev_reg_init); module_exit(chrdev_reg_exit); MODULE_LICENSE("GPL"); ``` 在 `chrdev_reg_init()` 中完成了的註冊,我們使用 `register_chrdev()` 進行註冊,`register_chrdev()` 接受三個參數,第一個參數可以分成為 0 的情況或是非 0 的情況 - `chrdev_reg_major == 0`: 表示動態註冊,動態的向 Kernel 得到一個主編號,接著 `register_chrdev()` 回傳大於 0 的值表示成功 - `chrdev_reg_major > 0`: 我們可以指定一個主編號向 Kernel 進行註冊,如果註冊成功,則 `register_chrdev()` 回傳 0 表示成功。 接著我們將 `chrdev_reg` 打包成 Kernel Object,並且通過 Virtme 進行測試,以下為 Makefile 以及 Kbuild 檔案內容 - [ ] `Makefile` ``` all: make -C /tmp/linux_rust LLVM=1 M=$(PWD) modules clean: make -C /tmp/linux_rust M=$(PWD) clean ``` - [ ] `Kbuild` ``` obj-m += chdev_reg.o ``` 將編譯完成的 `chrdev_reg.ko` 複製到 `/tmp/test-kmod/lib/modules` 底下,接著到虛擬機器中 `/lib/modules` 目錄底下,我們預期會看到 `chrdev_reg.ko` 這個核心模組 ```shell root@(none):/lib/modules# ls 6.1.24 6.2.0-g12860deed1d6-dirty chdev_reg.ko ``` 嘗試掛載模組,得到以下輸出訊息 ``` root@(none):/lib/modules# insmod chdev_reg.ko [ 44.305444] chrdev_reg driver(major 247) installed. ``` 接著我們到 `/dev` 目錄底下,我們會發現到我們並見到 `chrdev_chr` 裝置,原因為前面我們在進行的操作,為向核心掛載驅動程式,但是在 User space 中,我們沒有一個界面去對驅動程式進行存取,概念上如下圖所示 ==TODO: 重新製圖== [Understanding the Structure of a Linux Kernel Device Driver](https://www.youtube.com/watch?v=XoYkHUnmpQo&ab_channel=SergioPrado) ![](https://hackmd.io/_uploads/SJ-dTVgd3.png) 我們需要在 User space 這一端建立一個節點去存取這一些界面,概念上如下圖所示 ![](https://hackmd.io/_uploads/BkjspVed3.png) 使用 `mknod` 建立節點,使用節點對界面進行存取 ```shell # mknod /dev/chrdev_reg c 247 0 ``` `c` 表示建立的檔案節點為字元裝置,`247` 為主編號,`0` 為副編號,接著到 `/dev` 目錄底下預期得到裝置 `chrdev_reg` ```shell root@(none):/dev# ls -al | grep chrdev_reg crw-r--r-- 1 root root 247, 0 Jun 21 17:08 chrdev_reg ``` 接著對 `chrdev_reg` 使用 `cat` 指令進行存取,並通過 `dmesg` 得到輸出結果 ```shell root@(none):/dev# dmesg ... [ 44.305444] chrdev_reg driver(major 247) installed. [ 540.795404] Call chrdev_reg_open [ 540.796247] Call chrdev_reg_close ``` 也可以修改 `chrdev_reg_init` 程式碼,在註冊階段時,完成建立節點的操作 ```c static int chrdev_reg_init(void) { int major; struct class *cls; major = register_chrdev(chrdev_reg_major, DRIVER_NAME, &fops); if ((chrdev_reg_major == 0 && major < 0) || (chrdev_reg_major > 0 && major != 0)) { pr_err("%s Driver registration error\n", DRIVER_NAME); return major; } else { chrdev_reg_major = major; } pr_info(KERN_ALERT "%s driver(major %d) installed.\n", DRIVER_NAME, chrdev_reg_major); cls = class_create(THIS_MODULE, DRIVER_NAME); device_create(cls, NULL, MKDEV(major, 0), NULL, DRIVER_NAME); pr_info("Device created on /dev/%s\n", DRIVER_NAME); return 0; } ``` #### Scull: Register Rust version ```rust use kernel::prelude::*; use kernel::{file, miscdev}; module! { type: Scull, name: b"scull", author: b"ChaosBot", description: b"Rust scull sample", license: b"GPL", } struct Scull { _dev: Pin<Box<miscdev::Registration<Scull>>>, } #[vtable] impl file::Operations for Scull { fn open(_context: &(), _file: &file::File) -> Result { pr_info!("File was opened\n"); Ok(()) } } impl kernel::Module for Scull { fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> { pr_info!("Rust Scull sample (init)\n"); let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?; Ok(Self { _dev: reg }) } } ``` 最一開頭 `module!` 的部份為巨集,支援許多參數型別,包含 `type`, `name`, `author` 等等,更多資訊參考 [Macro macros::module](https://rust-for-linux.github.io/docs/macros/macro.module.html),其中 `type` 型別需要實現 `Module` 的特徵 (trait),使用特徵定義可以抽象出共同的行為,這邊為模組抽象定義出的共同行為對應到 C 語言中模組的撰寫為 `init_module` 以及 `cleanup_module`,`init_module` 對應到 Rust 中 `init` 的部份,而 `cleanup_module` 在 Rust 中對應到的為 `Drop` 這個特徵,而我們可以為 `Drop` 這個特徵去定義他的行為,以下面為例我們定義的行為會在 `fn drop` 中實作,概念如下所展示 ```rust impl kernel::Module for Scull { fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> { } } impl Drop for Scull { fn drop(&mut self) { } } ``` 在 [`file`](https://rust-for-linux.github.io/docs/kernel/file/index.html) 中 [`Operations`](https://rust-for-linux.github.io/docs/kernel/file/trait.Operations.html) 也是特徵,在該特徵中需要實作 `open` 方法,其餘 `release`, `read` 方法可以選擇不自行實作,因為這一些方法都具有預設的實作方式。與 C 語言的實作比較,C 語言中為初始化 `file_operation` 這個結構體,後者每個成員型別皆為函式指標 (function pointer),後面我們要進行檔案操作時存取該結構即可使用對應的檔案操作。 接著是註冊到子系統的部份,這邊註冊的設備為 `miscdev`,`miscdev` 對應的主編號為 10,所有 `miscdev` 形成一個鏈結串列,如果要對裝置進行存取時,則根據副編號找到對應的 `miscdev`,接著通過 `file_operation` 結構中的方法進行操作。 對於一個 misc 裝置如何進行註冊,在 `linux/rust/kernel/miscdev.rs` 中 `Registration` 結構中可以看到對應的操作以及說明 ```rust pub struct Registration<T: file::Operations> { registered: bool, mdev: bindings::miscdevice, name: Option<CString>, _pin: PhantomPinned, /// Context initialised on construction and made available to all file instances on /// [`file::Operations::open`]. open_data: MaybeUninit<T::OpenData>, } impl<T: file::Operations> Registration<T> { /// Creates a new [`Registration`] but does not register it yet. /// /// It is allowed to move. pub fn new() -> Self { } /// Registers a miscellaneous device. /// /// Returns a pinned heap-allocated representation of the registration. pub fn new_pinned(name: fmt::Arguments<'_>, open_data: T::OpenData) -> Result<Pin<Box<Self>>> { } /// Registers a miscellaneous device with the rest of the kernel. /// /// It must be pinned because the memory block that represents the registration is /// self-referential. pub fn register( self: Pin<&mut Self>, name: fmt::Arguments<'_>, open_data: T::OpenData, ) -> Result { Options::new().register(self, name, open_data) } /// ... ``` 在 `struct Scull` 中有一個成員為 `_dev`,型別為 `Pin<Box<miscdev::Registration<Scull>>>`,對應到上方的程式碼,得到 `_dev` 是用來儲存註冊相關資訊,對於註冊我們有幾種方法可以使用,分別為 `new`, `new_pinned`, `register` 等等,我們要進行的註冊操作為將 misc 裝置加入到 `miscdev` 對應到的鏈結串列上。 ==TODO: 使用 `pinned` 的原因,關於 self-referential-struct== #### Scull 設計 C: [scull.c](https://github.com/bill-kolokithas/kernel-drivers/blob/master/scull/scull.c) Rust: [Mentorship Session: Writing Linux Kernel Modules in Rust](https://www.youtube.com/watch?v=-l-8WrGHEGI&ab_channel=TheLinuxFoundation) #### 關於所有權 - [ ] Rust 程式碼 ```rust impl kernel::Module for Scull { fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> { pr_info!("Rust Scull sample (init)\n"); let dev = Ref::try_new(Device { number: 0, contents: Vec::new(), })?; let reg = miscdev::Registration::new_pinned(fmt!("scull"), ())?; Ok(Self { _dev: reg }) } } ``` - [ ] C 程式碼 (配合 [LKMPG Character Device drivers](https://sysprog21.github.io/lkmpg/#character-device-drivers)) ```c void scull_cleanup_module(void) { int i; dev_t devno = MKDEV(scull_major, scull_minor); /* Get rid of our char dev entries */ if (scull_devices) { for (i = 0; i < scull_nr_devs; i++) { scull_trim(scull_devices + i); cdev_del(&scull_devices[i].cdev); } kfree(scull_devices); } #ifdef SCULL_DEBUG /* use proc only if debugging */ scull_remove_proc(); #endif /* cleanup_module is never called if registering failed */ unregister_chrdev_region(devno, scull_nr_devs); } ``` :::info TODO: Rust version: - file operations - open - read - write - init - Register: 在 Rust 中通過 `miscdev::Registration::new_pinned` 進行裝置註冊 (在 `init_module` 時完成),C 中使用 `scull_setup_cdev(struct scull_dev *dev, int index)` ::: 在 Rust 程式碼中不存在 `kfree(scull_devices)` TODO: Rust version 中所有權運作 #### [TODO: C vs. Rust GPIO](https://lwn.net/Articles/863459/) ### 參考資料 https://zhuanlan.zhihu.com/p/387076919 https://zhuanlan.zhihu.com/p/424831636 https://richardweiyang-2.gitbook.io/kernel-exploring/00_index/02_common_targets_in_kernel * [Rust Kernel Module: Hello World](https://wusyong.github.io/posts/rust-kernel-module-01/)