[RustSBI output]
Hello, world!
.text [0x80200000, 0x80202000)
.rodata [0x80202000, 0x80203000)
.data [0x80203000, 0x80203000)
boot_stack [0x80203000, 0x80213000)
.bss [0x80213000, 0x80213000)
Panicked at src/main.rs:46 Shutdown machine!
除了显示 Hello, world! 之外还有一些额外的信息,最后关机。
./os/src
Rust 4 Files 119 Lines
Assembly 1 Files 11 Lines
├── bootloader(内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI)
│ ├── rustsbi-k210.bin(可运行在 k210 真实硬件平台上的预编译二进制版本)
│ └── rustsbi-qemu.bin(可运行在 qemu 虚拟机上的预编译二进制版本)
├── os(我们的内核实现放在 os 目录下)
│ ├── Cargo.toml(内核实现的一些配置文件)
│ ├── Makefile
│ └── src(所有内核的源代码放在 os/src 目录下)
│ ├── console.rs(将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出)
│ ├── entry.asm(设置内核执行环境的的一段汇编代码)
│ ├── lang_items.rs(需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑)
│ ├── linker-qemu.ld(控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上)
│ ├── main.rs(内核主函数)
│ └── sbi.rs(调用底层 SBI 实现提供的 SBI 接口)
bss段:
data段:
text段:
堆(heap):
栈(stack):
栈(stack):
从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区
OS编程与应用编程的一个显著区别是,OS编程需要理解栈上的物理内存结构和机器级内容。
Q: C语言与Rust语言中的 static 变量位于执行程序的哪个区域?
Q: C语言与Rust语言中的 static 变量位于执行程序的哪个区域?
A: static变量算是一种全局变量,位于data段。
# os/src/linker-qemu.ld
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;
SECTIONS
{
. = BASE_ADDRESS;
skernel = .;
stext = .;
.text : {
*(.text.entry)
.bss : {
*(.bss.stack)
sbss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
BSS:Block Started by Symbol
SBSS:small bss,近数据
rust-objcopy --strip-all \
target/riscv64gc-unknown-none-elf/release/os \
-O binary target/riscv64gc-unknown-none-elf/release/os.bin
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios ../bootloader/rustsbi-qemu.bin \
-device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 \
-s -S
riscv64-unknown-elf-gdb \
-ex 'file target/riscv64gc-unknown-none-elf/release/os' \
-ex 'set arch riscv:rv64' \
-ex 'target remote localhost:1234'
[GDB output]
0x0000000000001000 in ?? ()
伪指令 | 基本指令 | 含义 |
---|---|---|
ret | jalr x0, x1, 0 | 函数返回 |
call offset | auipc x6, offset[31:12]; jalr x1, x6, offset[11:0] | 函数调用 |
auipc(add upper immediate to pc)被用来构建 PC 相对的地址,使用的是 U 型立即数。 auipc 以低 12 位补 0,高 20 位是 U 型立即数的方式形成 32 位偏移量,然后和 PC 相加,最后把结果保存在寄存器 x1。
伪指令 ret 翻译为 jalr x0, 0(x1),含义为跳转到寄存器 ra(即x1)保存的地址。
快速入门RISC-V汇编的文档
伪指令 | 基本指令 | 含义 |
---|---|---|
ret | jalr x0, x1, 0 | 函数返回 |
call offset | auipc x6, offset[31:12]; jalr x1, x6, offset[11:0] | 函数调用 |
函数调用核心机制
函数调用约定
函数调用约定 (Calling Convention) 约定在某个指令集架构上,某种编程语言的函数调用如何实现。它包括了以下内容:
RISC-V函数调用约定
调用参数 返回值
RISC-V函数调用约定
RISC-V函数调用约定
栈帧(Stack Frames)
return address *
previous fp
saved registers
local variables
…
return address fp register
previous fp (pointed to *)
saved registers
local variables
… sp register
RISC-V函数调用约定
RISC-V函数调用约定
pc = return address
sp = fp + ENTRY_SIZE
fp = previous fp
RISC-V函数调用约定
函数结构组成:prologue
,body part
和epilogue
.global sum_then_double
sum_then_double:
addi sp, sp, -16 # prologue
sd ra, 0(sp)
call sum_to # body part
li t0, 2
mul a0, a0, t0
ld ra, 0(sp) # epilogue
addi sp, sp, 16
ret
RISC-V函数调用约定
函数结构组成:prologue
,body part
和epilogue
.global sum_then_double
sum_then_double:
call sum_to # body part
li t0, 2
mul a0, a0, t0
ret
Q:上述代码的执行与前一页的代码执行相比有何不同?
分配并使用启动栈 快速入门RISC-V汇编的文档
# os/src/entry.asm
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main
.section .bss.stack
.globl boot_stack
boot_stack:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:
分配并使用启动栈
# os/src/linker-qemu.ld
.bss : {
*(.bss.stack)
sbss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
}
ebss = .;
在链接脚本 linker.ld 中 .bss.stack 段最终会被汇集到 .bss 段中
.bss 段一般放置需要被初始化为零的数据
将控制权转交给 Rust 代码,该入口点在 main.rs 中的rust_main
函数
// os/src/main.rs
pub fn rust_main() -> ! {
loop {}
}
fn
关键字:函数; pub
关键字:对外可见,公共的loop
关键字:循环清空bss段(未初始化数据段)
pub fn rust_main() -> ! {
clear_bss(); //调用清空bss的函数clear_bss()
}
fn clear_bss() {
extern "C" {
fn sbss(); //bss段的起始地址
fn ebss(); //bss段的结束地址
}
//对[sbss..ebss]这段内存空间清零
(sbss as usize..ebss as usize).for_each(|a| {
unsafe { (a as *mut u8).write_volatile(0) }
});
}
在屏幕上打印 Hello world!
SBI服务编号
// os/src/sbi.rs
const SBI_SET_TIMER: usize = 0;
const SBI_CONSOLE_PUTCHAR: usize = 1;
const SBI_CONSOLE_GETCHAR: usize = 2;
const SBI_CLEAR_IPI: usize = 3;
const SBI_SEND_IPI: usize = 4;
const SBI_REMOTE_FENCE_I: usize = 5;
const SBI_REMOTE_SFENCE_VMA: usize = 6;
const SBI_REMOTE_SFENCE_VMA_ASID: usize = 7;
const SBI_SHUTDOWN: usize = 8;
usize
机器字大小的无符号整型// os/src/sbi.rs
#[inline(always)] //总是把函数展开
fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize {
let mut ret; //可修改的变量ret
unsafe {
asm!(//内嵌汇编
"ecall", //切换到更高特权级的机器指令
inlateout("x10") arg0 => ret, //SBI参数0&返回值
in("x11") arg1, //SBI参数1
in("x12") arg2, //SBI参数2
in("x17") which, //SBI编号
);
}
ret //返回ret值
}
在屏幕上输出一个字符
// os/src/sbi.rs
pub fn console_putchar(c: usize) {
sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0);
}
关机
// os/src/sbi.rs
pub fn shutdown() -> ! {
sbi_call(SBI_SHUTDOWN, 0, 0, 0);
panic!("It should shutdown!");
}
panic!
和println!
是一个宏(类似C的宏),!
是宏的标志优雅地处理错误panic
#[panic_handler]
fn panic(info: &PanicInfo) -> ! { //PnaicInfo是结构类型
if let Some(location) = info.location() { //出错位置存在否?
println!(
"Panicked at {}:{} {}",
location.file(), //出错的文件名
location.line(), //出错的文件中的行数
info.message().unwrap() //出错信息
);
} else {
println!("Panicked: {}", info.message().unwrap());
}
shutdown() //关机
}
优雅地处理错误panic
pub fn rust_main() -> ! {
clear_bss();
println!("Hello, world!");
panic!("Shutdown machine!");
}
运行结果
[RustSBI output]
Hello, world!
Panicked at src/main.rs:26 Shutdown machine!
https://blog.51cto.com/onebig/2551726 (深入理解计算机系统) bss段,data段、text段、堆(heap)和栈(stack)
https://blog.csdn.net/zoomdy/article/details/79354502 RISC-V Assembly Programmer's Manual https://shakti.org.in/docs/risc-v-asm-manual.pdf https://github.com/riscv-non-isa/riscv-asm-manual/blob/master/riscv-asm.md