3. 环境搭建:搭建基于Rust的微内核开发环境、配置QEMU模拟器、构建最小内核

好,咱们正式开始动手了。这一章的目标很明确——把开发环境搭起来,然后跑通一个最小内核。说白了,就是让我们的 Rust 代码能在裸机上打印出第一行字。

我个人习惯先把工具链理清楚,再动手写代码。你想想看,如果环境没配好,后面排查问题会非常痛苦。我在项目中遇到过好几次因为工具链版本不对,导致编译出来的内核跑不起来的情况。嗯,咱们这次一步到位。

3.1 开发环境概览

先看看我们需要哪些东西:

组件 用途 版本建议
Rust 工具链 编译内核代码 nightly-2024-01-01 或更新
QEMU 模拟硬件环境 7.2.0 以上
GDB(可选) 调试内核 13.1 以上
Make / Cargo 构建管理 最新稳定版

这里我特别强调一下 Rust 版本。微内核开发必须用 nightly 版本,因为我们需要一些不稳定特性,比如 asm! 宏、global_asm!、naked_functions 等。稳定版目前还支持不了这些。

注意: 不要用 stable 版本的 Rust 编译内核代码。我曾经见过有人用 stable 编译,结果报了一堆 "unstable feature" 错误,折腾了半天才发现是版本问题。

3.2 安装 Rust 工具链

如果你还没装 Rust,先执行这个:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

装完之后,切换到 nightly 版本:

rustup default nightly
rustup update

然后添加目标平台。我们做的是 x86_64 架构的微内核,所以需要:

rustup target add x86_64-unknown-none

这个 x86_64-unknown-none 是什么意思?说白了,就是告诉 Rust 编译器:我们没有操作系统,没有标准库,一切从零开始。嗯,这个目标平台会禁用 std,只保留 core 库。

我个人习惯再装几个有用的组件:

rustup component add rust-src
rustup component add llvm-tools-preview

rust-src 可以让你在 IDE 里跳转到标准库源码,llvm-tools-preview 后面做链接脚本时会用到。

3.3 配置 QEMU 模拟器

QEMU 是我们调试内核的主力工具。不用真机,直接在模拟器里跑,效率高得多。

安装方式根据系统不同略有区别:

系统 安装命令
Ubuntu/Debian sudo apt install qemu-system-x86
macOS (Homebrew) brew install qemu
Arch Linux sudo pacman -S qemu-full

装完之后验证一下:

qemu-system-x86_64 --version

看到版本号就说明装好了。我记得有一次在 macOS 上装 QEMU,Homebrew 编译了快半小时,差点以为死机了。嗯,耐心等就好。

3.4 构建最小内核

好,重头戏来了。我们要写一个能在 QEMU 里跑起来的最小内核。它什么都不做,就打印一行 "Hello from Microkernel!",然后死循环。

先创建项目:

cargo new --edition 2021 mykernel
cd mykernel

然后修改 Cargo.toml:

[package]
name = "mykernel"
version = "0.1.0"
edition = "2021"

[profile.release]
opt-level = "z"   # 优化体积
lto = true        # 链接时优化

接下来是关键——告诉 Rust 我们不需要标准库。在 src/main.rs 里写:

#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
pub extern "C" fn _start() -> ! {
    let vga_buffer = 0xb8000 as *mut u8;
    let hello = b"Hello from Microkernel!";

    for (i, &byte) in hello.iter().enumerate() {
        unsafe {
            *vga_buffer.offset(i as isize * 2) = byte;
            *vga_buffer.offset(i as isize * 2 + 1) = 0x0f;
        }
    }

    loop {}
}

这段代码做了几件事:

  • #![no_std] 告诉编译器不用标准库
  • #![no_main] 表示我们没有传统的 main 函数
  • _start 是内核入口,相当于操作系统的 main
  • 直接往 VGA 显存地址 0xb8000 写字符,这是 x86 实模式下的文本模式缓冲区
关键点: 为什么是 0xb8000?这是 x86 架构规定的 VGA 文本模式缓冲区起始地址。每个字符占两个字节:第一个是 ASCII 码,第二个是颜色属性。0x0f 表示白字黑底。

但是光有 Rust 代码还不够。我们需要一个链接脚本,告诉链接器内核应该放在内存的什么位置。创建 linker.ld:

ENTRY(_start)

SECTIONS {
    . = 0x100000;

    .text : {
        *(.text._start)
        *(.text*)
    }

    .rodata : {
        *(.rodata*)
    }

    .data : {
        *(.data*)
    }

    .bss : {
        *(.bss*)
    }
}

这里 0x100000 是 1MB 的位置。为什么是 1MB?因为 BIOS 会把自己加载到 0x7C00,然后跳转到我们的内核。1MB 以上的空间比较安全,不会被 BIOS 数据覆盖。

最后,我们需要一个 .cargo/config.toml 来指定编译目标:

[build]
target = "x86_64-unknown-none"

[target.x86_64-unknown-none]
rustflags = ["-C", "link-arg=-Tlinker.ld"]

好了,现在可以编译了:

cargo build --release

如果一切顺利,你会看到 target/x86_64-unknown-none/release/mykernel 这个文件。嗯,它现在是一个 ELF 格式的内核镜像。

3.5 在 QEMU 中运行

运行内核需要两步:先把 ELF 转成纯二进制,然后用 QEMU 加载。

用 objcopy 转换格式:

rust-objcopy -O binary target/x86_64-unknown-none/release/mykernel kernel.bin

然后启动 QEMU:

qemu-system-x86_64 -drive format=raw,file=kernel.bin -m 128M -nographic

你应该会看到 QEMU 窗口里显示 "Hello from Microkernel!"。如果没有图形界面,加上 -nographic 参数,输出会重定向到终端。

小技巧: 我习惯把启动命令写成一个 Makefile,这样每次调试只需要敲 make run。后面章节我们会逐步完善这个 Makefile。

3.6 常见问题与避坑

嗯,这里总结几个我踩过的坑:

  • 编译报错 "requires lang_items":忘了加 #[panic_handler]。每个 no_std 程序都必须提供 panic 处理函数。
  • QEMU 黑屏没反应:检查链接脚本里的入口地址。如果 _start 没有被正确放置到 .text 段开头,CPU 会执行到垃圾数据。
  • 字符显示乱码:VGA 缓冲区写入时,颜色属性字节写错了。0x0f 是白字黑底,0x1f 是白字蓝底,别搞混。
  • 编译出来的内核文件巨大:release 模式下加上 opt-level = "z",能把体积从几百 KB 压缩到几 KB。

我曾经有一次调试了整整一个下午,发现是 .cargo/config.toml 里的 rustflags 写错了路径,链接脚本根本没生效。嗯,从那以后我每次改配置都会先 cargo clean 再编译。

3.7 本章小结

到这里,我们的最小内核已经能在 QEMU 里跑起来了。虽然它什么都不做,只是打印一行字然后死循环,但这是万里长征的第一步。你想想看,从零开始,没有操作系统,没有标准库,我们硬是用 Rust 在裸机上点亮了屏幕。

下一章,我们会在这个基础上添加中断处理、内存管理,让内核真正"活"起来。嗯,到时候你会看到,微内核的架构设计是怎么一步步展开的。

对了,如果你在搭建过程中遇到任何问题,记得先检查工具链版本和链接脚本。这两个地方出问题的概率最高。