# 内存布局

## 计算机组成

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

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://nankai.gitbook.io/ucore-os-on-risc-v64/lab0.5/nei-cun-bu-ju.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
