ch1
2025年10月17日 · 4532 字 · 10 分钟
环境配置
一路操作下来遇到warning:
➤ make run
(rustup target list | grep "riscv64gc-unknown-none-elf (installed)") || rustup target add riscv64gc-unknown-none-elf
riscv64gc-unknown-none-elf (installed)
cargo install cargo-binutils
warning: `/home/f1owerc/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/home/f1owerc/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/home/f1owerc/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/home/f1owerc/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
Updating `ustc` index
Ignored package `cargo-binutils v0.4.0` is already installed, use --force to override
rustup component add rust-src
info: component 'rust-src' is up to date
rustup component add llvm-tools-preview
info: component 'llvm-tools' for target 'x86_64-unknown-linux-gnu' is up to date
Platform: qemu
warning: `/home/f1owerc/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/home/f1owerc/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
Finished `release` profile [optimized + debuginfo] target(s) in 0.02s
QEMU: Terminated
cargo新版使用~/.cargo/config.toml作为配置文件,删除旧的config新建config.toml即可
Ubuntu安装gcc-riscv64-unknown-elf包只包含编译器(gcc、objdump、ld 等),不包含 riscv64-unknown-elf-gdb,sudo apt install gcc-riscv64-unknown-elf gdb-multiarch之后还是显示没有
解决:手动在/usr/local/bin/下创建了一个可执行脚本,创建一个“伪命令”,让它指向 gdb-multiarch
sudo nano /usr/local/bin/riscv64-unknown-elf-gdb
写入
#!/bin/bash
exec gdb-multiarch "$@"
然后
sudo chmod +x /usr/local/bin/riscv64-unknown-elf-gdb
接下来再次riscv64-unknown-elf-gdb –version 即可
1.1应用程序执行环境
操作系统承接了硬件平台和各种编程语言标准库中的系统调用, 裸机程序就是没有操作系统的程序, 因此我们实现的程序需要绕过标准库直接和硬件打交道
1.2平台与目标三元组
➤ rustc --version --verbose
rustc 1.80.0-nightly (c987ad527 2024-05-01)
binary: rustc
commit-hash: c987ad527540e8f1565f57c31204bde33f63df76
commit-date: 2024-05-01
host: x86_64-unknown-linux-gnu
release: 1.80.0-nightly
LLVM version: 18.1.4
host中展示了: 目标平台是x86_64-unknown-linux-gnu,CPU架构是x86_64,CPU厂商是 unknown, 操作系统是linux,运行时库是gnu libc
1.3修改目标平台
1.4移除标准库依赖
config.toml文件用于定义和调整 Cargo 的各种行为和设置,Cargo是Rust的包管理器和构建工具,它处理依赖下载、编译过程及更多功能。.cargo/config.toml文件通常位于项目的根目录下的 .cargo 文件夹内,或者在用户的主目录下的 .cargo 文件夹内作为全局配置
一些常见配置操作:
-
设置构建目标
可以指定默认的构建目标(target triple):
[build] target = "riscv64gc-unknown-none-elf" -
设置构建标志
可以添加自定义的构建标志,比如优化等级、链接参数等:
[build] rustflags = ["-C", "opt-level=2"] -
设置环境变量
可以设置在构建脚本中使用的环境变量:
[build] rustc-env = ["RUST_BACKTRACE=1"] -
定义自定义构建目标
如果有一个自定义的架构,可以指定链接器和其他构建参数:
[target.i686-unknown-linux-gnu] linker = "gcc" -
重写依赖源
如果需要从不同的源或者私有源下载依赖项,可以重写它们:
[source.crates-io] replace-with = 'tuna' [source.tuna] registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" -
设置别名
可以为常用的命令设置别名,以便更快地调用:
[alias] b = "build" r = "run" t = "test" -
配置货物配置文件
可以为不同的构建配置(例如 debug 或 release)指定不同的设置:
[profile.dev] opt-level = 0 [profile.release] opt-level = 3
1.5移除println!宏
#![no_std] 告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core
1.6 提供语义项panic_handler
核心库 core 并没有提供这项功能需要自己实现
// os/src/lang_items.rs
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
1.6移除main函数
start 语义项代表了标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也找不到这项功能的实现
在 main.rs 的开头加入设置 #![no_main] 告诉编译器我们没有一般意义上的 main 函数, 并将原来的 main 函数删除。这样编译器也就不需要考虑初始化工作了。
1.7分析被移除标准库的函数
标准库的 main 函数在 no_std 环境下不可用,需要使用 _start 入口点,没有入口点会变成空程序
1.8 用户态最小化执行环境
入口函数_start()
// os/src/main.rs
#[no_mangle]
extern "C" fn _start() {
loop{};
}
1.9程序正常退出
执行环境还缺了一个退出机制,我们需要操作系统提供的 exit 系统调用来退出程序
代码见手册
- S模式下的操作系统设置 a0-a7 和 t0 寄存器,以指定所需的 SBI 调用和参数。
- 操作系统执行 ecall 指令,触发异常并切换到 M 模式。
- M 模式下的 SBI 实现查看寄存器的值,确定请求的服务并执行它。
- SBI 实现将结果存入寄存器中。
- 控制权返回给 S 模式的操作系统,操作系统读取寄存器以获取服务结果。
这种机制允许分离操作系统和机器模式执行环境,使得操作系统无须了解硬件的详细实现,也能够利用硬件提供的功能。
_start 函数调用了一个 sys_exit 函数, 向操作系统发出了退出的系统调用请求,退出码为 9
1.10有显示支持的用户态执行环境
代码见手册
1.11裸机启动过程
加载内核程序的命令:
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios $(BOOTLOADER) \
-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
-
-bios $(BOOTLOADER) 意味着硬件加载了一个 BootLoader 程序,即 RustSBI
-
-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) 表示硬件内存中的特定位置 $(KERNEL_ENTRY_PA) 放置了操作系统的二进制代码 $(KERNEL_BIN) 。 $(KERNEL_ENTRY_PA) 的值是 0x80200000
其中os.bin就是我们构建的裸机程序,在qemu虚拟平台上, 第一阶段的启动是qemu自己提供的程序, 第二段启动是bootloader, bootloader进行硬件相关的初始化工作, 第三个阶段是加载操作系统镜像(就是这里的裸机程序)。每一阶段的程序都需要将下一阶段程序放在指定的位置并进行跳转

1.12关机
Rust内联汇编
#[inline(always)]
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
let mut ret;
unsafe {
asm!(
"li x16, 0",
"ecall",
inlateout("x10") arg0 => ret,
in("x11") arg1,
in("x12") arg2,
in("x17") which,
);
}
ret
}
- #[inline(always)] Rust的属性 #[inline(always)] 告诉编译器这个函数应该总是被内联,也就是说,在每个调用点替换为函数体的代码,而不是实际进行函数调用。
- asm!asm! 宏是用来编写内联汇编代码的,这段代码直接使用了RISC-V的汇编语法:
- “li x16, 0”:将立即数0加载到寄存器x16中。li代表"load immediate"。
- “ecall”:陷入更低级的模式
- “inlateout(“x10”) arg0 => ret”:在ecall执行后,返回值通常存放在x10寄存器中。Rust内联汇编通过inlateout(“x10”) arg0 => ret这个约束来传递这个信息,即arg0的值在ecall执行前被放入x10寄存器中,并且ecall执行后,x10寄存器中的值会存放到变量ret中。
- in(“x11”) arg1 && in(“x12”) arg2 && in(“x17”) which:在执行汇编代码前将值加载到指定的寄存器中。在这种情况下,arg1被加载到x11寄存器,arg2被加载到x12寄存器,which被加载到x17寄存器。
所有的输入操作数都是在执行任何汇编指令之前就被放入相应寄存器中
1.13设置正确的程序内存布局
通过链接脚本 (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期
链接脚本是由 GNU 链接器 (ld) 使用的脚本语言,用于控制程序的链接过程。链接脚本的语法允许用户定义输出文件的内存布局,指定各个段的位置、大小和属性。
- 输出架构 (OUTPUT_ARCH)
- OUTPUT_ARCH(architecture)指定目标架构,告诉链接器生成针对特定架构的代码。
- 入口点 (ENTRY)
- ENTRY(symbol)指定程序的入口点,即程序开始执行的地方。
- 符号赋值
- symbol = expression;定义符号,并将其设置为特定的值或地址。
- 段定义 (SECTIONS)
- SECTIONS {…}段定义块开始和结束的标志,里面包含了对输出段的具体指令。
- 地址计数器
- . (点) 表示当前地址计数器,可以设置为特定值或者用于符号赋值。 进行字段包含后, 其会自动地增长
- 段地址和属性
- segment : { subsections }定义一个段(如 .text, .data),并指定包含在该段的子段内容。
- 子段
- *(.subsection)将特定的子段包含进父段中,如将 .text 子段包含进 .text 段中。
- 对齐指令 (ALIGN)
- ALIGN(expression)对当前地址计数器进行对齐,确保地址是特定值的倍数,常用于页对齐或数据结构对齐。
- 输出段属性
-
region指定输出段应该放置在哪个内存区域。
- AT(address)指定输出段的加载地址,与放置地址可能不同。
- :alignment指定段对齐。
- 内存布局 (MEMORY)
- MEMORY { … }定义内存布局和属性,用于告诉链接器如何使用不同的内存区域。
- /DISCARD/
- /DISCARD/ : { … }用于丢弃不需要的段,例如调试信息或未使用的段。
项目的ld文件分别将指的目标文件中各个段的内容整合在一起, 并且为每个段的开始地址声明了变量: stext, etext, erodata, edata, ebss
bootloader启动后跳转的地址是固定的(bootloader已经提供了, 不需要自己实现), 所以程序需要将起始位置放置在这固定的内存位置(此时还没有页表, 是物理内存地址)
#[no_mangle] 保持函数名,在系统编程和与外部代码交互时,我们需要确切的函数名而不能让rust自动名称修饰
直接嵌入汇编代码
#![feature(global_asm)]
global_asm!(include_str!("entry.asm"));
使用 global_asm! 宏包含全局汇编代码。这个宏允许在 Rust 代码中直接嵌入汇编代码片段,并且这些代码会在全局范围内被汇编器处理。
1.14清空 .bss 段
1.15 添加裸机打印相关函数
Rust的宏语法
#[macro_export]
macro_rules! print {
($fmt: literal $(, $($arg: tt)+)?) => {
$crate::console::print(format_args!($fmt $(, $($arg)+)?));
}
}
#[macro_export]
macro_rules! println {
($fmt: literal $(, $($arg: tt)+)?) => {
print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?));
}
}
-
#[macro_export] 表示这个宏是要被导出的,使得当这个宏所在的crate被其他crate引用时,这个宏也可以被使用。
-
macro_rules! 这是宏的声明开始,macro_rules! 是Rust中定义宏的关键字,后面跟着的 println是宏的名字。
-
宏的匹配部分 ($fmt: literal $(, $($arg: tt)+)?)
这行定义了宏的模式匹配部分。它匹配一个字面量 $fmt 作为第一个参数,后面可以跟一个逗号和任意数量的额外参数 $arg 。参数使用Rust宏的"token tree" (tt)设计,它可以匹配几乎任何Rust语法。
- $fmt: literal表示第一个参数必须是一个字面量(通常是一个字符串字面量)。
- literal: 一个特定的关键字, 用来指定宏参数应该是一个字面量
- $(, $ ($arg: tt)+)?是一个可选的模式,它使用了Rust宏的重复模式:
- $ ($arg: tt)+表示可以有一个或多个额外的参数,每个参数都是一个token tree。
rust中$和?+ 都用于宏定义中的模式匹配
- $ 符号:
- 用法1: 指示一个变量的开始,可以捕获宏输入中的相应部分。在宏规则中,$后面通常跟着一个标识符和一个冒号,再跟着一个设计符(designator),用来指定变量的类型。例如,$var:ident表示匹配一个标识符并将其绑定到变量$var中。
- 用法2: 标识重复的开始,例如$($arg:tt),*表示重复匹配$arg零次或多次,每次匹配由逗号分隔。
- ? 符号
- 它用于表示前面的模式是可选的。在宏规则中,将?放在模式的外部,表示这个模式可以出现零次或一次。这类似于正则表达式中的?运算符。例如,在$($arg:tt)?中,?表明$arg是可选的,可以有或没有。
- +符号
- 它用于表示前面的模式出现一次多次。在宏规则中,将+放在模式的外部,表示这个模式可以出现一次或多次。这类似于正则表达式中的+运算符。例如,在$($arg:tt)+中,?表明$arg以出现一次或多次
$ 捕获宏参数,而?或+指定这些参数的重复模式。所以$(, $($arg: tt)+)?这部分的意思是:
- $( … )? 表示整个模式是可选的,即可以有也可以没有。
- , 表示模式的开始,一个逗号,用来分隔参数。
- $($arg: tt)+ 表示匹配一个或多个$arg,每个$arg是一个token tree。
宏展开:
$ crate::console::print(format_args!(concat!($fmt, "\n") $(, $ ($arg)+)?))
- $ crate是一个特殊的变量,它在宏内部用于引用当前crate的根路径,这样即使宏被移动到其他crate中,它也能正确地引用到原来的crate中定义的项。
- format_args!是Rust的一个内置宏,用于处理格式化参数。它接受一个格式化字符串和对应的参数,并返回一个可以延迟计算的格式化参数结构体,这通常用于避免字符串的分配和复制。
- concat!($ fmt, “\n”)将传入的格式化字符串和一个换行符连在一起,确保输出后会自动换行。
- $(, $ ($arg)+)?是对传入参数的引用,如果有参数的话,它们会被插入到格式化参数结构体中。