# Lab 0: Booting

负责助教：[孙一](mailto:suny21@m.fudan.edu.cn)

本学期，我们将实现一个简单的操作系统内核。在 Lab 0 中，我们将配置好实验环境并完成 3 个实验任务。

## 1. 服务器操作

**连接服务器**

```shell
ssh root@10.176.34.210 -p 你的端口号
```

考虑到项目文件数目较多，我们推荐使用 [VS Code 的 Remote 插件](https://code.visualstudio.com/docs/remote/ssh) 进行远程开发。

**服务器概况**

容器内有两个特殊的目录（挂载点）：

* `/share` 目录只读，供所有同学共享，其中放置一些共享文件。
* `~/data` 目录可读写，挂载了一块4.4T的硬盘，建议在该目录下存储实验所用到的文件。

CPU、内存和硬盘资源所有同学共享，无使用配额，请注意合理使用资源。

## 2. 配置代码**仓库**

#### 在服务器上生成 SSH 密钥对

```sh
# 在终端输入，替换自己的邮箱地址
ssh-keygen -t rsa -b 4096 -C "your_email@example.com" 
​
# 按照提示连续按三次回车，即可生成ssh密钥
cat ~/.ssh/id_rsa.pub
# 观察到输出为：ssh-rsa xxxxxxxxxxxxxxx = xxxxx@xxxx.xx 即正确
```

#### 在 Github 上配置 SSH 公钥

1. 复制 `cat ~/.ssh/id_rsa.pub` 输出的结果（即公钥）。
2. 打开 Github 并登录自己的账号。
3. 点击右上角头像，进入 Settings ：

<figure><img src="/files/CCFXggYxlZbaPdxxhaEP" alt="" width="265"><figcaption></figcaption></figure>

4. 进入页面后，在左侧选择 `SSH and GPG keys`, 在右侧点击 `New SSH Key`。

<figure><img src="/files/VtetG9eWBQckif93wUkb" alt="" width="563"><figcaption></figcaption></figure>

5. 在框中粘贴入自己复制的公钥，点击 Add SSH key 即可。

#### 使用 SSH 克隆仓库

```sh
cd ~/data
​
# SSH克隆代码仓库（HTTPS克隆不稳定）
git clone git@github.com:FDUCSLG/OS-24Fall-FDU.git
```

{% hint style="info" %}
如果出现报错：（成功克隆则进入下一步）

```
Cloning into 'OS-24Fall-FDU'...
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
​
Please make sure you have the correct access rights
and the repository exists.
```

请检查前两步：在服务器上生成 SSH 和在 Github 上配置 SSH 是否正确完成。
{% endhint %}

#### 进入仓库，开始实验

```sh
cd OS-24Fall-FDU
​
# 切换到本实验分支
git checkout lab0
​
# 新建一个dev分支
git checkout -b lab0-dev
​
# 创建build目录用来构建运行内核
mkdir -p build
cd build
cmake ..
```

之后每次构建运行内核只要在 `build` 目录下 `cmake .. && make qemu` 。

**更新代码仓库**

本次实验中无需执行此部分，后续新实验发布后可参考此处更新代码仓库。

```bash
# 拉取远端仓库
git fetch --all

# 提交你的更改
git add .
git commit -m "your commit message"

# 切换到新lab的分支
git checkout lab2

# 新建一个分支，用于开发
git checkout -b lab2-dev

# 引入你在上个lab的更改
git merge lab1-dev
```

如果合并发生冲突，请参考错误信息自行解决。

## 3. QEMU

本学期的实验将在 QEMU 模拟的 [virt 通用虚拟平台](https://www.qemu.org/docs/master/system/arm/virt.html) 上运行我们编写的内核。我们已经为大家配置好了 QEMU，运行内核只需在 `build` 目录下输入 `make qemu` 。

{% hint style="info" %}
**QEMU 命令解析（不要求掌握）**

本学期实验所用的 QEMU 命令如下：

```shell
qemu-system-aarch64 -machine virt,gic-version=3 \
    -cpu cortex-a72 \
    -smp 4 \
    -m 4096 \
    -nographic \
    -monitor none \
    -serial mon:stdio \
    -global virtio-mmio.force-legacy=false \
    -kernel kernel8.elf\ 
```

* `-machine virt,gic-version=3` 指定模拟的机器类型为 `virt`（QEMU 中用于模拟虚拟的 ARM 系统的标准平台）。 `gic-version=3` 指定要使用的全局中断控制器 (GIC) 的版本为 3，用于支持更现代的中断处理功能。
* `-cpu cortex-a72` 指定模拟的 CPU 类型为 Cortex-A72（一款高性能的 ARMv8 处理器）。
* `-smp 4` 表示要模拟 4 个 CPU 核心（即 4 个对称多处理单元），后续我们的实验将涉及 SMP 的内容。
* `-nographic` 配置 QEMU 运行在无图形输出模式下，即所有的输入输出都通过命令行界面进行，而不是通过 GUI 窗口。
* `-monitor none` 禁用 QEMU 的监控控制台（QEMU monitor），避免了干扰正常的输出流。
* `-serial mon:stdio` 将虚拟机的串行端口绑定到标准输入输出（stdio），这意味着虚拟机的输出会显示在当前的终端窗口中，输入也来自终端。
* `-global virtio-mmio.force-legacy=false` 配置 QEMU 中的 VirtIO 设备（VirtIO 是一种用于加速虚拟化设备的标准）。`virtio-mmio.force-legacy=false` 禁用 VirtIO MMIO 设备的传统模式，确保这些设备使用现代化的接口。
* `-kernel kernel8.elf` 指定要加载的内核镜像文件 `kernel8.elf`。QEMU 会在虚拟机中启动这个内核，模拟其运行环境。
  {% endhint %}

**`qemu` 的退出方法为： `Ctrl+A`，松开后按 `x`。**

## 4. AArch64

> AArch64 等价于 ARMv8 的 64 位指令集。

本学期的教学 OS 将运行在 AArch64 架构下。

绝大多数代码都使用 C 语言完成，部分代码需要使用汇编语言编写，建议简要了解（当然暂时用不到）：

* 指令手册：[Arm Architecture Reference Manual for A-profile architecture](https://developer.arm.com/documentation/ddi0487/latest/)。
* 各通用寄存器及其别名、函数调用约定、栈结构。
* 算术、访存、跳转等基本汇编指令。

## 5. 操作系统

本课程的先修课程包括但不限于 COMP120006 程序设计，COMP130201计算机系统基础，COMP130191 计算机组成与体系结构。

* **COMP120006 程序设计**中，我们学习了用 C 语言编写**用户态**程序。**用户态**这一概念是相较于操作系统的**内核态**而言的，本课程实验的重心即为内核态编程。在后续的实验中，我们将逐步理解（1）用户态/内核态的定义；（2）划分用户态/内核态的意义；（3）用户态与内核态的交互等问题。
* **COMP130201 计算机系统基础**与 **COMP130191 计算机组成与体系结构**中，我们初步认识了计算机系统中的各类硬件组成，如 CPU，内存，缓存，外设等等。操作系统内核的作用在于**管理并虚拟化（或者说抽象）这些资源** \[1]。在后续的实验中，我们将逐步理解虚拟化这一概念。
* 除此以外，一系列软件开发实践中的重要问题，如并发和持久化，也将由操作系统中的特定的机制或策略解决。

换而言之，操作系统是「硬件资源」与「用户态程序」的中间层，负责管理用户态程序对于硬件资源的访问（随着学习的深入，我们将对这一命题做出补充）。

## 6. Booting

在 `build` 目录中执行 `make qemu` 后，构建系统将自动编译操作系统内核并启动 QEMU 运行内核。下面我们将介绍本实验操作系统内核的启动流程。

**入口：梦开始的地方**

我们通过链接器脚本（请见第7节）指定内核的入口为 `_start (src/start.S:28)` 函数。

{% hint style="info" %}
**为什么入口函数不是 `main` 函数？**

在学习用户态 C 语言编程时，我们认为 `main` 函数是程序的入口。然而，这一说法省略了编译层面的一些重要细节，编译器会在 `main` 函数插入一系列初始化代码，因而 `main` 函数不是函数严格意义上的入口（相关背景知识请自行回顾 COMP130201 计算机系统基础）。同样，尽管我们的内核定义了 `main` 函数，我们仍需要将入口设为 `_start` 函数并完成一些必要的准备动作。
{% endhint %}

目前，我们无需深究 `_start` 函数中的细节，具体内容我们会在合适的时机提供解读文档。

**`main`：内核，启动！**

`_start` 函数最终会跳转到 `main` 函数。在 `main` 函数中，我们将完成内核的初始化工作。至此，我们已经成功启动了我们的内核。

目前，`main` 函数中的主要内容是一个分支判断，其作用是区分 CPU 0 和其余核心的初始化逻辑。下面我们具体介绍之。

**对称多处理器（SMP）**

现代计算机广泛采用多处理器架构，感兴趣的同学可以通过 `lscpu` 命令查看我们服务器的处理器信息。同样地，我们的内核运行在 QEMU 虚拟出来的 4 核机器上。这 4 个核心可以并发（或者说并行，在这里我们暂不严格区分这两个概念）地执行不同的指令流。

{% hint style="warning" %}
**注意**：以下内容可能理解难度较高，因此为了完成实验任务，请着重注意加粗字体。此外，欢迎在 Github Issues 区提出问题。随着本课程实验的进行，同学们将逐步加深对这些问题的理解。
{% endhint %}

在我们的实验平台上，机器启动时仅有一个核（CPU 0）处于唤醒状态。作为唯一一个唤醒的核心，其主要负责：

* 内核相关服务的初始化，例如 `uart_init` 用于初始化通信串口，UART 可用于输出数据，前面我们提到串行端口被绑定到了标准输入输出，**也就是说 `uart_*` 函数类似于过去所学的 `putchar` 等标准输入输出函数（相关函数请见`src/driver/uart.c`）**。`prink_init` 则用于初始化 `printk` 服务，**`printk` 更加类似于用户态编程中 `printf`，语法与效果也基本一致**。
* 唤醒其他核心，即 `smp_init`。

`main` 函数中的分支判断用于：

* CPU 0 启动核心，并完成一系列初始化动作，最后放行其他核心。
* 其余核心被 CPU 0 唤醒后，等待 CPU 0 完成初始化动作并放行。

{% hint style="success" %}
**任务 1**

CPU 0 完成内核服务初始化后，打印 "Hello, world! (Core 0)" （提示：上面介绍了如何打印字符串）。
{% endhint %}

{% hint style="success" %}
**任务 2**

其余 CPU 被放行后，各自打印 "Hello, world! (Core \<cpuid>)" （提示：上面介绍了如何打印字符串）。
{% endhint %}

{% hint style="info" %}
**预期输出**（后面三行的顺序可变）

```
Hello, world! (Core 0)
Hello, world! (Core 2)
Hello, world! (Core 3)
Hello, world! (Core 1)
```

{% endhint %}

## 7. 链接器脚本

链接器脚本位于 `src/linker.ld`。

我们执行 `make qemu` 编译内核并启动QEMU，编译内核得到的产物是一个 ELF 文件。此 ELF 文件同我们之前用户态程序的 ELF 文件一样，具有 `text`，`rodata`，`bss` 等段，而链接器脚本规定了这些段在内存中的位置。

{% hint style="danger" %}
**复习**

如果对 ELF 文件的结构及上述 `text`，`rodata`，`bss` 等段的性质不熟悉，请复习 COMP130201 计算机系统基础所学相关内容。
{% endhint %}

此外，链接器脚本还暴露了一系列符号，用于描述内存地址。举例如下：

```linker-script
PROVIDE(data = .);
.data : AT(ADDR(.data) - 0xFFFF000000000000) {
	*(.data)
	*(.data.*)
}
PROVIDE(edata = .);
```

这一段链接器脚本的含义是：

* 暴露一个 `data` 符号，标识当前地址（`.`），即此时内存布局中 `.data` 段的起始地址。
* 定义一个 `.data` 段，其中包含所有 `.data` 和以 `.data.` 开头的子段。这个段放置在内存中的特定位置，并且其起始地址由 `AT` 子句指定。 `AT` 子句的含义暂不要求掌握。
* 在 `.data` 段的内容被分配后，再暴露一个 `edata` 符号，标识 `.data` 段结束时的地址。

**也就是说，`data` 标志了 `data` 段 的开始，`edata` 标志了 `data` 段 的结束。** 在 `main` 函数中，我们可以通过以下方式获得 `data` 和 `edata` 的地址：

```c
extern char data[], edata[];
printf("data is %p; edata is %p", (void*)data, (void*)edata);
```

{% hint style="success" %}
**任务 3**

按照惯例 BSS 段应当置零（为什么？请复习 COMP130201 计算机系统基础或咨询助教）。然而，当操作系统内核被装载到内存中时，BSS 段对应的内存无法保证是置零状态（因为很多时候没有比操作系统更高一级的有关方面来负责“打扫”内存，此时操作系统内核本身就是内存的管理者）。因此，请在CPU 0 进行内核初始化前增加**清零 BSS 段**的逻辑。

**提示 1**: 清零 BSS 段首先需要获取 BSS 段起始和终止的地址，上面已经演示了如何获取某段的起始和终止地址。

**提示 2**: 查找 `memset` 函数，使用此函数清零一段连续的内存空间。
{% endhint %}

## 8. 提交

**提交方式**：将实验报告提交到 elearning 上，格式为`学号-lab0.pdf`。本次实验中，**报告不计分**。

**截止时间**： <mark style="color:red;">**9 月 13 日 23:59**</mark>。逾期提交将扣除部分分数。

报告中可以包括下面内容

* 代码运行效果展示
* 实现思路和创新点
* 对后续实验的建议
* 其他任何你想写的内容

## 参考资料

1. Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. (2018). Operating systems: Three easy pieces.


---

# 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://osh.fducslg.com/operating-systems-h-24fall/shi-yan/lab-0-booting.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.
