執行人: fourcolor
專題解說錄影
LKMPG 是 Linux 核心內建文件所推薦的電子書,是世上極少數自由流通的 Linux 核心專書。本任務嘗試以 Rust 程式語言改寫 LKMPG 既有的 Linux 核心模組範例 (亦可新增),針對 Linux v6.1+。
閱讀以下素材:
紀錄閱讀過程中的認知和疑惑,並答覆以下:
至少應包含:
過程中詳細記錄,特別是 Linux v6.1+ 遇到的 Rust 編譯及相容性議題。
Linux 於 v6.1 將 Rust 引入,使得 Rust 成為了第二個 linux kernel 程式語言,此專案將會從文獻、演講了解 linux 開發者們為何做出此決定,並進一步學習如何在 Linux 開發核心組。
Linux 使用 C 語言撰寫已超過 30 年,然而 C 語言存在著不少的 Undefined Behavior (UB),這在使用 C 語言開發時容易產生安全隱患。以下是 What Every C Programmer Should Know About Undefined Behavior #2/3 所舉的例子
由於在 C 中,signed integer overflow 是個 UB,編譯器在做最佳化時,會認為 size > size+1
總是 false ,因此該程式碼很有可能會等價於
很明顯的當 size
為 INT_MAX
時,這段程式碼會出現錯誤。這件事代表即使開發人員在寫程式時有想到要檢查 overflow 的問題,也有機會因為編譯器的最佳化而產生錯誤。因此這些 UB 往往對程式造成許多安全上的疑慮。
延伸閱讀:memory-safety
從 lkml.org [PATCH 00/13] [RFC] Rust support 可以看到開發者們選擇 Rust 作為 Linux 的第二個程式語言的原因,除了 Rust 有比 C 更多的特徵與功能外,第一點提到了 Rust 是沒有所謂的 UB ,除此之外 Rust 本身也提供許多關於記憶體非法使用上的進階偵測。
Why Rust?
Rust is a systems programming language that brings several key
advantages over C in the context of the Linux kernel:
No undefined behavior in the safe subset (when unsafe code is
sound), including memory safety and the absence of data races.Stricter type system for further reduction of logic errors.
A clear distinction between safe and
unsafe
code.Featureful language: sum types, pattern matching, generics,
RAII, lifetimes, shared & exclusive references, modules &
visibility, powerful hygienic and procedural macros…Extensive freestanding standard library: vocabulary types such
asResult
andOption
, iterators, formatting, pinning,
checked/saturating/wrapping integer arithmetic, etc.Integrated out of the box tooling: documentation generator,
formatter and linter all based on the compiler itself.
Overall, Rust is a language that has successfully leveraged decades
of experience from system programming languages as well as functional
ones, and added lifetimes and borrow checking on top.
以下取自 Rust for Linux - Miguel Ojeda 的其中一個關於 Use-After-Free (UAF) 例子
下方的 C 程式碼每次輸出的結果都是不可預測的,而上方的程式碼 (Rust code) 在編譯時就會出現以下錯誤訊息
而 Rust 之所以可以做到這樣,這要歸功於 Rust 的記憶體管理機制 Ownership
但同時也在前面就提到,引入 Rust 並不是為了改寫整個 Linux 核心
Please note that the Rust support is intended to enable writing drivers and similar "leaf" modules in Rust, at least for the foreseeable future. In particular, we do not intend to rewrite the kernel core nor the major kernel subsystems (e.g.
kernel/
,mm/
,sched/
…). Instead, the Rust support is built on top
of those.
除此之外,在 Supporting Linux kernel development in Rust 提到關於 Rust 引入 Kernel 的三個探討的主題分別是: Binding to existing C APIs, Architecture support, ABI compatibility with the kernel
Rust 透過 std::ffi module 實現與其他語言的跨語言函式介面,而 Rust 的 bindgen crate 便是基於 std:ffi 實現 Rust 自動化產生與 C/C++ 的跨語言函式介面
由於 linux 引入 Rust 的目的並不是為了將整個 kernel 改寫,因此讓 Rust 可以呼叫現有的 C Kernel API 便是最重要。這裡我們以靜態函式庫作為範例。
假設我們要將下列 C 程式引入 Rust 來做使用。
將上述程式碼編譯成靜態函式庫
利用 cargo 新增 rust 專案
新增 build.rs
接著在使用 C 的函式需要在 rust 做相對應的定義
執行結果如下
除了自行定義相關結構和函式,我們也可以透過 bindgen 來自動化產生
更多相關用法可以參考
A first look at Rust in the 6.1 kernel 中提到,6.1 版作為第一個將 rust 引入的版本並不能做到太多事情,並且在 sample 中引入 rust_minimal.rs
用於展示如何使用 rust macro 建立 MODULE_DESCRIPTION() 和 MODULE_LICENSE() ,並且透過實作 trait (概念類似於 interface ) kernel::Module 的 init ,以及 trait Drop 中的 drop 來做到 module_init() 和 module_exit()。此外可以透過 Vec 做到類似於 array 的功能。而 try_push() -> Result<T, E> 會回傳成功與否,而 ?
則會在 try_push 失敗時讓 init 回傳失敗,成功時回傳 T 物件。
從 Rust in the 6.2 kernel 可看到在這個版本中支援了所有的 linux log level ,並且新增了 rust_print.rs 的範例
引入 #[vtable]
這個巨集,在 Linux 核心有許多結構體置入函式指標的應用,struct file_operations
就是一個典型的例子,使用者自定義 read(), write() 並將對應的函式指標傳入 file_operations
。雖然這個功能看似可以使用 rust 中的 trait 來達成,然而 linux 中允許省略任何不相關的功能,此狀況會造成空指標的發生,remap_file_range()
就是其中一例,它在大多時刻是沒有用處的。然而空指標是 Rust 竭盡避免的事情。因此透過 #[vtable]
來解決,後者會為每個 XXX function 新增一個 HAS_XXX 的變數,若是有實作該函式就會將其設置為 TRUE ,在編譯時就會透過存取這些變數產生對應的 struct ,若是 FALSE 則放入空指標。詳細實作可以看這個 patch
引入 declare_err!()
macro 產生對應的 error code
TODO 背後機制
引入 cStr 和 CString 對應 c string ,確保字串結尾為 NUL
TODO:
引入 dbg!
macro
從這個 patch 可以看到在 linux 6.3 支援 Arc、ArcBorrow 和 UniqueArc 類型,以及 ForeignOwnable 和 ScopeGuard
目前 Rust for linux 只支援 um (user-mode Linux) , x86 這兩個架構
先下載相關套件
接著下載 rustup , rustup 是負責安裝及管理 Rust 的工具。
然後安裝 linux 指定的 rustc (rust compiler)版本
由於目前 Rust-For-Linux 使用到許多 unstable feature ,因此所使用的是特定版本的 rustc 而非最小版本
unstable feature: https://github.com/Rust-for-Linux/linux/issues/2
bingen 是 rust 用來產生與 c/c++ 的跨語言程式面界的工具,然後透過 cargo (rust 的套件管理系統)來安裝 bindgen
下載 linux 核心原始碼 ( 版本至少要 6.1)
或是下載 Rust-for-Linux 專案(recommend)
接下來我們會透過 qemu 和 busybox 來建立測試環境,我們參考 Rust-for-Linux/linux 的 qemu-busybox-min.config 設定檔放在 kernel/configs 下
理論上在設定完後你應該要在 .config 看到以下 CONFIG_RUST=y
,若是沒有,可以透過 make LLVM=1 menuconfig
確認 CONFIG_RUST=y
相關的 dependency
或是直接使用 Writing Linux Kernel Modules in Rust 所提供的 disk.img
我們會透過 busybox 來搭建 linux 掛載的檔案系統及 initrd
我們先從 GitHub 上下載 busybox 的原始碼
接著使用預設的設定檔
然後我們希望透過靜態連結的方式來連結函式庫因此使用 menuconfig 做以下設定
設定完後接著編譯並建立一個 initramfs
做完上述指令後會在目錄下產生一個 _install
目錄,裡面包含了基本的檔案目錄架構,以及一些常用的工具(如 ps, shell, cat …),接著我們要將對其做一些調整
由於 busybox 預設會開啟 tty2 到 tty5 ,但測試環境並不會使用,因此修改 inittab
並且我們需要系統去掛載 /proc
新增 etc/init.d/rcS
最後將其打包成映像檔
利用上面建的環境,我們可以在 linux 透過 make LLVM=1 menuconfig
來將 sample/rust 中的範例引入
或是可以透過修改 samples/rust/Kconfig, samples/rust/Makefile 來新增自己的 kernel modules
接著編寫相對應的檔案
接著開機後就會在開機畫面看到
一開始會用 module! 來定義一個 type 接著要為這個 type 實作 Module trait ,其中 trait kernel::Module
中的 fn init
對應的就是 C linux kernel module 中的 void init_module()
,相當於 kernel module 的進入點,而 trait Drop
中的 fn drop
對應的則是 void cleanup_module()
,會在 kernel module remod 時執行。pr_info!
則對應 pr_info
linux kernel 的 file operations 如下
在 rust 中與之相對應的為 Trait kernel::file::Operations
在使用 char device 前,需要先向核心註冊,並且 kernel 會透過 major number 和 minor number 對該裝置進行存取,在 C 中我們會透過
或藉由下列兩個其中一函式:
接著會使用 cdev_alloc()
創健一個 char device 並使用 file_operations 實作 char device 的 ops ,最後使用 int cdev_add(struct cdev *p, dev_t dev, unsigned count);
將 char device 加入系統。
而在 rust 中我們我們可查看 Struct kernel::chrdev::Registration 發現到這個 struct 會需要一個參數 N 代表這個 char device 最多可以註冊幾次,並且透過 pub fn new_pinned
讓 char device 的資料固定在某個記憶體位址上(TODO 原因),最後則是使用 pub fn register<T: Operations<OpenData = ()>>
將 char device 加入系統。
rust 中的 pub fn register<T: Operations<OpenData = ()>> 包含呼叫 C 語言的 alloc_chrdev_region
, cdev_alloc()
和 cdev_add()
source code
TODO
目前還不清楚確定有執行 init 的情況下,/dev/ 下沒有 rust_chrdev 的原因 (已手動新增 i-node)
misc device 是在當裝置無法被分類時所使用的,所有 misc driver 的 major number 都為 10 並且建立 misc device 時會自動建立 char device 可以大幅簡化 char device 的編寫。在 C 中 會透過 misc_register 向系統註冊,而在 rust 中則是透過 Struct kernel::miscdev::Registration
pub fn new_pinned()
proc 檔案系統可用來讓核心模組傳遞訊息給行程。
目前還沒找到相關支援,考慮使用 kernel::fs 或使用 out of tree 的方式實作
Workqueue 常常被用在需要執行多個非同步任務的場合,使用 Workqueue 的好處在於開發者不需要額外管理生產新任務所需要的配置與釋放。在 C 中,其主要的三個 API 如下
alloc_workqueu
: 配置一個 workque ,並透過 flags 來指定排程特性queue_work
: 將任務加入 workqueuedestroy_workqueue
: 釋放 workqueuq在 Rust 中,我們透過以下 API 來使用 workqueue
WQ_FREEZABLE
WQ_HIGHPRI
WQ_UNBOUND
而在使用 work_queue 前我們需要建立 work item ,每個 work item 會綁定一個 function pointer 代表要執行的事情,而在 C 中 struct work_struct 定義如下
在 Rust 中,我們需要實作 trait kernel::workqueue::WorkAdapter 中的 run() 來做到綁定任務給 work item ,我們可以透過 kernel::impl_self_work_adapter! 這個 macro 來達成。
以下為幾個 workqueue 簡單的使用案例
執行結果:
執行結果:
rust_echo server 是在 7ee240 加入 Rust-for-linux 的範例,展示如何用 Rust 的 async 搭配 workqueue worker 以達成類似 kecho 的核心模組。
可藉由 telnet 127.0.0.1 8080
傳送訊息給 echo server