概述
第一章就一个基本目标,说起来很快,实际上难度不小:隔离应用与硬件。更像一堆库函数,为硬件提供一层简单接口,让应用可以通过这层接口访问硬件。
阶段一的OS称为LibOS
这个系统的bootloader由RustSBI完成,OS负责为APP初始化加载环境,然后就跳转到app执行,app通过函数调用获得OS提供的输出字符串等服务。
移除标准库依赖
我跳过了第一节,老知识了,需要的同学自行查阅吧。
本节目标是构建一个最小环境,使得程序可以打印Hello, world。
在之前的编程里,我们都是通过Rust std实现的,我们要扔掉std(std本身也依赖于系统调用),直达硬件。
移除println!
我们首先准备一个干净的环境,在原来基础上checkout到我们自己的分支
git checkout -b my_ch1
rm -rf os
# 新建我们的项目
cargo new os
# 修改编译target arch
cd os && mkdir .cargo
echo '# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"' \
>> .cargo/config
此时编译,会出现找不到std的问题
当然,预料之中,我们在main的开头加上\#![no_std],告诉Rust不要用Rust标准库。
Rust会自动使用不依赖os的core库
我们需要提供panic_handler,来处理致命系统错误,按教程提供后,现在应该长这样:
移除main中的println,再次编译的时候出现了新的错误(这个错误和教程不太一样)
虽然不太一样但是意思是一致的,main本身是依赖标准库的,如果你熟悉实际上的C编程,会有印象大部分的linux ELF会有个.init,然后调用libc_start_main,才进入真正的main函数。
main只是对编译器有意义罢了,并不是说我们一定必须进去。所以,我们把main删掉,按编译提示加入no_main注解。
此时的程序终于可以通过编译了,不过也有点可怜
接下来会二进制分析已经编译出的程序,看看我们应该怎么办
入口点是0,0显然不会是一个可执行的代码地址
内核的第一条指令
这节分为基础篇和实践篇,我们从基础篇的第二节开始,看看Qemu是怎么执行的。(其他知识已经很熟了,我这边先行跳过,同学有需要可以自行查阅)
qemu会把bootloader和内核镜像分别加载到0x80000000 (虚拟设备内存起始地址)和0x80200000(我们指定的地址),具体如何boot是由rustsbi帮我们完成的。正常的Boot会有Bios引导,先把bootloader加载到内存,然后控制权转交给bootloader,bootloader对CPU初始化,把OS加载到内存,最后把控制权流转给内核。
但是我们这不是个正常的boot,由qemu负责把bootloader和os都加载到内存,再把控制权流转给bootloader,不管怎么样,我们知道RustSBI帮我们初始化了一些事,只要我们能控制0x80200000,我们就能获得CPU控制权了。
编写第一条指令
# os/src/entry.asm
.section .text.entry
.globl _start
_start:
li x1, 100
它只执行了一条简单的指令,即l(oad)i(mm) x1, 100把立即数100赋值给寄存器x1
这里还定义了全局符号_start,并声明这段代码应该在.text.entry上,
我们把这段汇编嵌入到rust中。并通过它检测qemu是否正确的把控制权流转给了我们的代码
当然,做到这还不够,我们希望指令出现在0x80200000,熟悉编译的同学都知道,这个最后在哪是链接器决定的,所以通过修改cargo配置文件,我们使用自定义链接器。
// os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"
[target.riscv64gc-unknown-none-elf]
rustflags = [
"-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
]
// os/src/linker.ld
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;
SECTIONS
{
. = BASE_ADDRESS;
skernel = .;
stext = .;
.text : {
*(.text.entry)
*(.text .text.*)
}
. = ALIGN(4K);
etext = .;
srodata = .;
.rodata : {
*(.rodata .rodata.*)
*(.srodata .srodata.*)
}
. = ALIGN(4K);
erodata = .;
sdata = .;
.data : {
*(.data .data.*)
*(.sdata .sdata.*)
}
. = ALIGN(4K);
edata = .;
.bss : {
*(.bss.stack)
sbss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
}
. = ALIGN(4K);
ebss = .;
ekernel = .;
/DISCARD/ : {
*(.eh_frame)
}
}
配置过程文档里都有,我不赘述了
这块的意思就是把所有目标文件的.text.entry段、.text段,.text.*段都放到最后链接完毕的文件的.text段中,这里的*统统是通配符,括号外代表目标文件,括号内代表目标文件的段。按从上到下的顺序组织,以BASE_ADDRESS为基准偏移。
小功告成!此时我们是不是可以直接扔给qemu了呢?别急还有点早。因为qemu会把整个ELF load到它的物理内存里。我们的代码段之前是存在一些其他的元信息的,通过objcopy直接获得程序真正的部分
rust-objcopy --strip-all target/riscv64gc-unknown-none-elf/release/os \
-O binary target/riscv64gc-unknown-none-elf/release/os.bin
评论 (0)