Slowboot's Linux Kernel Hacks: 1) This blog is created to deeply analyze and share bootloader, linux kernel, device tree & device drivers, build systems (Yocto, Buildroot, OpenWrt, etc.) and RTOS(zephyr, FreeRTOS, etc.) on various embedded boards (ARM, MIPS, RISC-V, X86). 2) It also introduces various network packet processing technologies such as DPDK, XDP, VPP and WireGuard, etc. If you have any questions, please leave a note at any time. ;)
"물론 최근에 M$가 C/C++로 개발된 자신들의 code(windows kernel, Azure infrastructure 등) 중 일부를 Rust로 전환하여 효과를 보았다는 얘기가 들리며, Google 역시 Android를 구성하는 C/C++ 코드를 Rust로 전환하여 상당한 효과(C/C++ 대비 메모리 안전 문제를 1,000배 이상 줄였다고 함)를 보고 있는 것도 사실인 것 같다. 또한 Linux kernel에는 Rust로 구현한 코드가 6.1 version(2022년 10월)부터 보이기 시작하더니, 급기야는 작년 말(2025년 12월)에 C, Assembly에 이어 Rust를 공식 언어로 사용하기로 결정했다고 하는 걸 보니, 앞서 불가능하다고 했던 말이 다소 무색해지는 느낌도 든다."이전 posting 내용 중에서 ...😋
1. BeaglePlay 상에서 Rust Application 돌려 보기
이번 장에서는 BeaglePlay board 위에서 Rust application code를 돌려보는 방법을 소개해 보고자 한다. BeaglePlay board와 관련해서는 (2년 전에 작성한) 이전 posting 내용을 참조해 주기 바란다.
먼저 rust를 설치하는 부분부터 시작해 보자. 아래 내용은 Ubuntu 22.04를 기준으로 하였다.
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
info: downloading installer
Welcome to Rust!
This will download and install the official compiler for the Rust
programming language, and its package manager, Cargo.
Rustup metadata and toolchains will be installed into the Rustup
home directory, located at:
/home/chyi/.rustup
This can be modified with the RUSTUP_HOME environment variable.
The Cargo home directory located at:
/home/chyi/.cargo
This can be modified with the CARGO_HOME environment variable.
The cargo, rustc, rustup and other commands will be added to
Cargo's bin directory, located at:
/home/chyi/.cargo/bin
This path will then be added to your PATH environment variable by
modifying the profile files located at:
/home/chyi/.profile
/home/chyi/.bashrc
You can uninstall at any time with rustup self uninstall and
these changes will be reverted.
Current installation options:
default host triple: x86_64-unknown-linux-gnu
default toolchain: stable (default)
profile: default
modify PATH variable: yes
1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
>1
$ source $HOME/.cargo/env
-> .bashrc에 이 내용을 넣어두면 편리하다.
$ rustc --version
$ cargo --version
1.2 ARM64용 Rust cross toolchain 설치하기
이제 부터는 cross toolchain을 설치할 차례이다.
$ cargo install cross
Updating crates.io index
Ignored package `cross v0.2.5` is already installed, use --force to override
$ rustup target list
-> 설치 가능한 cross toolchain 목록을 확인한다.
aarch64-apple-darwin
aarch64-apple-ios
aarch64-apple-ios-macabi
aarch64-apple-ios-sim
aarch64-linux-android
aarch64-pc-windows-gnullvm
aarch64-pc-windows-msvc
aarch64-unknown-fuchsia
aarch64-unknown-linux-gnu <--- 이걸 사용하자.
aarch64-unknown-linux-musl
aarch64-unknown-linux-ohos
aarch64-unknown-none
aarch64-unknown-none-softfloat
aarch64-unknown-uefi
arm-linux-androideabi
arm-unknown-linux-gnueabi
arm-unknown-linux-gnueabihf
arm-unknown-linux-musleabi
arm-unknown-linux-musleabihf
...
$ rustup target add aarch64-unknown-linux-gnu
-> arm64용 rust toolchain을 설치하도록 한다.
info: downloading component 'rust-std' for 'aarch64-unknown-linux-gnu'
info: installing component 'rust-std' for 'aarch64-unknown-linux-gnu'
29.9 MiB / 29.9 MiB (100 %) 20.8 MiB/s in 1s
1.3 Hello world project 생성 후, cross-compile 하기
자, 이제 hello world project를 하나 만들고, beagleplay board에서 돌려 볼 수 있도록 cross compile해 보자.
$ cargo new hello_beagleplay
Created binary (application) `hello_beagleplay` package
$ cd hello_beagleplay
$ cross build --target aarch64-unknown-linux-gnu
...
Compiling hello_beagleplay v0.1.0 (/project)
Finished dev [unoptimized + debuginfo] target(s) in 0.20s
$ cd target/aarch64-unknown-linux-gnu/debug
-> debug mode로 동작하는 실행 파일이 생성된다.
$ ls -la
합계 4812
drwxr-xr-x 7 chyi chyi 4096 2월 18 17:10 .
drwxr-xr-x 3 chyi chyi 4096 2월 18 17:10 ..
-rw-r--r-- 1 chyi chyi 0 2월 18 17:10 .cargo-lock
drwxr-xr-x 3 chyi chyi 4096 2월 18 17:10 .fingerprint
drwxr-xr-x 2 chyi chyi 4096 2월 18 17:10 build
drwxr-xr-x 2 chyi chyi 4096 2월 18 17:10 deps
drwxr-xr-x 2 chyi chyi 4096 2월 18 17:10 examples
-rwxr-xr-x 2 chyi chyi 4890952 2월 18 17:10 hello_beagleplay
-rw-r--r-- 1 chyi chyi 79 2월 18 17:10 hello_beagleplay.d
drwxr-xr-x 3 chyi chyi 4096 2월 18 17:10 incremental
$ file hello_beagleplay
hello_beagleplay: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=18f0e0dcafba75c521158f6dc39a362ff22abd38, with debug_info, not stripped
마지막으로, hello_beagleplay binary를 target board로 올려 돌려 보도록 하자(파일을 target board로 올리는 절차는 생략함).
<Target board>
debian@BeagleBone:~/workspace$ uname -a
Linux BeagleBone 6.6.32-ti-arm64-r7 #1 SMP PREEMPT_DYNAMIC Wed Jul 3 19:49:27 UTC 2024 aarch64 GNU/Linux
이어지는 장에서는 Linux kernel에서 동작하는 rust kernel module에 관한 이야기를 해 보도록 하겠다.
2. Rust-For-Linux Kernel Build 환경 구성하기
이번 장에서는 BeaglePlay board 상에서 Rust kernel module을 돌리기 위한 준비 작업(Rust 기능을 사용하도록 linux kernel을 build하는 과정)을 소개해 보고자 한다.
2.1 Linux kernel과 Rust의 결합
지난 35년 동안의 리눅스 커널(Linux Kernel) 개발 역사상 가장 중요한 전환점 중 하나는 Rust가 6.1 버전부터 메인라인에 병합되기 시작했다는 사실이다. 메모리 안전성(Memory Safety)과 Data Race 방지를 컴파일 타임에 보장하는 Rust 언어의 특성은, 오랫동안 C 언어가 지배해 온 커널 공간(Kernel Space)에서의 안정성을 획기적으로 향상시킬 만한 잠재력을 가지고 있다고 말할 수 있다.
[그림 2.1] Rust for Linux - rust 관련 작업 내용(1) [출처 - 참고문헌 14]
[그림 2.2] Rust for Linux - rust 관련 작업 내용(2) [출처 - 참고문헌 16]
Linux는 전통적으로 GCC(GNU Compiler Collection)와 GNU Make 기반의 생태계에 최적화되어 있는반면, Rust-for-Linux는 LLVM/Clang 툴체인, bindgen을 통한 FFI(Foreign Function Interface) 자동 생성, 그리고 Rust 컴파일러(rustc)와 커널 버전 간의 엄격한 버전 동기화를 전제로 설계되었다.
리눅스 커널에서의 Rust 지원은 이제 실험적 단계에서 성숙기로 넘어가는 과정(2025년 12월에 공식으로 사용하기로 결정)에 있는데, 각 커널 버전은 특정 버전의 Rust 컴파일러를 엄격하게 요구하고 있다. 이는 Rust 언어 자체가 아직 C 언어처럼 ABI(Application Binary Interface)가 완전히 고정되지 않았고, 커널 내부에서 사용하는 불안정 기능(Unstable Features)들이 컴파일러 버전에 따라 달라지기 때문이다.
따라서, 리눅스 커널에서 Rust 코드를 돌리기 위해서는 단순 메뉴 설정 과정 이상의 다소 복잡한 setup 과정이 선행되어야 한다.
2.2 LLVM/Clang 기반 빌드 시스템
전통적인 Linux 커널 빌드는 CROSS_COMPILE 변수를 통해 gcc를 사용한다. 그러나 Rust 커널 모듈을 빌드하기 위해서는 LLVM 툴체인이 사실상 필수적이다.
Bindgen과 libclang: 커널의 C 헤더 파일을 Rust 코드가 이해할 수 있는 바인딩으로 변환해주는 도구인 bindgen은 libclang에 의존하여 C 코드를 파싱한다. 즉, Clang의 프론트엔드 기능이 호스트 머신에 반드시 존재해야 한다.
LLVM=1 옵션: 커널 커뮤니티는 Rust 지원 커널을 빌드할 때 C 코드와 Rust 코드 모두를 LLVM/Clang으로 빌드하는 것을 "가장 잘 지원되는 설정(Best Supported Setup)"으로 권장하고 있다. GCC를 사용하여 C를 빌드하고 Rust만 rustc로 빌드하는 혼합 방식도 이론적으로 가능하지만, 링커(Linker) 호환성 문제나 LTO(Link Time Optimization) 구성에서 복잡한 문제가 발생할 수 있다.
일반적인 Rust 애플리케이션 개발과 달리, 커널은 no_std 환경(운영체제 없는 환경)에서 동작한다. 따라서 커널은 Rust의 표준 라이브러리 중 가장 핵심적인 core와 alloc 라이브러리를 자신의 빌드 옵션에 맞춰 재컴파일해주어야 한다. 이를 위해서는 호스트 머신에 단순히 rustc 바이너리만 있어서는 안 되며, Rust 표준 라이브러리의 **소스 코드(rust-src)**가 반드시 설치되어 있어야 한다.
📌 바야흐로, GCC의 시대가 저물고, Clang -> LLVM의 시대가 도래한 것 같다. LLVM은 위의 그림에서 보는 것과 같이, clang, rustc 등의 frontend compiler가 생성한 IR code를 토대로 실제 architecture(backend)에 맞는 machine code를 생성해 주는 compiler infrastructure로 이해할 수 있다. 자세한 사항은 참고문헌 [11,12]를 참고하기 바란다.
Ignored package `bindgen-cli v0.65.1` is already installed, use --force to override
$ rustup override set $(scripts/min-tool-version.sh rustc)
-> 이 명령을 실행하면, 기본 toolchain에는 영향을 주지 않고, working directory가 지정된 버젼의 rustc를 사용하도록 설정된다.
info: override toolchain for '/mnt/hdd/workspace/bootlin/beagleplay/bootlin/embedded-linux-beagleplay-labs/linux' set to '1.73.0-x86_64-unknown-linux-gnu'
📌scripts/min-tool-version.sh rustc를 실행하면, rust version 즉, 1.73.0 같은 string이 출력된다. rust update하여 버젼을 upgrade한 후에는 rustup override set 1.93.1 처럼 해 주어야, 실제 rustc version이 변경된다.
$ rustup component add rust-src
-> rust 1.73.0 version에 해당하는 rust source code를 내려 받는다.
info: downloading component 'rust-src'
info: installing component 'rust-src'
$ ARCH=arm64 make LLVM=1 rustavailable
Rust is available!
-> OK, 일단 깔금하게 정리가 되었다.
$ ARCH=arm64 LLVM=1 make menuconfig
어라, 그래도 여전히 "Rust support" menu 항목이 보이질 않는다. 😓
<여기서 잠깐!>
많은 사용자가 "General Setup 메뉴에 Rust support가 없다"고 호소(?)하는 것 같다(필자만의 문제는 아닌 것 같다 😂). 이는 커널의 init/Kconfig 파일에 정의된 RUST 옵션의 depends on 조건이 충족되지 않았기 때문인데, 주요 조건은 다음과 같다.
depends on RUST_IS_AVAILABLE depends on!MODVERSIONS depends on!GCC_PLUGINS depends on!RANDSTRUCT
이 조건들을 하나씩 만족시켜야 비로소 메뉴에 Rust support 체크박스가 나타난다고 한다.
아무리 해 보아도, menuconfig에 Rust 항목이 보이질 않는다. 일전에 Buildroot에 있는 beagleplay config로 build했던 기억이 있는데, 다시 확인해 보니 kernel 6.10 버젼을 사용하고 있다. 그렇다면, kernel version을 6.6 -> 6.10으로 올려 보도록 하자(config는 나중에 다시 고려하기로 하자).
$ git checkout stable/linux-6.10.y
$ git branch
* (HEAD stable/linux-6.10.y 위치에서 분리됨)
master
$ ARCH=arm64 make LLVM=1 rustavailable
***
*** Rust compiler 'rustc' is too old.
*** Your version: 1.73.0
*** Minimum version: 1.78.0 //이 버젼을 설치해 주어야 한다.
***
***
*** Please see Documentation/rust/quick-start.rst for details
지금까지 Rust로 구현한 kernel module을 돌리기 위해 필요한 설정 내용을 자세히 정리해 보았다. 이어지는 장에서는 또 다른 개발 환경인 QEMU를 구축하는 방법을 살펴 보도록 하자.
3. QEMU 환경 설정하기
좀 더 다양하고, 실질적인 rust kernel module을 돌려 보고 싶은데, 몇가지 예제를 테스트해 보니 대부분 kernel 버젼과 일치하지 않아 compile이 안되는 문제가 있다. BeaglePlay board에서 최신 rust kernel module을 모두 돌려 볼 수 있었으면 좋으련만, 어쩔 수 없이 이제 부터는 QEMU(가상 환경)를 사용하여 rust kernel module을 돌려 보기로 하자. 😂
다른 터미널 창에서 qemu process를 확인해 보니, 아래와 같은 모습이 보인다. 마치 docker를 사용하는 듯한 느낌도 드는데, CPU & memory 사용량도 많지 않은 것 같고, 안정적이라는 느낌이 든다. 앞으로 qemu를 좀 더 자주 사용해 보아야겠다. 😍
-> <YOUR_DIR_PATH> 디렉토리에 필요한 kernel module을 복사해 둔다.
-> 부팅 후, mount_tag로 지정한 내용(예: modules)을 이용하여 mount하여 사용한다.
<QEMU 기반 linux booting 후>
~ # mount rootfs on / type rootfs (rw,size=989308k,nr_inodes=247327)
devtmpfs on /dev type devtmpfs (rw,relatime,size=989308k,nr_inodes=247327,mode=755)
none on /proc type proc (rw,relatime)
none on /sys type sysfs (rw,relatime)
~ # mkdir test2
~ # mount -t 9p -o trans=virtio modules ./test2
~ # df
Filesystem 1K-blocks Used Available Use% Mounted on
devtmpfs 989308 0 989308 0% /dev
modules 1921724544 1627240448 196792064 89% /test2
/test2 # mount
rootfs on / type rootfs (rw,size=989308k,nr_inodes=247327)
devtmpfs on /dev type devtmpfs (rw,relatime,size=989308k,nr_inodes=247327,mode=755)
none on /proc type proc (rw,relatime)
none on /sys type sysfs (rw,relatime)
modules on /test2 type 9p (rw,relatime,access=client,trans=virtio)
~ # cd test2/
/test2 # ls -la
total 2328
drwxrwxr-x 2 1000 1000 4096 Feb 21 07:02 . drwxrwxr-x 12 1000 1000 300 Feb 21 07:03 .. -rw-rw-r-- 1 1000 1000 241720 Feb 21 07:02 rust_configfs.ko
-rw-rw-r-- 1 1000 1000 280352 Feb 21 07:02 rust_debugfs.ko
-rw-rw-r-- 1 1000 1000 316336 Feb 21 07:02 rust_debugfs_scoped.ko
-rw-rw-r-- 1 1000 1000 251520 Feb 21 07:02 rust_dma.ko
-rw-rw-r-- 1 1000 1000 181072 Feb 21 07:02 rust_driver_auxiliary.ko
-rw-rw-r-- 1 1000 1000 136344 Feb 21 07:02 rust_driver_faux.ko
-rw-rw-r-- 1 1000 1000 214584 Feb 21 07:02 rust_driver_pci.ko
-rw-rw-r-- 1 1000 1000 209392 Feb 21 07:02 rust_driver_platform.ko
-rw-rw-r-- 1 1000 1000 151840 Feb 21 07:02 rust_minimal.ko
-rw-rw-r-- 1 1000 1000 198304 Feb 21 07:02 rust_misc_device.ko
-rw-rw-r-- 1 1000 1000 165424 Feb 21 07:02 rust_print.ko
______________________________________________
개발 환경이 어느 정도 마무리되었으니, 이어지는 장에서는 Rust kernel module을 만드는 방법을 구체적으로 파악해 보도록 하자.
4. Linux Kernel Module in Rust
이번 장에서는 Rust kernel module(or device driver)을 만드는 방법에 관하여 좀더 깊게 들어가 보도록 하자.
"Kernel Rust has a different flavour to idiomatic userspace Rust (different APIs, different build system, failable allocations, no dependencies, etc)."
커널에서 동작하는 Rust는 관용적인 사용자 공간 Rust와는 다른 특징을 갖는다 (다른 API, 다른 빌드 시스템, 실패 가능한 할당, 의존성 없음 등).
4.1 Rust Kernel 아키텍쳐
기존에 C 언어로 linux kernel module을 만들었던 것을 떠올려 보면, Linux kernel code를 작성할 때에는 libc에서 제공하는 다양한 함수(printf, strcmp, exit, malloc, free 등)를 이용할 수도 없으며, thread를 만들기 위해 pthread library를 사용할 수도 없었다. Linux kernel programming을 한다는 것은, kernel에서 자체적으로 구현한 코드(일종의 framework으로 볼 수 있음)을 사용해야만 한다는 뜻이다.
그렇다면, Rust kernel module의 경우는 어떨까 ? Rust로 사용자 영역에서 동작하는 application code를 작성하는 방식과 동일하게 kernel code를 만들면 되는 것일까 ? 대답은 당연히 "아니오"이다.
사설이 좀 길었는데, Rust로 kernel module(or device driver)을 작성하기 위해서는 kernel에 구현되어 있는 rust framework을 이해하고 있어야만 가능하다는 말을 하고 싶었을 뿐이다. 😂
[그림 4.1] /linux/rust 디렉토리 파일 목록
아래 2개의 그림은 kernel에 구현된 rust framework(/linux/rust directory)의 대략적인 architecture를 표현한 것이다. 드라이버나 커널 모듈을 구현하는 일반 사용자는 kernel crate를 사용하여 safe한 방식으로 rust code를 작성할 수 있는데 반하여, kernel crate 내부적으로는 binding crate를 통해 C code를 unsafe한 방법으로 호출하는 구조로 설계되어 있음을 알 수가 있다.
Linux C code => rust-bindgen(자동 호출) => bindings crate => kernel crate => drivers/kernel module 구현
Linux kernel에서 Rust programming을 한다는 것은 rust를 사용하여 처음부터 새로운 것을 만드는 것이 아니라, 지금까지 오랜 세월 동안 안정적으로 개발되어 온 C code를 재활용하여 동일한 작업(driver or kernel module 구현)을 할 수 있게 하는 것을 의미한다.
Rust kernel 아키텍쳐와 관련한 보다 세부적인 사항에 대해서는 이전 posting을 참조하기 바란다.
Rust kernel module이 C kernel module과 특별히 다른 점이 있다면, rust kernel module은 무조건 structure 변수를 하나 선언하고, impl { init() and drop() } 내에서 연관함수나 메서드를 구현하는 형태로 코드가 구성되어 있다는 점이다.
-rw-rw-r-- 1 chyi chyi 160200 2월 21 15:54 rust_minimal.ko
<Target board>
$ insmod ./rust_minimal.ko
$ rmmod rust_minimal
[그림 4.5] rust-minimal kernel module 예제 실행 모습
4.4 rust misc device 커널 모듈 돌려보기
이번에 살펴볼 rust kernel modue은 misc device driver로 /dev/rust-misc-device 파일을 통해 사용자 application과 통신(open, ioctl, close)하는 역할을 한다. misc device driver 구현 및 동작 원리와 관련해서는 참고문헌 [17]에 상세히 설명되어 있다.
위의 kernel module과 사용자 application을 순차적으로 실행해 보면 다음과 같다.
[그림 4.6] rust_misc_device kernel module 예제 실행 모습
[그림 4.7] misc_dev_app 사용자 영역 application 예제 실행 모습
4.5 그 밖의 커널 모듈(or 디바이스 드라이버)에 관하여
지금까지는 아주 기초적인 rust 기반 kernel module에 대해 살펴 보았다. 하지만, 실제 환경에서는 이러한 내용 보다는 platform driver, i2c driver, spi driver, usb driver, pci driver, network driver 등 다양한 device driver를 구현해야 하는 상황에 직면하게 된다. 다행히도 이들과 관련한 sample code가 linux 최신 커널에 일부 포함되어 있으니, 1차적으로 이들을 참조하면 될 것으로 보인다. 😂