从源代码开始
"from S to S".
注意: 在进行本章的内容之前, 你需要安装 X server.
Docker 容器中只提供了命令行界面, 为了运行带有 GUI 的 Bochs 虚拟机程序, 你需要根据你所使用的操作系统类型, 下载并安装不同的 X server:
Windows 用户: 点击这里下载, 安装并打开 Xming.
macOS 用户: 点击这里进入 XQuartz 工程网站, 下载, 安装并打开 XQuartz.
GNU/Linux 用户: 系统中已经自带 X server, 你不需要额外下载.
然后根据主机操作系统类型, 为 SSH 打开 X11 转发功能:
macOS 用户和 GNU/Linux 用户: 在运行
ssh
时加入-X
选项即可:ssh -X -p 20022 username@127.0.0.1
Windows 用户: 在使用
PuTTY
登录时, 在PuTTY Configuration
窗口左侧的目录中选择Connection -> SSH -> X11
, 在右侧勾选Enable X11 forwarding
, 然后登录即可.
通过带有 X11 转发功能的 SSH 登录之后, 在之后使用 Bochs 或 QEMU 运行 EOS 时就可以在弹出的新窗口中看到虚拟机输出的画面了.
写在前面
本章将引导你从零开始搭建 OS lab 的实验环境, 在此之前, 你需要注意:
受限于 Engintime 公司的私有开放源代码协议, 本教程在配置环境的过程中不会直接提供 EOS 操作系统的源代码, 包括随本教程发布的所有 Docker 镜像内也同样不予附带. 你需要访问 EOS 的官方 Git 仓库, 手动下载 EOS 的源代码, 并且仔细阅读“EOS 核心源代码协议”.
教程将默认你已阅读并遵守协议, 任何因你个人原因违反了该协议而导致的后果, 均与本教程及其作者无关.
注意: 你需要接入北科大校园网才能下载此 Docker 镜像.
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 官方教程.
从源代码到操作系统
在进一步配置 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 ...
collect2
是 g++
的链接器, 用来把各种目标文件组合成最后的可执行文件. 你会发现, 这个程序输出了最终的 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 |
+------------+
为什么要说这么多呢?
在学习计算机科学专业的课程时, 多数人都不会去思考某些表象背后的意义. 比如程序究竟是怎么被编译的. 而在编译大型软件, 甚至中等规模的工程时, 先将源码编译到目标文件, 再逐级链接得到可执行文件的做法是必不可少的.
比如在一个拥有一百万个源文件的项目里, 你只修改了某个文件中的一个小 bug. 如果你之前已经将其他文件编译成了目标文件, 此时你只需要重新编译修改过的这个文件, 然后执行链接操作即可; 而假如你使用的是把一百万个文件都扔给编译器, 让它解决剩下的问题的方法, 你可能不得不等待若干小时才能开始后续的调试工作.
虽然诸如 Visual Studio 之类的 IDE 会帮你完成这些步骤, 但是在构建 EOS 时, 这些操作必须由你手动完成, 所以你有必要了解这背后的故事.
Last updated
Was this helpful?