剖析 Linux 的开机

深 入 Linux 的 开 机 过 程 , 介 绍 一 般 IBM PC
开 机 时 的 动 作 , 并 解 读 Linux 开 机 档 如 何 动 作
刚 好 使 用 的 是 大 家 熟 悉 的 386, 486系 列 PC, 所 以
在 说 明 其 程 式 流 程 时 , 也 刚 好 可 以 对 其 相 关 的 PC
硬 体 架 构 做 探 讨 ,


陈 柏 翰


    这 篇 文 章 的 目 的 , 在 将 linux kernel的 boot 部 份 做 一 个 介 绍 , 因 为 笔 者 觉 得 很 少 有 这 样 的 文 章 介 绍 一 个 作 业 系 统 最 最 开 始 的 一 步 --把 kernel 本 身 载 入 至 记 忆 体 中 , 同 时 进 行 一 些 机 器 相 关 (machine dependent)的 初 设 动 作 , 由 於 linux刚 好 使 用 的 是 大 家 最 熟 悉 的 386, 486系 列 PC, 所 以 在 说 明 其 程 式 流 程 时 , 也 刚 好 可 以 对 其 相 关 的 PC 硬 体 架 构 做 探 讨 , 可 以 说 是 一 举 两 得 , 不 过 , 我 必 须 假 设 读 者 对 於 组 合 语 言 及 PC 最 基 础 的 架 构 , 如 暂 存 器 , 分 段 , 分 页 , 中 断 服 务 等 有 大 概 的 认 识 。

    读 者 可 在 linux source code的 /boot子 目 录 下 找 到 几 个 以 .S 作 为 副 档 名 的 组 合 语 言 档 , 本 文 要 说 明 的 即 是 其 中 的 bootsect.S及 setup.S两 个 档 案 , 及 尽 量 简 单 的 说 明 其 所 牵 涉 的 相 关 硬 体 部 份 。

bootsect.S

    这 个 程 式 是 linux kernel的 第 一 个 程 式 , 包 括 了 linux自 己 的 bootstrap程 式 , 但 是 在 说 明 这 个 程 式 前 , 必 须 先 说 明 一 般 IBM PC开 机 时 的 动 作 (此 处 的 开 机 是 指 "打 开 PC的 电 源 " ):

    一 般 PC在 电 源 一 开 时 , 是 由 记 忆 体 中 地 址 FFFF:0000开 始 执 行 (这 个 地 址 一 定 在 ROM BIOS中 , ROM BIOS一 般 是 在 FEOOOh到 FFFFFh中 ), 而 此 处 的 内 容 则 是 一 个 jump指 令 , jump到 另 一 个 位 於 ROM BIOS中 的 位 置 , 开 始 执 行 一 系 列 的 动 作 , 包 括 了 检 查 RAM, keyboard, 显 示 器 , 软 硬 磁 碟 机 等 等 , 这 些 动 作 是 由 系 统 测 试 码 (system test code)来 执 行 的 , 随 着 制 作 BIOS厂 商 的 不 同 而 会 有 些 许 差 异 , 但 都 是 大 同 小 异 , 读 者 可 自 行 观 察 自 家 机 器 开 机 时 , 萤 幕 上 所 显 示 的 检 查 讯 息 。

    紧 接 着 系 统 测 试 码 之 後 , 控 制 权 会 转 移 给 ROM中 的 启 动 程 式 (ROM bootstrap routine), 这 个 程 式 会 将 磁 碟 上 的 第 零 轨 第 零 磁 区 读 入 记 忆 体 中 (这 就 是 一 般 所 谓 的 boot sector, 如 果 你 曾 接 触 过 电 脑 病 毒 , 就 大 概 听 过 它 的 大 名 ), 至 於 被 读 到 记 忆 体 的 哪 里 呢 ? --绝 对 位 置 07C0:0000(即 07C00h处 ), 这 是 IBM系 列 PC的 特 性 。 而 位 在 linux开 机 磁 碟 的 boot sector上 的 正 是 linux的 bootsect程 式 , 也 就 是 说 , bootsect是 第 一 个 被 读 入 记 忆 体 中 并 执 行 的 程 式 。 现 在 , 我 们 可 以 开 始 来 看 看 到 底 bootsect做 了 什 麽 。

第 一 步

    首 先 , bootsect将 它 "自 己 "从 被 ROM BIOS载 入 的 绝 对 位 址 0x7C00处 搬 到 0x90000处 , 然 後 利 用 一 个 jmpi(jump indirectly)的 指 令 , 跳 到 新 位 置 的 jmpi的 下 一 行 去 执 行 , 关 键 的 assembly code如 下 :

.
( 搬 移 bootsect 本 身 )
.
.
jmpi go,INITSEC
go:
.
.
.

    表 示 将 跳 到 CS为 0x9000, IP为 offset "go" 的 位 置 (CS: IP = 0x9000:offset go), 其 中 INITSEC=0x9000定 义 於 程 式 开 头 的 部 份 , 而 go这 个 label则 恰 好 是 下 一 行 指 令 所 在 的 位 置 。

第 二 步

    接 着 , 将 其 他 segment registers包 括 DS, ES, SS都 指 向 0x9000这 个 位 置 , 与 CS看 齐 。 另 外 将 SP及 DX指 向 一 任 意 位 移 地 址 ( offset ), 这 个 地 址 等 一 下 会 用 来 存 放 磁 碟 参 数 表 (disk para- meter table )

    提 到 磁 碟 参 数 表 , 就 必 须 提 到 BIOS中 断 1Eh 。 先 简 单 的 介 绍 一 下 BIOS的 中 断 服 务 :80x86将 记 忆 体 最 低 的 256*4 byte保 留 给 256个 中 断 向 量 (每 个 interrupt vector大 小 为 4 byte, 所 以 一 共 有 256*4=1024byte), 而 其 中 的 第 1Eh个 向 量 指 向 " 磁 碟 参 数 表 ", 这 个 表 会 告 诉 电 脑 如 何 去 读 取 磁 碟 机 , 而 我 们 所 要 做 的 事 是 搬 移 磁 碟 参 数 表 到 刚 才 所 设 定 的 任 意 地 址 。

    接 着 , 改 变 搬 移 来 的 参 数 表 的 参 数 , 以 符 合 我 们 的 需 要 。 再 将 中 断 向 量 1Eh指 向 我 们 所 修 改 过 的 磁 碟 参 数 表 , 然 後 呼 叫 BIOS interrupt的 int 13h(function 0, 即 AH=0)重 置 磁 碟 控 制 卡 及 磁 碟 驱 动 器 , 之 後 磁 碟 机 就 会 照 我 们 的 意 思 动 作 了 。 如 果 你 曾 trace过 DOS的 kernel, 你 会 发 现 , 上 述 的 动 作 在 DOS中 也 有 类 似 的 对 应 流 程 。

现 在 让 我 们 来 看 看 关 键 的 程 式 码 : .
.
.
push #0
pop fs
mov bx,#0x78
.
( 使 GS:SI = FS:BX , 指 向 磁 碟 参 数 表 ,
再 将 GS:SI 所 指 地 址 的 内 容 搬 移 6 个
word 至 ES:DI 所 指 的 地 址 )
.
.

    此 段 程 式 是 将 FS: BX调 整 成 0000: 0078, 接 着 再 将 GS: SI的 内 容 设 成 与 FS: BX相 同 , 此 处 0x78h 即 为 int 1Eh 的 起 始 位 置 (7*16+8=120, (1*16+14)*4=120)。 调 整 ES:DI为 刚 才 所 设 定 的 任 意 地 址 , 从 GS:SI搬 移 6个 word ( 即 12 byte ) 到 ES:DI所 指 的 位 置 , 显 然 磁 碟 参 数 表 的 长 度 就 是 6个 word, (不 过 事 实 上 , 磁 碟 参 数 表 的 确 实 长 度 是 11个 byte )。 关 於 磁 碟 参 数 表 , 有 兴 趣 的 读 者 可 自 行 参 阅 讲 述 BIOS interrupt services的 技 术 手 册 , 会 有 详 细 的 说 明 。

    读 者 可 以 用 debug自 行 观 察 自 家 机 器 上 DOS的 磁 碟 参 数 表 的 起 始 位 置 (即 int 1Eh 的 内 容 )。 以 下 是 笔 者 机 器 的 情 形 (笔 者 使 用 的 作 业 系 统 是 MS DOS 6.2 ):

C:\>debug
-d 0000:0000
0000:0000 8A 10 16 01 F4 06 70 00-16 00 CB 04 F4 06 70 00 ......p.......p.
0000:0010 F4 06 70 00 03 01 79 0E-43 EB 00 F0 EB EA 00 F0 ..p...y.C.......
0000:0020 04 10 8E 34 0C 11 8E 34-57 00 CB 04 6F 00 CB 04 ...4...4W...o...
0000:0030 87 00 CB 04 08 07 94 33-B7 00 CB 04 F4 06 70 00 .......3......p.
0000:0040 0C 01 79 0E 4D F8 00 F0-41 F8 00 F0 BA 16 5F 06 ..y.M...A....._.
0000:0050 39 E7 00 F0 1B 01 79 0E-70 11 8E 34 12 01 79 0E 9.....y.p..4..y.
0000:0060 00 E0 00 F0 85 17 5F 06-6E FE 00 F0 EE 06 70 00 ......_.n.....p.
0000:0070 53 FF 00 F0 A4 F0 00 F0-22 05 00 00 3E 46 00 C0 S......."...>F..
^^ ^^ ^^ ^^

由 上 图 中 可 知 , 在 DOS中 磁 碟 参 数 表 的 起 始 位 置 ( int 1Eh 的 内 容 ) 为 0000:0522 。 接 着 观 察 DOS 中 位 置 0000:0522 开 始 的 11 个 byte, 也 就 是 磁 碟 参 数 表 的 内 容

C:\>debug
-d 0000:0520 l10
0000:0520 4D 53 DF 02 25 02 12 1B-FF 54 F6 0F 08 00 00 00 MS..%....T......
^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^ ^^
此 11 byte 即 为 磁 碟 参 数 表 的 内 容 ( 分 别 是 byte 00h 到 0Ah )

    在 程 式 中 我 们 所 更 动 的 是 第 五 个 byte ( byte 04h ), 改 为 18h (在 上 图 例 子 中 为 12h ), 这 个 byte的 功 能 是 定 义 磁 轨 上 一 个 磁 区 的 资 料 笔 数 。 关 键 的 程 式 码 如 下 :

.
movb 4(di),*18
.

第 叁 步

    接 着 利 用 BIOS中 断 服 务 int 13h的 第 0号 功 能 , 重 置 磁 碟 控 制 器 , 使 得 刚 才 的 设 定 发 挥 功 能 。

.
.
xor ah,ah
xor dl,dl
int 0x13
.
.

第 四 步

    完 成 重 置 磁 碟 控 制 器 之 後 , bootsect就 从 磁 碟 上 读 入 紧 邻 着 bootsect的 setup程 式 , 也 就 是 以 後 将 会 介 绍 的 setup.S, 此 读 入 动 作 是 利 用 BIOS中 断 服 务 int 13h的 第 2号 功 能 。 setup的 image将 会 读 入 至 程 式 所 指 定 的 记 忆 体 绝 对 位 址 0x90200处 , 也 就 是 在 记 忆 体 中 紧 邻 着 bootsect 所 在 的 位 置 。 待 setup的 image读 入 记 忆 体 後 , 利 用 BIOS中 断 服 务 int 13h的 第 8号 功 能 读 取 目 前 磁 碟 机 的 参 数 。

第 五 步

    再 来 , 就 要 读 入 真 正 linux的 kernel了 , 也 就 是 你 可 以 在 linux的 根 目 录 下 看 到 的 "vmlinuz" 。 在 读 入 前 , 将 会 先 呼 叫 BIOS中 断 服 务 int 10h 的 第 3号 功 能 , 读 取 游 标 位 置 , 之 後 再 呼 叫 BIOS 中 断 服 务 int 10h的 第 13h号 功 能 , 在 萤 幕 上 输 出 字 串 "Loading", 这 个 字 串 在 boot linux时 都 会 首 先 被 看 到 , 相 信 大 家 应 该 觉 得 很 眼 熟 吧 。

    linux的 kernel将 会 被 读 入 至 记 忆 体 绝 对 位 址 0x10000处 , 键 关 的 程 式 码 如 下 :

.
.
mov ax,#SYSSEG
mov es,ax
call read_it
call kill_motor
.
.

    其 中 SYSSEG於 程 式 开 头 时 定 义 为 0x1000, 先 将 ES内 容 设 为 0x1000, 接 着 在 read_it这 个 副 程 式 便 以 ES为 目 的 地 的 节 位 址 , 将 kernel读 入 记 忆 体 中 , 至 於 read_it副 程 式 的 详 细 内 容 笔 者 并 不 想 一 一 介 绍 , 不 过 聪 明 的 读 者 们 应 该 已 经 猜 到 , read_it一 定 又 利 用 了 BIOS int 13h与 磁 碟 有 关 的 I/O中 断 服 务 了 。

    至 於 kill_motor副 程 式 , 它 的 功 能 在 於 停 止 软 碟 机 的 马 达 (各 位 聪 明 的 读 者 会 不 会 觉 得 这 个 副 程 式 的 名 称 取 得 颇 为 传 神 呢 ? ), 其 程 式 码 如 下 :

.
.
kill_motor:
push dx
mov dx,#0x3f2
xor al,al
outb
pop dx
ret
.
.

    首 先 利 用 DX指 定 要 输 出 的 port, 而 03f2这 个 port 则 是 代 表 了 软 碟 控 制 器 (floppy disk controller )的 所 在 , 再 利 用 outb将 资 料 送 出 , 而 我 们 送 出 的 资 料 , 当 然 就 是 归 零 过 的 AL了 。 如 此 一 来 , 软 碟 的 马 达 就 停 止 了 。

第 六 步

    接 下 来 做 的 事 是 检 查 root device, 之 後 就 仿 照 一 开 始 的 方 法 , 利 用 indirect jump 跳 至 刚 刚 已 读 入 的 setup部 份 , 程 式 码 如 下 :

.
.
jmpi 0,SETYPSEG

    其 中 SETUPSEG已 在 先 前 定 义 为 0x9020, 所 以 CS: IP会 设 定 为 9020:0000, 即 跳 到 绝 对 位 址 为 0x90200, 也 就 是 setup的 起 点 。 而 bootsect也 大 功 告 成 了 。

到 此 为 止 , 记 忆 体 的 内 容 应 该 如 下 图 所 示 :

比 较

    把 大 家 所 熟 知 的 MS DOS 与 linux的 开 机 部 份 做 个 粗 浅 的 比 较 , MS DOS 由 位 於 磁 碟 上 boot sector的 boot程 式 负 责 把 IO.SYS载 入 记 忆 体 中 , 而 IO.SYS则 负 有 把 DOS的 kernel -- MSDOS.SYS载 入 记 忆 体 的 重 责 大 任 。 而 linux则 是 由 位 於 boot sector 的 bootsect程 式 负 责 把 setup及 linux的 kernel载 入 记 忆 体 中 , 再 将 控 制 权 交 给 setup。

    至 於 setup.S, 就 留 到 下 一 次 再 来 讨 论 了 。