rCore-Tutorial #1 应用程序与基本执行环境[WIP]

yyi
yyi
2024-02-24 / 0 评论 / 45 阅读 / 正在检测是否收录...

概述

第一章就一个基本目标,说起来很快,实际上难度不小:隔离应用与硬件。更像一堆库函数,为硬件提供一层简单接口,让应用可以通过这层接口访问硬件。

阶段一的OS称为LibOS

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

image-20240223215834384

此时编译,会出现找不到std的问题

image-20240223215954273

当然,预料之中,我们在main的开头加上\#![no_std],告诉Rust不要用Rust标准库。

Rust会自动使用不依赖os的core库

image-20240223220131838

我们需要提供panic_handler,来处理致命系统错误,按教程提供后,现在应该长这样:

image-20240223220512814

移除main中的println,再次编译的时候出现了新的错误(这个错误和教程不太一样)

image-20240223220811137

虽然不太一样但是意思是一致的,main本身是依赖标准库的,如果你熟悉实际上的C编程,会有印象大部分的linux ELF会有个.init,然后调用libc_start_main,才进入真正的main函数。

main只是对编译器有意义罢了,并不是说我们一定必须进去。所以,我们把main删掉,按编译提示加入no_main注解。

此时的程序终于可以通过编译了,不过也有点可怜

image-20240223221102661

接下来会二进制分析已经编译出的程序,看看我们应该怎么办

image-20240223221304464

入口点是0,0显然不会是一个可执行的代码地址

内核的第一条指令

这节分为基础篇和实践篇,我们从基础篇的第二节开始,看看Qemu是怎么执行的。(其他知识已经很熟了,我这边先行跳过,同学有需要可以自行查阅)

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是否正确的把控制权流转给了我们的代码

image-20240223222955813

当然,做到这还不够,我们希望指令出现在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)
    }
}

配置过程文档里都有,我不赘述了

image-20240223223428232

这块的意思就是把所有目标文件的.text.entry段、.text段,.text.*段都放到最后链接完毕的文件的.text段中,这里的*统统是通配符,括号外代表目标文件,括号内代表目标文件的段。按从上到下的顺序组织,以BASE_ADDRESS为基准偏移。

image-20240223223855907

小功告成!此时我们是不是可以直接扔给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

评论 (0)

取消