从源代码开始
"from S to S".
Last updated
Was this helpful?
"from S to S".
Last updated
Was this helpful?
注意: 在进行本章的内容之前, 你需要安装 X server.
Docker 容器中只提供了命令行界面, 为了运行带有 GUI 的 Bochs 虚拟机程序, 你需要根据你所使用的操作系统类型, 下载并安装不同的 X server:
Windows 用户: 点击下载, 安装并打开 Xming.
macOS 用户: 点击进入 XQuartz 工程网站, 下载, 安装并打开 XQuartz.
GNU/Linux 用户: 系统中已经自带 X server, 你不需要额外下载.
然后根据主机操作系统类型, 为 SSH 打开 X11 转发功能:
macOS 用户和 GNU/Linux 用户: 在运行ssh
时加入-X
选项即可:
Windows 用户: 在使用PuTTY
登录时, 在PuTTY Configuration
窗口左侧的目录中选择Connection -> SSH -> X11
, 在右侧勾选Enable X11 forwarding
, 然后登录即可.
通过带有 X11 转发功能的 SSH 登录之后, 在之后使用 Bochs 或 QEMU 运行 EOS 时就可以在弹出的新窗口中看到虚拟机输出的画面了.
本章将引导你从零开始搭建 OS lab 的实验环境, 在此之前, 你需要注意:
受限于 Engintime 公司的私有开放源代码协议, 本教程在配置环境的过程中不会直接提供 EOS 操作系统的源代码, 包括随本教程发布的所有 Docker 镜像内也同样不予附带. 你需要访问 EOS 的, 手动下载 EOS 的源代码, 并且仔细阅读.
教程将默认你已阅读并遵守协议, 任何因你个人原因违反了该协议而导致的后果, 均与本教程及其作者无关.
注意: 你需要接入北科大校园网才能下载此 Docker 镜像.
当我们下载完成 EOS 的内核源代码之后, 新建一个名为 src
的目录用来存放这些文件. 此时我们会发现, 所有的文件都杂乱地堆放在根目录中, 就像这样:
显然这样太乱了, 身患强迫症的作者表示无法接受. 所以, 为了方便我们的阅读和编辑, 并且为后续编译内核的工作打下基础, 我们需要首先整理内核源代码的目录结构.
EOS 官方的实验教程中给我们提供了一种组织方式:
当我们按照上述方法整理完毕后, 我们会发现多了三个文件:
License.txt
: EOS 核心源代码协议, 请保留, 并放入与 src
同级的目录中;
kernel.oslprog
: 没用, 可以删除;
kernel.puo
: 没用, 可以删除.
方便起见, 我们现在在 OS lab 容器中新建一个目录 eos
:
然后我们将刚才整理好的文件从宿主机拷贝到容器中, 目前 OS lab 容器的目录结构如下:
至于如何将文件和文件夹拷贝到容器中, 请参考第二章的内容. 如果遇到问题, 请 STFW.
可能经过之前的许多步骤, 尤其是上一部分中那个枯燥乏味的整理文件的过程之后, 你已经不耐烦了: 说了这么多, 这套看起来 low 到爆炸的 OS lab 环境到底能不能跑操作系统啊? 这全是命令行界面, 又没个应用程序图标可以双击, 我们要通过什么方法来启动操作系统呢? 不要着急, 接下来我们就来先启动一个已经编译好的 EOS 镜像玩一玩.
注意: 你需要接入北科大校园网才能下载此软盘镜像.
此时你需要再次确认, 目前的 SSH 连接是否已经按照本章开头所述开启了 X11 转发, 之后的教程将不再强调这一点. 在容器中执行如下命令:
第二条命令看起来可能有点长, 这里解释一下它的含义:
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
目录中执行:
然后把以下内容粘贴进去:
保存退出后, eos
的目录结构应该如下:
这样你就可以使用 Bochs 启动 EOS:
注意, Bochs 默认在启动后会暂停模拟, 这时你会看到弹出的窗口中一片漆黑. 回到 SSH 中, 你发现 Bochs 停在了这个地方:
在进一步配置 OS lab 环境之前, 我们首先要明确一些概念, 例如: 这些源代码究竟是怎么变成最终那个可以启动的软盘镜像的?
在程序设计基础课程当中, 你一定还记得老师和你反复强调过的内容: C++ 编程的几个过程分别是编辑, 编译, 链接, 运行. 但是在实际编写程序的时候, 你快乐地在 IDE 中编码, 之后直接点击了那个绿色的三角形, 程序就能出现在屏幕上了. 所谓“编译”和“链接”的概念似乎已经被那个邪恶的绿色三角取代.
可能你还用过 GNU/Linux, 或者直接在命令行中调用过编译器对源码进行编译. 就算是这样, 你可能也会认为编译器直接将源代码“变”成了可执行文件. 我们可以在 OS lab 中试一试.
首先写一个 Hello World 程序, 这个你一定很拿手:
然后将其保存到随便什么位置, 假设它的名称叫做 hello.cpp
. 如何编译这个源文件呢? 执行:
然后你就会发现当前目录多出一个叫做 hello
的可执行文件, 运行这个文件:
屏幕上出现了“Hello, world!”. 当然如此, 编译器“直接”将 hello.cpp
变成了 hello
, 是这样吗? 我们再来编译一次:
这次多加了一个参数 -v
(verbose), 这个参数告诉编译器, 不管编译过程中发生了什么, 都必须事无巨细的输出. 这个时候终端上显示出了一大串内容, 你可能感到很凌乱. 没关系, 我们来捋一捋:
编译器 (g++
) 首先调用了 cc1plus
:
这个才是实际的 C++ 编译程序, g++
只是一个方便其他程序或者用户调用的接口. cc1plus
拿到了我们的源文件 hello.cpp
然后将其编译为了一个汇编程序 /tmp/cc5Sjeqn.s
.
之后, g++
调用了汇编器 as
:
汇编器将汇编程序转换成了二进制的目标文件 /tmp/cc0jY1Vt.o
, 这个目标文件随后会被 collect2
链接:
collect2
是 g++
的链接器, 用来把各种目标文件组合成最后的可执行文件. 你会发现, 这个程序输出了最终的 hello
, 还用到了刚才汇编器输出的 /tmp/cc0jY1Vt.o
. 那么之后的那堆东西又是什么呢?
我们所写的 hello.cpp
直接编译得到的目标文件是没有办法执行的. 一方面, 程序中使用的 cout
, endl
来自于头文件 iostream
中的定义, 而它们的实际实现都在 C++ 的标准库中, 程序必须依赖于标准库提供的基础设施才能实现诸如“输入输出”, “快速排序”, “容器”等的功能.
另一方面, 操作系统在执行程序时, 并不是从 main
函数开始执行的. 在此之前, 用户程序还必须完成处理环境变量, 分配栈空间, 初始化运行时库等步骤. 这些步骤和平台实现密切相关, 而且所有用户程序都必须执行.
此时再回看 collect2
的所作所为, 你就很清楚了: 除了程序本身, 链接器还链接了 C Runtime (各种 crt
文件), 以及各种 C++ 基础设施 (标准库 stdc++
, 数学库 m
, 底层运行时库 gcc/gcc_s
等等).
通过给 g++
添加 -v
选项, 聪明的你不难发现 C++ 程序实际的编译过程如下图所示:
为什么要说这么多呢?
在学习计算机科学专业的课程时, 多数人都不会去思考某些表象背后的意义. 比如程序究竟是怎么被编译的. 而在编译大型软件, 甚至中等规模的工程时, 先将源码编译到目标文件, 再逐级链接得到可执行文件的做法是必不可少的.
比如在一个拥有一百万个源文件的项目里, 你只修改了某个文件中的一个小 bug. 如果你之前已经将其他文件编译成了目标文件, 此时你只需要重新编译修改过的这个文件, 然后执行链接操作即可; 而假如你使用的是把一百万个文件都扔给编译器, 让它解决剩下的问题的方法, 你可能不得不等待若干小时才能开始后续的调试工作.
虽然诸如 Visual Studio 之类的 IDE 会帮你完成这些步骤, 但是在构建 EOS 时, 这些操作必须由你手动完成, 所以你有必要了解这背后的故事.
首先从下载一个只包含 EOS 内核的最小软盘镜像, 然后将其拷贝到容器的 eos
目录中, 使其目录结构如下:
FFFFFFF0h
是 i386 16 位实模式下的, 也就是说目前 Bochs 停在了进入 BIOS 之前的那条跳转语句. 这时输入 c
(代表 “continue”) 然后回车, Bochs 就会继续运行了. 关于 Bochs 的基本用法, 请参考 EOS 官方教程.
这样看来, 把这些步骤留给程序员去单独编写就显得很不明智了, 所以各类编译器都会实现一个在 main
函数之前运行的函数 (通常叫 _start
). 这个函数会完成上述操作, 并且调用 main
函数, 之后还会负责执行退出操作. 所有这些内容都实现在了 C/C++ 运行时库中 (C/C++ Runtime), 详情请看.