3. 开发环境搭建:QEMU模拟器安装、RISC-V交叉编译工具链配置、最小化内核项目结构、Makefile编写
好,咱们正式开始动手。这一章说白了就是「把家伙事儿备齐」。我见过太多人兴致勃勃想写内核,结果卡在环境搭建上,一卡就是两三天。其实没那么玄乎,咱们一步步来。
3.1 QEMU模拟器安装
QEMU 是什么?简单说,它能在你的电脑上模拟出一台 RISC-V 机器。你写的内核不用烧到真芯片上,直接在 QEMU 里跑就行。我个人习惯用 QEMU 做开发,因为调试起来太方便了——断点、单步、看寄存器,样样都行。
安装 QEMU 其实很简单。以 Ubuntu 为例:
sudo apt-get update
sudo apt-get install qemu-system-misc
如果你用的是其他发行版,比如 Arch:
sudo pacman -S qemu-system-riscv
装完之后验证一下:
qemu-system-riscv64 --version
看到版本号就说明装好了。嗯,这里要注意:qemu-system-misc 这个包在有些老版本 Ubuntu 里可能不包含 RISC-V 支持。我曾经踩过这个坑,装了半天发现模拟器不认识 -machine virt 参数。后来换成 qemu-system-riscv64 单独装才搞定。
brew install qemu。Windows 用户可以用 WSL2,然后在 Linux 子系统里装。
3.2 RISC-V交叉编译工具链配置
你想想看,你的电脑是 x86 架构,但你要编译出能在 RISC-V 上跑的程序。这就需要一个「交叉编译工具链」。说白了,就是一套能在 x86 上生成 RISC-V 机器码的编译器、链接器、汇编器。
我推荐用 riscv64-unknown-elf- 这套工具链。为什么是 unknown-elf?因为它不依赖任何操作系统——咱们写的是内核,本身就是操作系统,不需要底层的 libc 支持。
安装方式有两种:
- 直接安装预编译包(推荐,省时间)
- 从源码编译(如果你有特殊需求)
咱们选第一种:
sudo apt-get install gcc-riscv64-unknown-elf
装完检查一下:
riscv64-unknown-elf-gcc --version
如果看到类似 riscv64-unknown-elf-gcc (GCC) 12.2.0 的输出,就说明搞定了。
3.3 最小化内核项目结构
好,工具链有了,模拟器有了。接下来咱们搭项目骨架。一个最小化的微内核,目录结构长什么样?我习惯这样组织:
mykernel/
├── arch/
│ └── riscv64/
│ ├── boot.S # 启动汇编代码
│ ├── trap.c # 中断/异常处理
│ └── memlayout.h # 内存布局定义
├── kernel/
│ ├── main.c # 内核入口
│ ├── syscall.c # 系统调用实现
│ └── sched.c # 调度器(后面会讲)
├── include/
│ ├── types.h # 类型定义
│ └── syscall.h # 系统调用号定义
├── Makefile
└── linker.ld # 链接脚本
你可能会问:为什么这么分?
arch/放架构相关的代码。以后你想移植到 x86 或 ARM,只需要改这个目录。kernel/放内核核心逻辑。这部分理论上与架构无关。include/放头文件。我习惯把公共类型定义放在这里。
咱们先写一个最简单的 boot.S,让 QEMU 能跳转到我们的内核入口:
.section .text.init
.globl _start
_start:
# 设置栈指针
la sp, _stack_top
# 跳转到 C 语言入口
call kernel_main
# 如果返回,就死循环
loop:
wfi
j loop
对应的 main.c:
void kernel_main(void) {
// 暂时什么都不做
while(1) {
asm volatile("wfi");
}
}
嗯,这里要注意:wfi 指令是 RISC-V 的「等待中断」指令。内核没事干的时候就该休眠,等中断来了再干活。这是微内核的基本哲学——不忙等。
3.4 Makefile编写
Makefile 是咱们和编译器之间的「翻译官」。我见过很多新手把 Makefile 写得巨复杂,动不动就上百行。其实对于微内核开发,一个 30 行的 Makefile 就够了。
咱们来看一个最小化的版本:
CROSS_COMPILE = riscv64-unknown-elf-
CC = $(CROSS_COMPILE)gcc
LD = $(CROSS_COMPILE)ld
OBJCOPY = $(CROSS_COMPILE)objcopy
CFLAGS = -march=rv64gc -mabi=lp64d -nostdlib -ffreestanding -Iinclude
LDFLAGS = -T linker.ld -nostdlib
SRCS = arch/riscv64/boot.S kernel/main.c
OBJS = $(SRCS:.S=.o)
OBJS := $(OBJS:.c=.o)
all: kernel.elf
%.o: %.S
$(CC) $(CFLAGS) -c $< -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
kernel.elf: $(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
run: kernel.elf
qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf
clean:
rm -f $(OBJS) kernel.elf
解释几个关键点:
-nostdlib:不链接标准库。咱们写内核,不需要 printf 那些东西。-ffreestanding:告诉编译器这是独立环境,没有操作系统支持。-march=rv64gc:指定 RISC-V 64位架构,带通用扩展。-mabi=lp64d:LP64 数据模型,双精度浮点。
linker.ld 是必须的。它告诉链接器内核代码应该放在内存的哪个位置。对于 QEMU virt 机器,我们通常把内核放在 0x80000000 处。
对应的 linker.ld 长这样:
OUTPUT_ARCH(riscv)
ENTRY(_start)
SECTIONS
{
. = 0x80000000;
.text : {
*(.text.init)
*(.text)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
}
_stack_top = . + 0x10000;
}
你可能会问:为什么是 0x80000000?因为 QEMU 的 virt 机器把 DRAM 映射到这个地址。这是硬件决定的,咱们改不了。
3.5 验证环境
所有文件写好后,在项目根目录执行:
make run
如果一切正常,你会看到 QEMU 窗口弹出来,然后... 黑屏。别慌,这是正常的!因为咱们的内核目前只会执行 wfi 指令,什么都不做。但至少证明:
- 交叉编译工具链能正常工作
- QEMU 能加载并运行我们的内核
- 项目结构是合理的
-d in_asm 参数,它会打印执行的每一条汇编指令。我调试启动阶段时经常用这招。
好,环境搭好了。下一章咱们开始写真正的系统调用——让内核和用户程序说上话。你想想看,一个连系统调用都没有的内核,跟一个死循环有什么区别?