Rust 커널 프로그래밍 가이드 — Ubuntu에서 Rust for Linux 빌드 환경 만들기

업스트림 커널 소스를 --depth 1로 clone해서 rust_minimal.o를 빌드하는 데는 성공했지만, 그건 지금 부팅된 우분투 배포 커널과는 버전이 다른 별개의 트리였다. 실제로 insmod까지 되는지 확인하려면 “지금 이 컴퓨터가 실행 중인 커널” 그 자체를 기준으로 빌드해야 한다. 해보니 만만치 않았다 — 깨진 패키지 심볼릭 링크, rustc 버전 문자열은 같은데 메타데이터가 안 맞는 문제, 배포 커널의 module! 매크로가 최신 업스트림보다 기능이 적은 문제, LLVM과 GCC가 섞이는 문제까지 네 개의 장벽을 순서대로 만났다. 이 글은 그 과정을 실패한 시도까지 그대로 기록하고, 최종적으로 rust_template.koinsmod/dmesg/rmmod까지 실제로 확인한 결과를 정리한다. 환경은 이전과 동일한 UBUNTU_TEST_HOST(Ubuntu 24.04.4 LTS, 커널 6.17.0-35-generic)다.

배포 커널의 Rust 지원 확인

먼저 지금 부팅된 커널 자체가 CONFIG_RUST로 빌드됐는지부터 확인한다.

$ uname -r
6.17.0-35-generic

$ grep -E "^CONFIG_RUST=|^CONFIG_RUST_IS_AVAILABLE=" /boot/config-$(uname -r)
CONFIG_RUST_IS_AVAILABLE=y
CONFIG_RUST=y

$ ls -la /lib/modules/$(uname -r)/build
/lib/modules/6.17.0-35-generic/build -> /usr/src/linux-headers-6.17.0-35-generic

CONFIG_RUST=y가 확인됐고, out-of-tree 모듈 빌드의 표준 진입점인 build 심볼릭 링크도 이미 linux-headers 패키지로 준비되어 있었다.

1단계 장벽 — 깨진 rust 심볼릭 링크

헤더 트리 안의 rust/는 그 자체로 실체가 아니라 별도 패키지를 가리키는 심볼릭 링크였다. Rust로 컴파일된 kernel crate(rust/kernel.o 등 미리 빌드된 바이너리)를 담은 linux-lib-rust-6.17.0-35-generic 패키지를 설치했는데도 링크가 깨져 있었다.

$ sudo apt-get install -y linux-lib-rust-6.17.0-35-generic
...
Setting up linux-lib-rust-6.17.0-35-generic (6.17.0-35.35~24.04.1) ...

$ ls -la /usr/src/linux-headers-6.17.0-35-generic/ | grep rust
lrwxrwxrwx 1 root root 49 rust -> ../linux-hwe-6.17-lib-rust-6.17.0-35-generic/rust

$ ls /usr/src/ | grep rust
linux-lib-rust-6.17.0-35-generic

심볼릭 링크는 linux-hwe-6.17-lib-rust-6.17.0-35-generic이라는 패키지를 기대하는데, 실제로 apt가 설치한 패키지 이름은 hwe-6.17- 접두사가 빠진 linux-lib-rust-6.17.0-35-generic이었다. apt-cache search로 찾아봐도 hwe-6.17-가 붙은 패키지 자체가 존재하지 않았다 — HWE 커널 헤더 메타패키지 쪽의 실제 패키징 버그로 보인다. 링크를 직접 고쳐야 했다.

$ sudo ln -sfn ../linux-lib-rust-6.17.0-35-generic/rust \
    /usr/src/linux-headers-6.17.0-35-generic/rust

$ ls /usr/src/linux-headers-6.17.0-35-generic/rust/ | head -3
bindings
bindings.o
build_error.o

2단계 장벽 — rustup stable로는 rustavailable부터 실패

링크를 고친 뒤 rustup의 stable 툴체인(1.97.1)으로 rustavailable을 돌리면, 커널이 만든 커스텀 타깃 스펙 JSON을 최신 rustc가 다른 스키마로 해석하면서 깨진다.

error: error loading target specification: target-pointer-width:
invalid type: string "64", expected u16 at line 7 column 32

배포 커널의 rust/kernel.orustc 1.82.0 (f6e511eec 2024-10-15) (built from a source tarball)로 미리 컴파일되어 있었다(/boot/config-6.17.0-35-genericCONFIG_RUSTC_VERSION_TEXT로 확인 가능). rustup으로 정확히 같은 버전(1.82.0)을 설치해도 결과는 나아지지 않았다.

$ rustup toolchain install 1.82.0
$ rustup run 1.82.0 rustc --version
rustc 1.82.0 (f6e511eec 2024-10-15)

$ make RUSTC=$(rustup which --toolchain 1.82.0 rustc) ...
error[E0514]: found crate `core` compiled by an incompatible version of rustc
  = note: crate `core` compiled by rustc 1.82.0 (f6e511eec 2024-10-15)
          (built from a source tarball): .../libcore.rmeta
  = help: please recompile that crate using this compiler

버전 문자열과 커밋 해시는 완전히 같은데도 rustc 크레이트 메타데이터가 호환되지 않는다고 나온다. (built from a source tarball)이 힌트다 — 배포 커널은 Canonical이 소스에서 직접 빌드한 rustc로 만들어졌고, rustup이 공식 배포하는 동일 버전 바이너리와는 내부 빌드 설정이 달라 크레이트 메타데이터 해시가 어긋난다. rustc는 semver가 같아도 빌드 자체가 다르면 ABI 호환을 보장하지 않는다.

해결책은 rustup이 아니라 apt가 제공하는, 배포판이 직접 빌드한 rustc를 쓰는 것이다.

$ sudo apt-get install -y rustc-1.82 rust-1.82-src

$ rustc-1.82 --version
rustc 1.82.0 (f6e511eec 2024-10-15) (built from a source tarball)

$ ls /usr/src/rustc-1.82.0/library/core/src/lib.rs
/usr/src/rustc-1.82.0/library/core/src/lib.rs

rust-1.82-src 패키지는 소스를 커널 빌드 스크립트가 기대하는 정확한 경로(/usr/src/rustc-<version>/library)에 그대로 풀어준다. 이 조합으로 바꾸자 rustavailable과 E0514 에러가 둘 다 사라졌다.

3단계 장벽 — module! 매크로의 params 필드 미지원

4부에서 쓴 params: 필드가 있는 템플릿을 그대로 컴파일하면 이번엔 새로운 에러가 난다.

error: proc macro panicked
  = help: message: Unknown key "params". Valid keys are:
          ["type", "name", "authors", "description", "license", "alias", "firmware"]

4부는 git clone으로 받은 업스트림 최신 마스터 기준으로 검증한 것이고, 이 배포 커널의 module! 매크로는 2026년 5월 27일 시점 스냅샷이라 그 사이 업스트림에 추가된 params 지원이 아직 없다. 업스트림 샘플이 항상 배포 커널에 그대로 얹힌다는 보장은 없다는 걸 실제로 확인한 셈이다. params 없이 다시 작성했다.

// SPDX-License-Identifier: GPL-2.0

//! Rust kernel module template sample.

use kernel::prelude::*;

module! {
    type: RustTemplate,
    name: "rust_template",
    authors: ["Junorion"],
    description: "Rust kernel module template",
    license: "GPL",
}

struct RustTemplate {
    id: u32,
}

impl kernel::Module for RustTemplate {
    fn init(_module: &'static ThisModule) -> Result<Self> {
        let id: u32 = 1;

        if id == 0 {
            pr_err!("id must be greater than 0\n");
            return Err(EINVAL);
        }

        pr_info!("Rust template loaded (id={})\n", id);

        Ok(RustTemplate { id })
    }
}

impl Drop for RustTemplate {
    fn drop(&mut self) {
        pr_info!("Rust template unloaded (id was {})\n", self.id);
    }
}

4단계 장벽 — LLVM=1과 GCC로 빌드된 커널의 충돌

지금까지 습관적으로 붙이던 LLVM=1을 그대로 쓰면 Rust 컴파일(RUSTC)은 성공하지만, 모듈 래퍼(.mod.c)를 컴파일하는 단계에서 clang이 이 커널 설정에 박혀 있는 GCC 전용 플래그를 이해하지 못해 실패한다.

warning: the compiler differs from the one used to build the kernel
  The kernel was built by: x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0) 13.3.0
  You are using:           Ubuntu clang version 18.1.3

  RUSTC [M] rust_template.o        # 여기까지는 성공
clang: error: unknown argument: '-fconserve-stack'
clang: error: unsupported argument 'bounds-strict' to option '-fsanitize='
clang: error: unsupported option '-mrecord-mcount' for target 'x86_64-unknown-linux-gnu'

이 배포 커널은 애초에 GCC 13으로 빌드됐다(x86_64-linux-gnu-gcc-13). LLVM=1CC/LD/AR 전체를 clang 계열로 바꾸는 옵션인데, .config에는 이미 “이 커널은 GCC로 빌드됐다”는 정보가 박혀 있어 두 정보가 충돌한다. Rust 컴파일 자체는 RUSTC만 지정하면 되고 LLVM=1과는 무관하므로, LLVM=1을 빼고 커널을 빌드한 것과 같은 GCC로 나머지를 컴파일하면 된다.

최종 빌드와 insmod/rmmod 실전 검증

out-of-tree 모듈용 Makefile은 표준 형태 그대로다.

KDIR := /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

obj-m += rust_template.o

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

네 가지 장벽을 다 걷어낸 뒤 빌드하면 마지막까지 통과한다.

$ make RUSTC=/usr/bin/rustc-1.82 BINDGEN=$(which bindgen)
  RUSTC [M] rust_template.o
  MODPOST Module.symvers
  CC [M]  rust_template.mod.o
  CC [M]  .module-common.o
  LD [M]  rust_template.ko
  BTF [M] rust_template.ko
Skipping BTF generation for rust_template.ko due to unavailability of vmlinux

$ ls -la rust_template.ko
-rw-rw-r-- 1 noble noble 681320 rust_template.ko

실제로 로드해본다.

$ sudo insmod rust_template.ko
$ lsmod | grep rust_template
rust_template          12288  0

$ sudo dmesg | tail -3
rust_template: loading out-of-tree module taints kernel.
rust_template: module verification failed: signature and/or required key missing - tainting kernel
rust_template: Rust template loaded (id=1)

$ sudo rmmod rust_template
$ sudo dmesg | tail -1
rust_template: Rust template unloaded (id was 1)

Rust template loaded (id=1)Rust template unloaded (id was 1) 둘 다 실제 dmesg에 그대로 찍혔다. 컴파일 검증을 넘어, 지금 이 컴퓨터가 실행 중인 커널에 Rust 모듈을 실제로 얹고 내리는 것까지 확인한 것이다.

주의사항

  • 서명되지 않은 out-of-tree 모듈은 insmod 시점에 커널을 taint시킨다(“module verification failed” 경고). 실서비스 환경이라면 모듈 서명 절차가 별도로 필요하다.
  • 여기서 겪은 네 가지 문제(심볼릭 링크, rustc 버전, params 미지원, LLVM/GCC 충돌)는 이 Ubuntu 24.04 HWE 6.17.0-35 스냅샷 한정 사례다. 다른 배포판·다른 커널 버전에서는 증상이 다르거나 아예 없을 수 있다. 핵심은 “배포 커널이 어떤 rustc로, 어떤 C 컴파일러로 빌드됐는지”를 CONFIG_RUSTC_VERSION_TEXT와 dmesg/insmod 경고에서 먼저 확인하고 거기에 맞추는 것이다.
  • 1~4부의 코드는 업스트림 최신 마스터를 clone해 컴파일 검증한 것이라, 이 글처럼 특정 배포 커널에 얹으려면 API 드리프트(이번엔 params)를 다시 확인해야 할 수 있다.
  • rustup으로 설치한 툴체인은 배포 커널이 자체 빌드한 rustc와 버전 문자열이 같아도 크레이트 메타데이터가 다를 수 있다는 걸 실제로 확인했다. 배포 커널 기준으로 빌드해야 한다면 rustup보다 apt의 버전별 rustc-N.NN 패키지를 먼저 찾아보는 게 지름길이다.

마무리

“컴파일이 된다”와 “이 컴퓨터에 실제로 로드된다”는 다른 수준의 검증이었다. 업스트림 마스터로는 전자까지만 확인할 수 있었고, 후자를 확인하려면 지금 부팅된 커널이 정확히 어떤 rustc·어떤 C 컴파일러로 만들어졌는지부터 되짚어야 했다. 네 가지 장벽 모두 각각 다른 층위의 버전 불일치(패키징, rustc ABI, 매크로 API, C 툴체인)였다는 점이 흥미롭다 — Rust for Linux를 실전에 쓰려면 코드 자체보다 이런 빌드 환경 정합성 쪽에서 시간을 더 쓰게 될 가능성이 높다.

참고

답글 남기기