第二课:开发环境搭建 — QEMU模拟器、Rust工具链与最小化内核项目
好,咱们正式开始动手了。这一课说白了就是「把家伙事儿备齐」。我见过太多初学者,兴致勃勃想写内核,结果卡在环境配置上整整一周。嗯,咱们不干那种事。今天的目标很明确:装好QEMU、配好Rust工具链、搭出一个能跑的最小化内核骨架。
2.1 QEMU模拟器安装
QEMU是什么?简单说,它是个硬件模拟器。你写的内核跑在QEMU上,就像跑在一台真实的物理机上。我个人习惯用QEMU做开发,原因很简单:调试方便,不怕把机器搞崩。
我在项目中遇到过好几次,内核写了个死循环,真机直接黑屏。用QEMU的话,Ctrl+C就能救回来。所以,先装它。
Linux(Ubuntu/Debian)
sudo apt update
sudo apt install qemu-system-arm qemu-system-riscv32 qemu-system-x86-64
macOS
brew install qemu
Windows
建议用WSL2,然后在WSL里执行Linux的安装命令。我曾经试过原生Windows版QEMU,坑比较多,不推荐。
qemu-system-riscv32 --version,看到版本号就说明装好了。
2.2 Rust工具链配置
咱们要用Rust写内核,为什么?因为Rust在系统编程领域,说白了就是「安全版的C」。它的所有权模型能帮你避免内存泄漏、空指针这些头疼的问题。
你想想看,写内核最怕什么?最怕内存越界。Rust在编译阶段就把这类问题堵死了。嗯,这里要注意:我们用的是Rust的no_std模式,因为标准库依赖操作系统,而咱们自己就是操作系统。
安装rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装完成后,重启终端,或者手动加载环境变量:
source $HOME/.cargo/env
添加目标平台
咱们的目标架构是RISC-V 32位。为什么选它?指令集简洁,适合教学。我在项目中用过ARM和x86,但RISC-V的规范文档只有200多页,ARM的有几千页。你想想看,哪个更适合入门?
rustup target add riscv32imac-unknown-none-elf
这个target名字很长,但拆开看就明白了:riscv32是32位RISC-V,imac是指令集扩展(整数、乘除、原子、压缩),unknown-none-elf表示没有操作系统,生成ELF格式。
安装必要的工具
cargo install cargo-binutils
rustup component add llvm-tools-preview
这些工具用来处理编译后的二进制文件,比如查看符号表、反汇编。我曾经调试一个内核bug,就是靠反汇编找到的栈溢出位置。
rustup default stable。如果你非要尝鲜nightly,出了问题别找我(笑)。
2.3 最小化内核项目结构
好,工具装完了,咱们来搭项目骨架。一个微内核再小,也得有这几个东西:启动代码、链接脚本、内核入口、构建配置。
创建项目
cargo new --edition 2021 mykernel
cd mykernel
目录结构
我个人习惯这样组织:
mykernel/
├── Cargo.toml # 项目配置
├── src/
│ ├── main.rs # 内核入口
│ ├── lang_items.rs # 语言项(panic处理等)
│ └── sbi.rs # SBI调用封装
├── linker.ld # 链接脚本
└── Makefile # 构建脚本
Cargo.toml配置
[package]
name = "mykernel"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = "z" # 优化体积
lto = true # 链接时优化
[dependencies]
# 这里先留空,后续章节会加
链接脚本(linker.ld)
链接脚本告诉链接器:内核代码放在内存的什么位置。RISC-V QEMU默认从0x80000000启动,所以:
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80000000;
SECTIONS
{
. = BASE_ADDRESS;
.text : {
*(.text.entry)
*(.text .text.*)
}
.rodata : {
*(.rodata .rodata.*)
}
.data : {
*(.data .data.*)
}
.bss : {
*(.bss .bss.*)
}
/DISCARD/ : {
*(.eh_frame)
}
}
嗯,这里要注意:.eh_frame是异常处理帧,内核里用不上,直接丢弃。我曾经没加这行,结果内核体积大了好几倍,全是无用数据。
内核入口(src/main.rs)
#![no_std]
#![no_main]
mod lang_items;
mod sbi;
use core::panic::PanicInfo;
/// 内核入口点
#[no_mangle]
pub extern "C" fn _start() -> ! {
// 清空BSS段
extern "C" {
fn sbss();
fn ebss();
}
unsafe {
core::slice::from_raw_parts_mut(sbss as *mut u8, ebss as usize - sbss as usize)
.fill(0);
}
// 打印欢迎信息
sbi::console_putchar('H');
sbi::console_putchar('e');
sbi::console_putchar('l');
sbi::console_putchar('l');
sbi::console_putchar('o');
loop {}
}
/// panic处理
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
语言项(src/lang_items.rs)
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
SBI调用(src/sbi.rs)
SBI是RISC-V的底层接口,用来操作硬件。咱们先封装一个最简单的字符输出:
const SBI_CONSOLE_PUTCHAR: usize = 1;
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
let ret;
unsafe {
core::arch::asm!(
"ecall",
in("a0") arg0,
in("a1") arg1,
in("a2") arg2,
in("a7") which,
lateout("a0") ret,
);
}
ret
}
pub fn console_putchar(c: char) {
sbi_call(SBI_CONSOLE_PUTCHAR, c as usize, 0, 0);
}
Makefile
KERNEL := target/riscv32imac-unknown-none-elf/release/mykernel
.PHONY: build run clean
build:
cargo build --release
run: build
qemu-system-riscv32 \
-machine virt \
-nographic \
-bios none \
-kernel $(KERNEL)
clean:
cargo clean
#![no_std]告诉Rust不要链接标准库#![no_main]表示我们不使用常规的main函数入口_start是真正的入口点,由链接脚本指定- BSS段必须手动清零,否则全局变量初始值不确定
2.4 构建与运行
执行 make run,你会看到QEMU启动,然后终端输出"Hello"。虽然只是几个字符,但这是你的内核在裸机上跑出来的!
我第一次看到这个输出时,说实话挺激动的。从零开始,没有操作系统,没有标准库,就靠几百行代码让硬件听你的话。这种感觉,嗯,你试试就知道了。
-d cpu_reset 参数,能看到CPU的复位信息。我曾经靠这个发现链接脚本的入口地址写错了。
2.5 本章小结
咱们今天干了三件事:装QEMU、配Rust工具链、搭最小化内核。你现在有了一个能跑在RISC-V模拟器上的内核骨架。虽然它只会打印"Hello",但万丈高楼平地起,这个骨架就是地基。
下一课,咱们会深入RISC-V的特权级和异常处理。你想想看,内核怎么从S模式切换到U模式?怎么处理系统调用?嗯,这些内容,咱们下回分解。
| 工具/组件 | 版本要求 | 用途 |
|---|---|---|
| QEMU | ≥ 7.0 | 模拟RISC-V硬件 |
| Rust | 稳定版 ≥ 1.65 | 编写内核代码 |
| rustup target | riscv32imac-unknown-none-elf | 交叉编译目标 |
| cargo-binutils | 最新版 | 二进制分析工具 |
课后练习: 试着修改 _start 函数,让它打印你的名字。比如 sbi::console_putchar('Z'); 这样。如果卡住了,检查一下链接脚本里的BASE_ADDRESS是不是0x80000000。