从源代码开始

"from S to S".

写在前面

为了节约大家的时间, 此处提供一个开箱即用 (Ready-to-Use) 版 Docker 镜像, 该镜像包括了除 EOS 源码外的所有实验环境, 你可以参考第三章的内容直接使用该镜像开始操作系统实验. 当然, 你可能还需要阅读本章的内容, 才能对实验环境配置过程中的一些细节有所了解.

关于该镜像的安装方法, 请参考第三章第一节.

EOS 内核源代码的结构

当我们下载完成 EOS 的内核源代码之后, 新建一个名为 src 的目录用来存放这些文件. 此时我们会发现, 所有的文件都杂乱地堆放在根目录中, 就像这样:

src
├── 8253.c
├── 8259.c
├── License.txt
├── block.c
...
├── vadlist.c
└── virtual.c

显然这样太乱了, 身患强迫症的作者表示无法接受. 所以, 为了方便我们的阅读和编辑, 并且为后续编译内核的工作打下基础, 我们需要首先整理内核源代码的目录结构.

EOS 官方的实验教程中给我们提供了一种组织方式:

src
├── api                 # EOS 内核的 API 定义
│   └── eosapi.c
├── boot                # EOS 的引导程序
│   ├── boot.asm
│   └── loader.asm
├── inc                 # EOS 内核的公用头文件
│   ├── eos.h
│   ├── eosdef.h
│   ├── error.h
│   ├── io.h
│   ├── ke.h
│   ├── mm.h
│   ├── ob.h
│   ├── ps.h
│   ├── rtl.h
│   └── status.h
├── io                  # IO 管理器及设备驱动
│   ├── driver
│   │   ├── fat12.c
│   │   ├── fat12.h
│   │   ├── floppy.c
│   │   ├── keyboard.c
│   │   └── serial.c
│   ├── block.c
│   ├── console.c
│   ├── file.c
│   ├── io.c
│   ├── ioinit.c
│   ├── iomgr.c
│   ├── iop.h
│   └── rbuf.c
├── ke                  # 内核管理及系统进程
│   ├── i386
│   │   ├── 8253.c
│   │   ├── 8259.c
│   │   ├── bugcheck.c
│   │   ├── cpu.asm
│   │   ├── dispatch.c
│   │   └── int.asm
│   ├── ki.h
│   ├── ktimer.c
│   ├── start.c
│   └── sysproc.c
├── mm                  # 内存管理部分
│   ├── i386
│   │   └── mi386.h
│   ├── mempool.c
│   ├── mi.h
│   ├── mminit.c
│   ├── pas.c
│   ├── pfnlist.c
│   ├── ptelist.c
│   ├── syspool.c
│   ├── vadlist.c
│   └── virtual.c
├── ob                  # 内核对象管理部分
│   ├── obhandle.c
│   ├── obinit.c
│   ├── object.c
│   ├── obmethod.c
│   ├── obp.h
│   └── obtype.c
├── ps                  # 进程管理和线程管理
│   ├── i386
│   │   ├── pscxt.c
│   │   └── psexp.c
│   ├── create.c
│   ├── delete.c
│   ├── event.c
│   ├── mutex.c
│   ├── peldr.c
│   ├── psinit.c
│   ├── psobject.c
│   ├── psp.h
│   ├── psspnd.c
│   ├── psstart.c
│   ├── sched.c
│   └── semaphore.c
└── rtl                 # 内核运行时库 (Runtime Library)
    ├── i386
    │   ├── hal386.asm
    │   └── setjmp.asm
    ├── crt.c
    ├── generr.c
    ├── keymap.c
    └── list.c

当我们按照上述方法整理完毕后, 我们会发现多了三个文件:

  • License.txt: EOS 核心源代码协议, 请保留, 并放入与 src 同级的目录中;

  • kernel.oslprog: 没用, 可以删除;

  • kernel.puo: 没用, 可以删除.

方便起见, 我们现在在 OS lab 容器中新建一个目录 eos:

cd ~
mkdir eos

然后我们将刚才整理好的文件从宿主机拷贝到容器中, 目前 OS lab 容器的目录结构如下:

eos
├── src             # EOS 内核源代码目录
└── License.txt     # 开源协议

至于如何将文件和文件夹拷贝到容器中, 请参考第二章的内容. 如果遇到问题, 请 STFW.

先启动试试?

可能经过之前的许多步骤, 尤其是上一部分中那个枯燥乏味的整理文件的过程之后, 你已经不耐烦了: 说了这么多, 这套看起来 low 到爆炸的 OS lab 环境到底能不能跑操作系统啊? 这全是命令行界面, 又没个应用程序图标可以双击, 我们要通过什么方法来启动操作系统呢? 不要着急, 接下来我们就来先启动一个已经编译好的 EOS 镜像玩一玩.

首先从这里下载一个只包含 EOS 内核的最小软盘镜像, 然后将其拷贝到容器的 eos 目录中, 使其目录结构如下:

eos
├── build           # 放置编译完成的二进制文件
│   └── floppy.img  # 最小软盘镜像
├── src
└── License.txt

此时你需要再次确认, 目前的 SSH 连接是否已经按照本章开头所述开启了 X11 转发, 之后的教程将不再强调这一点. 在容器中执行如下命令:

cd ~/eos
qemu-system-i386 -drive file=build/floppy.img,if=floppy,format=raw

第二条命令看起来可能有点长, 这里解释一下它的含义:

  • qemu-system-i386: 运行 OS lab 环境中的 QEMU, 这是一款著名的虚拟机软件, 它可以用来模拟一台 i386 指令集架构的计算机的运行;

  • -drive: 这个参数告诉 QEMU, 后面会指定虚拟机的启动镜像;

  • file=build/floppy.img,if=floppy,format=raw: 很显然这个参数分了三个部分. file 指定了启动时加载的软盘镜像, if 告诉 QEMU 现在要将其作为软盘启动, format 说明了镜像的格式.

如此之后, 你就可以看到在弹出的新窗口中, QEMU 已经在运行镜像中的 EOS 内核了.

当然, 除了使用 QEMU 启动 EOS, 我们还可以用 Bochs 来试试. Bochs 需要依赖一个叫做 bochsrc 的配置文件, 在 eos 目录中执行:

mkdir vm
vim vm/bochsrc

然后把以下内容粘贴进去:

mouse: enabled=0
floppya: 1_44=build/floppy.img, status=inserted
boot: floppy
megs: 32

保存退出后, eos 的目录结构应该如下:

eos
├── build
│   └── floppy.img
├── src
├── vm              # Bochs 相关的配置文件
│   └── bochsrc
└── License.txt

这样你就可以使用 Bochs 启动 EOS:

bochs -f vm/bochsrc

注意, Bochs 默认在启动后会暂停模拟, 这时你会看到弹出的窗口中一片漆黑. 回到 SSH 中, 你发现 Bochs 停在了这个地方:

00000000000i[      ] set SIGINT handler to bx_debug_ctrlc_handler
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b          ; ea5be000f0
<bochs:1>

FFFFFFF0h 是 i386 16 位实模式下的复位向量, 也就是说目前 Bochs 停在了进入 BIOS 之前的那条跳转语句. 这时输入 c (代表 “continue”) 然后回车, Bochs 就会继续运行了. 关于 Bochs 的基本用法, 请参考 EOS 官方教程.

思考: 为什么实模式下的 i386 处理器要从 FFFFFFF0h 处开始取指执行?

从源代码到操作系统

在进一步配置 OS lab 环境之前, 我们首先要明确一些概念, 例如: 这些源代码究竟是怎么变成最终那个可以启动的软盘镜像的?

在程序设计基础课程当中, 你一定还记得老师和你反复强调过的内容: C++ 编程的几个过程分别是编辑, 编译, 链接, 运行. 但是在实际编写程序的时候, 你快乐地在 IDE 中编码, 之后直接点击了那个绿色的三角形, 程序就能出现在屏幕上了. 所谓“编译”和“链接”的概念似乎已经被那个邪恶的绿色三角取代.

可能你还用过 GNU/Linux, 或者直接在命令行中调用过编译器对源码进行编译. 就算是这样, 你可能也会认为编译器直接将源代码“变”成了可执行文件. 我们可以在 OS lab 中试一试.

首先写一个 Hello World 程序, 这个你一定很拿手:

#include <iostream>
using namespace std;

int main(int argc, const char *argv[]) {
  cout << "Hello, world!" << endl;
  return 0;
}

然后将其保存到随便什么位置, 假设它的名称叫做 hello.cpp. 如何编译这个源文件呢? 执行:

g++ hello.cpp -o hello

然后你就会发现当前目录多出一个叫做 hello 的可执行文件, 运行这个文件:

./hello

屏幕上出现了“Hello, world!”. 当然如此, 编译器“直接”将 hello.cpp 变成了 hello, 是这样吗? 我们再来编译一次:

g++ hello.cpp -o hello -v

这次多加了一个参数 -v (verbose), 这个参数告诉编译器, 不管编译过程中发生了什么, 都必须事无巨细的输出. 这个时候终端上显示出了一大串内容, 你可能感到很凌乱. 没关系, 我们来捋一捋:

编译器 (g++) 首先调用了 cc1plus:

.../cc1plus ... hello.cpp ... -o /tmp/cc5Sjeqn.s

这个才是实际的 C++ 编译程序, g++ 只是一个方便其他程序或者用户调用的接口. cc1plus 拿到了我们的源文件 hello.cpp 然后将其编译为了一个汇编程序 /tmp/cc5Sjeqn.s.

之后, g++ 调用了汇编器 as:

as -v --64 -o /tmp/cc0jY1Vt.o /tmp/cc5Sjeqn.s

汇编器将汇编程序转换成了二进制的目标文件 /tmp/cc0jY1Vt.o, 这个目标文件随后会被 collect2 链接:

.../collect2 ... -o hello /tmp/cc0jY1Vt.o .../Scrt1.o .../crti.o .../crtbeginS.o .../crtendS.o .../crtn.o -lstdc++ -lm -lgcc_s -lgcc ...

collect2g++ 的链接器, 用来把各种目标文件组合成最后的可执行文件. 你会发现, 这个程序输出了最终的 hello, 还用到了刚才汇编器输出的 /tmp/cc0jY1Vt.o. 那么之后的那堆东西又是什么呢?

我们所写的 hello.cpp 直接编译得到的目标文件是没有办法执行的. 一方面, 程序中使用的 cout, endl 来自于头文件 iostream 中的定义, 而它们的实际实现都在 C++ 的标准库中, 程序必须依赖于标准库提供的基础设施才能实现诸如“输入输出”, “快速排序”, “容器”等的功能.

另一方面, 操作系统在执行程序时, 并不是从 main 函数开始执行的. 在此之前, 用户程序还必须完成处理环境变量, 分配栈空间, 初始化运行时库等步骤. 这些步骤和平台实现密切相关, 而且所有用户程序都必须执行.

这样看来, 把这些步骤留给程序员去单独编写就显得很不明智了, 所以各类编译器都会实现一个在 main 函数之前运行的函数 (通常叫 _start). 这个函数会完成上述操作, 并且调用 main 函数, 之后还会负责执行退出操作. 所有这些内容都实现在了 C/C++ 运行时库中 (C/C++ Runtime), 详情请看这里.

此时再回看 collect2 的所作所为, 你就很清楚了: 除了程序本身, 链接器还链接了 C Runtime (各种 crt 文件), 以及各种 C++ 基础设施 (标准库 stdc++, 数学库 m, 底层运行时库 gcc/gcc_s 等等).

通过给 g++ 添加 -v 选项, 聪明的你不难发现 C++ 程序实际的编译过程如下图所示:

+------------+
|   source   |
+------------+
      | 输入
      V
+------------+
|  cc1plus   |
+------------+
      | 汇编程序
      V
+------------+
|     as     |
+------------+
      | 目标文件
      V
+------------+
|  collect2  |
+------------+
      | 链接
      V
+------------+
| executable |
+------------+

Last updated

Was this helpful?