TL;DR
在Ubuntu 22.04 LTS平台进行实验
下载所有依赖
首先确保安装了rust, 可以按照官网用一行命令下载.
下载所有依赖:
sudo apt install -y pkg-config libglib2.0-dev libpixman-1-dev
从源码开始编译并下载qemu:
wget https://download.qemu.org/qemu-5.0.0.tar.xz tar xvJf qemu-5.0.0.tar.xz cd qemu-5.0.0 ./configure --target-list=riscv32-softmmu,riscv64-softmmu make -j$(nproc) sudo make install
创建工作目录
在工作目录中创建一个文件rust-toolchain
, 并且写入:
nightly-2020-06-27
使用cargo
创建新的工程:
cargo new os
运行如下命令安装rust项目依赖:
cargo install cargo-binutils
rustup component add llvm-tools-preview
rustup target add riscv64gc-unknown-none-elf
移除标准库
rust中, 项目默认链接到rust的标准库std
, 而std
依赖于特定的OS, 如果在裸机上写代码需要移除标准库依赖.
在main.rs
中, 加入#![no_std]
:
#![no_std]
fn main() {
println!("Hello, world!");
}
然后运行cargo run
, 会出现以下错误:
error: cannot find macro `println` in this scope
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
其中, 第一个错误是因为没了标准库, 就没有了println
这个宏, 直接把println
删除就可以.
第二个错误是因为: rust中需要一个函数作为panic_handler
, panic_handler
在程序发生panic
后执行, 也是标准库中的函数. 我们需要在main.rs
中实现:
// main.rs
// core这个库不依赖于标准库和OS
use core::panic::PanicInfo
// ! 表示函数从不返回
#[panic_handler]
// PanicInfo包含了panic发生的一些信息, 例如文件名, 代码行数以及错误信息
fn panic(_info: &PanicInfo) -> ! {
}
之后, 第三个eh_personality
是用来标记堆栈展开(Stack Unwinding)函数的语义项:
堆栈展开:
- 当程序出现异常时, 一般需要记录堆栈信息, 这就需要沿着最里层的函数一步步向上收集信息.
- 每到一层函数调用, 我都调用需要这个堆栈展开函数来处理信息, 收集完信息后, 调用
panic
函数打印有效信息给用户. - 这个函数在标准库中实现, 并且依赖于OS, 在这里直接在
Cargo.toml
进行配置, 要求出现错误直接调panic
终止程序即可, 不要先进行Stack Unwinding然后再调panic
终止.
...
# panic 时直接终止,因为我们没有实现堆栈展开的功能
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
覆盖运行时环境
一个链接了标准库std
的rust程序的运行过程是这样的:
- 跳转到C语言运行时环境的入口
crt0
(C Runtime Zero)的_start
函数, 开始配置C语言运行的堆栈/寄存器等参数. - 然后跳转到rust运行时环境的入口
start
, 配置rust的运行时环境. - 然后跳转到rust的
main
函数运行.
因此, 需要覆盖C语言的运行时环境_start
以及rust的运行时环境, main.rs
中的内容是:
// 禁用标准库
#![no_std]
// 不使用main作为入口点
#![no_main]
// panic_handler
#[panic_handler]
fn panic(_info: &PanicInfo) {
loop {}
}
// 覆盖crt0的_start函数
// no_mangle表示禁用rust编译器的名称调整(Name Mangling), 保证最终生成的函数一定叫_start
// pub extern "C"表示这个定义的函数可以在外面被C语言进行调用
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
编译为目标平台代码
rust使用一个叫做Target triple的字符串来描述不同的平台, 可以使用rustc --version --verbose
查看:
rustc 1.70.0 (90c541806 2023-05-31) (built from a source tarball)
binary: rustc
commit-hash: 90c541806f23a127002de5b4038be731ba1458ca
commit-date: 2023-05-31
host: aarch64-apple-darwin
release: 1.70.0
LLVM version: 16.0.2
其中的host
就是这个Target triple, 它的格式是: arch-vendor-os-ABI
.
rust编译生成的代码会自动适配这个平台, 我们的平台是riscv64gc-unknown-none-elf
, 其中none
表示没有OS.
在os
文件夹中新建一个.cargo
文件夹, 创建一个config
文件, 写入编译的目标平台:
# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
调整内存布局
对接的目标平台是qemu-virt, 其中DRAM的起始地址是0x80000000, 大小是128MB.
OS代码的起始地址是0x80200000
, 因为程序首先要在M态运行OpenSBI, 然后才会切换到S态, 并且跳转到0x80200000
并且运行OS代码.
一种典型的程序内存布局如下:
.text
段: 存放汇编代码.rodata
段: 只读数据段, 一般存储程序中的常量..data
段: 存储初始化的全局变量..bss
段: 存储被初始化为0的课读写数据, 与.data
段不同, 由于知道了这个段中的数据会被初始化为0, 在可执行文件中只需要记录这个段的大小, 以及位置, 而不用记录数据, 因此这个段一般不占用内存.
链接脚本如下:
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;
SECTIONS
{
. = BASE_ADDRESS;
kernel_start = .;
text_start = .;
.text : {
*(.text.entry)
*(.text .text.*)
}
rodata_start = .;
.rodata : {
*(.rodata .rodata.*)
}
data_start = .;
.data : {
*(.data .data.*)
}
.bss_start = .;
.bss : {
*(.sbss .bss .bss.*)
}
kernel_end = .;
}
然后在os/.cargo/config
中添加配置, 使用自己的链接脚本:
# 使用我们的 linker script 来进行链接
[target.riscv64gc-unknown-none-elf]
rustflags = [
"-C", "link-arg=-Tsrc/linker.ld",
]
编写入口函数
假设入口函数文件叫entry.S
:
.section .text.entry
.globl _start
_start:
# 将sp指向栈顶
la sp, boot_stack_top
call rust_main
.section .bss.stack
.globl boot_stack
boot_stack:
# 16KB的启动栈(4096 x 4 Bytes)
.space 4096 * 16
# 栈顶
.globl boot_stack_top
boot_stack_top:
然后在main.rs
中添加如下代码:
#![no_std]
#![no_main]
// 用来支持内联汇编
#![feature(llvm_asm)]
// 用来内嵌整个汇编文件
#![feature(global_asm)]
// 汇编入口
global_asm!(include_str!("entry.S"));
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
// 向屏幕输出一个字符
pub fn console_putchar(ch: u8) {
let _ret: usize;
let arg0: usize = ch as usize;
let arg1: usize = 0;
let arg2: usize = 0;
let which: usize = 1;
unsafe {
llvm_asm!("ecall"
: "={x10}" (_ret)
: "{x10}" (arg0), "{x11}" (arg1), "{x12}" (arg2), "{x17}" (which)
: "memory"
: "volatile"
);
}
}
// rust入口函数
#[no_mangle]
pub extern "C" fn rust_main() -> ! {
// 在屏幕上输出 "OK\n" ,随后进入死循环
console_putchar(b'O');
console_putchar(b'K');
console_putchar(b'\n');
loop {}
}
生成内核镜像
做好上述准备后, 可以通过cargo build
生成二进制elf
文件.
生成的文件在target/riscv64gc-unknown-none-elf/debug/os
中.
然后去除elf文件中符号表等额外信息, 生成bin
文件:
rust-objcopy os --strip-all -O binary kernel.bin
启动内核
生成内核镜像后, 使用如下命令启动内核:
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios default \
-device loader,file=kernel.bin,addr=0x80200000