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 等。稳定版目前还支持不了这些。
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 实模式下的文本模式缓冲区
但是光有 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 参数,输出会重定向到终端。
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 在裸机上点亮了屏幕。
下一章,我们会在这个基础上添加中断处理、内存管理,让内核真正"活"起来。嗯,到时候你会看到,微内核的架构设计是怎么一步步展开的。
对了,如果你在搭建过程中遇到任何问题,记得先检查工具链版本和链接脚本。这两个地方出问题的概率最高。