rCore从汇编到 Rust 的衔接详解

本文完整解析 os/src/entry.asmos/src/main.rs 之间的关联,说明底层汇编如何与 Rust 内核协作、符号是如何在链接时解析的、以及 #[no_mangle] 的必要性。

这部分内容是理解操作系统启动机制的关键环节。


一、总体概览

当计算机启动后,Bootloader(例如 RustSBI) 会完成基本的硬件初始化,并将内核(kernel)加载到内存中。接下来,它会跳转到内核的入口地址,也就是我们在 entry.asm 文件中定义的 _start 标签。

因此,entry.asm 的使命非常单纯且关键:

  1. 建立最基础的执行环境:为 Rust 代码准备一个可用的栈空间(Stack)。
  2. 转交控制权:跳转到用 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

  • laLoad Address(加载地址)的伪指令。
  • 它将 boot_stack_top 标签所代表的内存地址加载进 sp(栈指针寄存器)。
  • 栈(Stack)是函数调用的基础,call 指令执行时会自动将返回地址压入栈中。
  • 因此在调用 rust_main 之前,必须先为 sp 指定一个有效的栈区域,否则程序会崩溃。

4. call rust_main

  • 这是一个函数调用伪指令:

    1. 将下一条指令的地址存入 ra(Return Address)寄存器。
    2. 跳转到 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_topboot_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_topboot_stack_lower_bound,即使它们定义在汇编中。


阶段 3:链接器解析符号

编译器分别会产生:

  • Rust 代码的目标文件(包含对外部符号的引用)
  • 汇编的目标文件(包含符号定义)

链接器(Linker) 在最终阶段做了三件事:

  1. 找到 Rust 代码中引用的符号(boot_stack_topboot_stack_lower_bound)。
  2. 在汇编目标文件中找到同名符号的定义。
  3. 将 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    |
+-----------------------------+

运行顺序如下:

  1. Bootloader 跳转到 _start
  2. _start 设置栈 (sp = boot_stack_top)
  3. call rust_main
  4. Rust 接管控制权,开始内核逻辑

六、总结(核心逻辑)

  1. entry.asm 定义启动入口与栈,并导出符号。
  2. main.rsglobal_asm! 引入汇编,用 extern "C" 引用符号。
  3. #[no_mangle] 确保 Rust 函数名与汇编调用名一致。
  4. 链接器在最终阶段完成符号地址替换。
  5. _start 初始化栈后调用 rust_main,控制权正式转交给 Rust。