# 内存布局

## 计算机组成

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

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

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

还差什么呢？硬盘。

## OpenSBI

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

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

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

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

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

{% hint style="success" %}
知识点

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

RISCV有四种**特权级（privilege level）**。
{% endhint %}

| Level | Encoding | 全称                 | 简称 |
| ----- | -------- | ------------------ | -- |
| 0     | 00       | User/Application   | U  |
| 1     | 01       | Supervisor         | S  |
| 2     | 10       | Reserved(目前未使用，保留) |    |
| 3     | 11       | Machine            | M  |

{% hint style="success" %}
粗略的分类：

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

详细内容请自行查阅RISC-v手册。
{% endhint %}

## elf与bin

我们可以想象这样的过程：操作系统的二进制可执行文件被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)。

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

`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文件的内存布局。
