# 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 工具的概念視圖
![image](https://hackmd.io/_uploads/BJigeyuIA.png)
> [出處](https://github.com/immunant/c2rust/tree/master/docs)
以下為嘗試將上方 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/)