ucore step by step
tutorial
tutorial
  • 欢迎来到ucore step-by-step的世界
  • lab0: 预备起
    • 溯源: ucore的历史
    • 概览: 指导书的结构
    • 开搞: 实验环境搭建
  • lab0.5: 比麻雀更小的麻雀(最小可执行内核)
    • 内存布局,OpenSBI,elf和bin
    • 链接脚本和入口点
    • "真正的"入口点
    • 从SBI到stdio
    • Just make it
    • 项目组成和执行流
  • lab1: 断, 都可以断
    • 掉进兔子洞(中断入口点)
    • 中断处理程序
    • 滴答滴答(时钟中断)
    • 项目组成和执行流
  • lab2: 物理内存和页表
    • 内核初始映射
    • 物理内存管理
    • 页面分配算法
    • 项目组成和执行流
  • lab3: 缺页异常和页面置换
    • 扫清外围
    • 使用多级页表
    • 页面置换机制
    • FIFO置换算法
    • 项目组成和执行流
  • lab4: 进程管理
    • 进程与线程
    • 相关数据结构
    • 进程模块初始化
    • 进程切换
    • 项目组成和执行流
  • lab5: 用户程序
    • 用户程序
    • system call!
    • 中断处理
    • 项目组成和执行流
  • lab6: 进程调度
    • 再次认识进程切换
    • 调度算法框架
    • 项目组成和执行流
  • lab7: 同步互斥
    • 同步互斥的基本概念
    • 信号量
    • 条件变量与管程
    • 项目组成和执行流
  • lab8: 文件系统
    • 文件系统抽象层VFS
    • 硬盘文件系统SFS
    • 设备即文件
    • 从zhong duan 到 zhong duan
    • 项目组成和执行流
  • 练习题
    • lab1
    • lab2
    • lab3
    • lab4
    • lab5
    • lab6
    • lab7
    • lab8
  • 附录
    • makefile
由 GitBook 提供支持
在本页

这有帮助吗?

  1. lab0.5: 比麻雀更小的麻雀(最小可执行内核)

内存布局,OpenSBI,elf和bin

最小可执行内核里, 我们主要完成两件事:

  1. 内核的内存布局和入口点设置

  2. 通过sbi封装好输入输出函数

首先我们回顾计算机的组成:

CPU, 存储设备(粗略地说,包括断电后遗失的内存,和断电后不遗失的硬盘),输入输出设备,总线。

QEMU会帮助我们模拟一块riscv64的CPU,一块物理内存,还会借助你的电脑的键盘和显示屏来模拟命令行的输入和输出。虽然QEMU不会真正模拟一堆线缆,但是总线的通信功能也在QEMU内部实现了。

还差什么呢?硬盘。

我们需要硬盘上的程序和数据。比如崭新的windows电脑里C盘已经被占据的二三十GB空间,除去预装的应用软件,还有一部分是windows操作系统的内核。在插上电源开机之后,就需要运行操作系统的内核,然后由操作系统来管理计算机。

问题在于,操作系统作为一个程序,必须加载到内存里才能执行。而“把操作系统加载到内存里”这件事情,不是操作系统自己能做到的,就好像你不能拽着头发把自己拽离地面。

因此我们可以想象,在操作系统执行之前,必然有一个其他程序执行,他作为“先锋队”,完成“把操作系统加载到内存“这个工作,然后他功成身退,把CPU的控制权交给操作系统。

这个“其他程序”,我们一般称之为bootloader. 很好理解:他负责boot(开机),还负责load(加载OS到内存里),所以叫bootloader.

在QEMU模拟的riscv计算机里,我们使用QEMU自带的bootloader: OpenSBI固件。

须知

在计算机中,固件(firmware)是一种特定的计算机软件,它为设备的特定硬件提供低级控制,也可以进一步加载其他软件。固件可以为设备更复杂的软件(如操作系统)提供标准化的操作环境。对于不太复杂的设备,固件可以直接充当设备的完整操作系统,执行所有控制、监视和数据操作功能。 在基于 x86 的计算机系统中, BIOS 或 UEFI 是固件;在基于 riscv 的计算机系统中,OpenSBI 是固件。OpenSBI运行在M态(M-mode),因为固件需要直接访问硬件。

RISCV有四种特权级(privilege level)。

Level

Encoding

全称

简称

0

00

User/Application

U

1

01

Supervisor

S

2

10

Reserved(目前未使用,保留)

3

11

Machine

M

粗略的分类:

U-mode是用户程序、应用程序的特权级,S-mode是操作系统内核的特权级,M-mode是固件的特权级。

我们可以想象这样的过程:操作系统的二进制可执行文件被OpenSBI加载到内存中,然后OpenSBI会把CPU的"当前指令指针"(pc, program counter)跳转到内存里的一个位置,开始执行内存中那个位置的指令。

OpenSBI怎样知道把操作系统加载到内存的什么位置?总不能随便选个位置。也许你会觉得可以把操作系统的代码总是加载到固定的位置,比如总是加载到内存地址最高的地方。

问题在于,之后OpenSBI还要把CPU的program counter跳转到一个位置,开始操作系统的执行。如果加载操作系统到内存里的时候随便加载,那么OpenSBI怎么知道把program counter跳转到哪里去呢?难道操作系统的二进制可执行文件需要提供“program counter跳转到哪里"这样的信息?

实际上,操作系统的二进制可执行文件,会指定它自己应该被加载到内存的哪个地址。而OpenSBI会很听话地把二进制可执行文件放到她想去的位置上。但是关于program counter的跳转,OpenSBI是独断专行的,总是会把program counter跳到0x80200000这个内存地址开始执行, 所以故事(版本1)其实是这样的:

​ OpenSBI: 操作系统, 你到0x8020000等着program counter跳过来执行!

​ 操作系统:好的!请把我加载到xxxxxx这个位置,这样program counter跳过来的时候就不会出问题了。

实际上,二进制程序加载到内存中是一件很精细的工作。一个二进制程序包括很多section, 如text(程序代码),bss(需要初始化为零的数据),rodata(只读数据)。二进制程序的每个section都可以指定一个希望被加载到的内存地址。

故事可以是这样的吗?(版本2)

​ OpenSBI: 操作系统, 你到0x8020000等着program counter跳过来执行!

​ 操作系统:好的!请把我的text section加载到A位置,data section加载到B位置,rodata section加载到C位置......这样program counter跳过来的时候就不会出问题了!

​ OpenSBI: 你说啥?

两个版本的故事是因为,我们有两种不同的可执行文件格式:elf(e是executable的意思, l是linkable的意思,f是format的意思)和bin(binary)。

bin文件就比较简单了,简单地在文件头之后解释自己应该被加载到什么起始位置。OpenSBI可以理解得很清楚,这就是版本1的故事。

我们举一个例子解释elf和bin文件的区别:初始化为零的一个大数组,在elf文件里是bss数据段的一部分,只需要记住这个数组的起点和终点就可以了,等到加载到内存里的时候分配那一段内存。但是在bin文件里,那个数组有多大,有多少个字节的0,bin文件就要对应有多少个零。所以如果一个程序里声明了一个大全局数组(默认初始化为0),那么可能编译出来的elf文件只有几KB, 而生成bin文件之后却有几MB, 这是很正常的。实际上,可以认为bin文件会把elf文件指定的每段的内存布局都映射到一块线性的数据里,这块线性的数据(或者说程序)加载到内存里就符合elf文件之前指定的布局。

那么我们的任务就明确了:得到内存布局合适的elf文件,然后把它转化成bin文件(这一步通过objcopy实现),然后加载到QEMU里运行(QEMU自带的OpenSBI会干这个活)。下面我们来看如何设置elf文件的内存布局。

上一页lab0.5: 比麻雀更小的麻雀(最小可执行内核)下一页链接脚本和入口点

最后更新于5年前

这有帮助吗?

elf文件()比较复杂,包含一个文件头(ELF header), 包含冗余的调试信息,指定程序每个section的内存布局,需要解析program header才能知道各段(section)的信息。如果我们已经有一个完整的操作系统来解析elf文件,那么elf文件可以直接执行。但是对于OpenSBI来说,elf格式还是太复杂了,把操作系统内核的elf文件交给OpenSBI就会发生版本2的悲惨故事。

wikipedia: elf