rCore从汇编到 Rust 的衔接详解
本文完整解析 os/src/entry.asm 与 os/src/main.rs 之间的关联,说明底层汇编如何与 Rust 内核协作、符号是如何在链接时解析的、以及 #[no_mangle] 的必要性。
这部分内容是理解操作系统启动机制的关键环节。
一、总体概览
当计算机启动后,Bootloader(例如 RustSBI) 会完成基本的硬件初始化,并将内核(kernel)加载到内存中。接下来,它会跳转到内核的入口地址,也就是我们在 entry.asm 文件中定义的 _start 标签。
因此,entry.asm 的使命非常单纯且关键:
- 建立最基础的执行环境:为 Rust 代码准备一个可用的栈空间(Stack)。
- 转交控制权:跳转到用 Rust 编写的内核主函数
rust_main。
二、文件结构分为两部分
entry.asm 通常由两个主要部分构成:
- 代码区 (
.text.entry) - 数据区 (
.bss.stack)
(1)代码区:建立栈并跳转到 Rust
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main
逐行说明如下:
1. .section .text.entry
.section是汇编器指令(Assembler Directive),告诉汇编器接下来的内容属于哪个段(Section)。.text是标准的“代码段”,存放可执行指令。.text.entry是一个更具体的命名,用于标记“程序入口部分”。 链接脚本(如linker-qemu.ld)会确保.text.entry位于整个程序最前面,以便 Bootloader 能找到。
2. .globl _start
.globl(或.global)使_start成为全局符号,对链接器可见。_start:定义了一个标签,标识该行所在的内存地址。- 在最终生成的可执行文件中,链接器会将
_start标记为整个程序的入口地址。Bootloader 跳转到的正是这里。
3. la sp, boot_stack_top
la是 Load Address(加载地址)的伪指令。- 它将
boot_stack_top标签所代表的内存地址加载进sp(栈指针寄存器)。 - 栈(Stack)是函数调用的基础,
call指令执行时会自动将返回地址压入栈中。 - 因此在调用
rust_main之前,必须先为sp指定一个有效的栈区域,否则程序会崩溃。
4. call rust_main
-
这是一个函数调用伪指令:
- 将下一条指令的地址存入
ra(Return Address)寄存器。 - 跳转到
rust_main函数的地址执行。
- 将下一条指令的地址存入
- 从此刻开始,CPU 就正式进入 Rust 代码。
- 关键假设:
rust_main不会返回。如果返回,ra会试图跳到call的下一条指令(实际上没有),导致系统崩溃。
(2)数据区:定义栈空间
.section .bss.stack
.globl boot_stack_lower_bound
boot_stack_lower_bound:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:
逐行说明:
1. .section .bss.stack
.bss是标准段名,用于未初始化的全局变量或静态变量。.bss.stack是一个命名约定,专门为栈保留空间。.bss段在可执行文件中不占空间,加载时系统会为其分配内存并清零。
2. .globl boot_stack_lower_bound
- 定义一个全局标签,标记栈的最低地址(内存分配的起点)。
3. .space 4096 * 16
.space用于预留指定字节数的空间。4096是 4 KiB(一个内存页)。4096 * 16= 64 KiB,因此我们分配了一个 64 KiB 的启动栈。
4. .globl boot_stack_top
- 定义栈顶(最高地址)标签。
- 它紧跟
.space之后,所以位置正好是那 64 KiB 空间的顶端。
(3)栈的布局图示
在 RISC-V 架构中,栈是向下增长的。也就是说,每次压栈操作会让 sp 减小。
High Address +---------------------+ <-- boot_stack_top (sp 初始化指向这里)
| |
| Stack Memory |
| (Grows downward) |
| ↓ |
| |
Low Address +---------------------+ <-- boot_stack_lower_bound
la sp, boot_stack_top 就是让栈指针从这块区域的顶部开始。
三、main.rs 中的 rust_main 与汇编文件的衔接
在 os/src/main.rs 中,我们看到如下关键内容:
global_asm!(include_str!("entry.asm"));
#[no_mangle]
pub fn rust_main() -> ! {
unsafe extern "C" {
fn boot_stack_lower_bound();
fn boot_stack_top();
}
println!(
"[kernel] boot_stack top={:#x}, lower_bound={:#x}",
boot_stack_top as usize,
boot_stack_lower_bound as usize
);
// ... 内核主逻辑 ...
}
这段 Rust 代码与汇编的协作过程可以分为三个阶段理解。
阶段 1:汇编导出符号(entry.asm)
.globl boot_stack_lower_bound
boot_stack_lower_bound:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:
.globl使得boot_stack_top和boot_stack_lower_bound成为全局可见符号。- 它们分别代表 64 KiB 栈空间的两端地址。
- 当该文件被编译成目标文件(
.o)时,符号表中会包含这两个符号和它们的实际地址。
换句话说,entry.asm 在告诉编译系统:“我这里定义了这两个地址,其他文件可以引用它们。”
阶段 2:Rust 引入并声明外部符号
1. global_asm!(include_str!("entry.asm"));
- Rust 宏,指示编译器在编译阶段直接把
entry.asm内容汇编进去。 - 这样 Rust 的 crate 就包含了这段汇编代码。
2. extern "C" 声明
unsafe extern "C" {
fn boot_stack_lower_bound();
fn boot_stack_top();
}
extern "C"块告诉编译器:这些符号定义在外部(C 或汇编中)。- 这里的
fn并不代表可调用函数,而是一个标签地址。 "C"表示这些符号使用 C ABI(名称不会被 Rust 改动或修饰)。
这样 Rust 就能在编译时引用 boot_stack_top 和 boot_stack_lower_bound,即使它们定义在汇编中。
阶段 3:链接器解析符号
编译器分别会产生:
- Rust 代码的目标文件(包含对外部符号的引用)
- 汇编的目标文件(包含符号定义)
链接器(Linker) 在最终阶段做了三件事:
- 找到 Rust 代码中引用的符号(
boot_stack_top、boot_stack_lower_bound)。 - 在汇编目标文件中找到同名符号的定义。
- 将 Rust 引用处的符号地址替换为实际的地址。
最终生成的内核镜像中,Rust 代码引用的符号都变成了真实的内存地址。
阶段 4:#[no_mangle] 的作用
什么是名字修饰(Name Mangling)
Rust、C++ 等现代语言在编译时会对函数名做“修饰”,例如:
fn print_thing(x: i32) {}
可能会变成 _ZN11mycrate11print_thing17h2f3a9e9f7b...E。
这样做的目的是支持命名空间、泛型等特性。
但在底层汇编中,call rust_main 这条指令只会去找字面名叫 rust_main 的符号。如果 Rust 编译后函数名被改成了 _ZN...,链接器就会找不到,报出:
undefined reference to `rust_main`
#[no_mangle] 的作用
#[no_mangle] 告诉编译器:“不要改动这个函数的名字,在目标文件中保留原样。”
#[no_mangle]
pub fn rust_main() -> ! { ... }
这确保了:
- 汇编中的
call rust_main和 Rust 的rust_main能成功匹配。 - 链接器能正确把两者对接起来。
四、完整启动流程总结
| 阶段 | 参与者 | 关键动作 | 说明 |
|---|---|---|---|
| 1 | 汇编 (entry.asm) | 定义 _start、设置栈、导出符号 | 初始化栈并跳转到 Rust |
| 2 | Rust (main.rs) | 导入汇编 (global_asm!),声明外部符号 | 通过 extern "C" 引用汇编定义 |
| 3 | 编译 | 汇编和 Rust 代码生成目标文件 | 各自符号独立存在 |
| 4 | 链接器 | 解析并绑定符号 | 将 Rust 的符号引用替换为汇编定义地址 |
| 5 | 启动 | _start 执行 la sp, boot_stack_top,然后 call rust_main | 从汇编世界切换到 Rust 世界 |
五、最终运行内存示意图
Memory Layout:
+-----------------------------+ ← boot_stack_top (sp 初始指向)
| 栈空间(64 KiB) |
| 向下增长 (push) |
| |
+-----------------------------+ ← boot_stack_lower_bound
| 其他内核数据段 |
| Rust 代码区 (.text) |
| entry.asm 中的 _start |
+-----------------------------+
运行顺序如下:
- Bootloader 跳转到
_start _start设置栈 (sp = boot_stack_top)call rust_main- Rust 接管控制权,开始内核逻辑
六、总结(核心逻辑)
entry.asm定义启动入口与栈,并导出符号。main.rs用global_asm!引入汇编,用extern "C"引用符号。#[no_mangle]确保 Rust 函数名与汇编调用名一致。- 链接器在最终阶段完成符号地址替换。
_start初始化栈后调用rust_main,控制权正式转交给 Rust。