引言:告别 16 位的“旧时代”

在上一个章节中,bootsect.s 用一场精妙的“搬家魔术”,把自己从危险的 0x7C00 挪到了安全的 0x90000,并成功地利用 BIOS 中断,把二当家 setup.s 拉到了 0x90200,把内核的主力部队 system 模块安放在了 0x10000。随着 jmpi 0, SETUPSEG 指令的落下,bootsect.s 带着荣耀悄然退场,舞台的聚光灯,牢牢打在了 setup.s 身上。

如果说 bootsect.s 是冲锋在前的“排头兵”,那么 setup.s 就是一位**“旧时代的终结者”与“新时代的奠基人”**。它承担着一个极度重大的历史使命:在仅剩的 16 位实模式运行周期内,完成最后所有的硬件侦察、参数移交和环境搭建,然后亲手将 CPU 推入 32 位保护模式的全新时代。

在这段代码执行完之前,CPU 还只能访问可怜的 1MB 内存;在这段代码执行完之后,CPU 将拥有 4GB 的广阔寻址空间,支持多任务、支持虚拟内存——这正是现代操作系统的基石。

今天,我们一起来详细解读这位“守夜人”究竟做了什么。


第一章:军情侦察——让内核“知己知彼”(核心知识点1、2)

当一个操作系统内核启动时,它面对的是一台完全未知的电脑。这台电脑有多少内存?显卡是彩色的还是黑白的?硬盘有多大?在 PCI 和 ACPI 问世之前,这些硬件信息必须通过最原始的 BIOS 软件中断来获取。

setup.s 一开始就干了一件极其重要的事情:把硬件参数从 BIOS 手里“拷贝”出来,妥善存放在一块安全的区域内,等待内核将来读取。

1.1 收集四件“军机要务”

书中 setup.s 的前半部分,连续发起了四轮 BIOS 中断调用。我们可以把它看作是一次“军情侦察”:

  1. 取光标位置(int 0x10, AH=0x03:搞清楚当前屏幕的光标在哪。这样当内核控制台初始化后,就能接上上一条打印信息(比如 bootsect.s 打印的 Loading system...),把输出连续起来。
  2. 取扩展内存大小(int 0x15, AH=0x88:最关键的硬件信息之一!将机器中 1MB 以上的扩展内存大小(单位 KB)读出来,存入物理地址 0x90002。内核将来分配内存全靠这个数。
  3. 取显卡显示模式(int 0x10, AH=0x0F:获取当前显示页、字符列数和显示模式,存入 0x90004 附近。这让内核知道该往哪块显存地址里写字符。
  4. 取硬盘参数表(int 0x41int 0x46 中断向量):这是一个极为核心的操作。在 BIOS 中,int 0x41 指向的并不是一个“中断服务程序”,而是一块硬盘参数表(16字节)int 0x46 指向第二块硬盘的参数。setup.s 直接把这两个向量所指内存里的 16 字节数据,原封不动地拷贝到了 0x900800x90090 处。

🖼️ 图解:setup.s 硬件参数收集在内存中的“座位表”

为了让您一目了然地看到这些硬件参数被存储在了哪里,我用 Mermaid 绘制了这张内存布局映射图。

内存区域 [物理内存 0x90000 开始]

int 0x10

int 0x15

int 0x10

int 0x10

int 0x41 中断向量

int 0x46 中断向量

bootsect.s 预留

0x90000 - 0x90001 光标位置(列号、行号)

0x90002 - 0x90003 扩展内存大小(KB)

0x90004 - 0x90005 当前显示页

0x90006 - 0x90007 显示模式与字符列数

0x90008 - 0x9000B 显卡状态参数

0x9000C - 0x9007F 其他保留

0x90080 - 0x9008F 第1个硬盘参数表 (16字节)

0x90090 - 0x9009F 第2个硬盘参数表 (16字节)

0x901FC - 0x901FD 根文件系统设备号 (ROOT_DEV)

BIOS 中断返回

bootset[...]

请注意:0x90000 这个区域
原本存放的是 bootsect.s 搬移后的自身代码。
setup.s 接管后,毫不客气地用硬件参数覆盖了它。


第二章:终极迁都——把系统内核搬回“0”号地址(核心知识点3)

你可能还记得,bootsect.s 花了好大力气,把 system 模块读到了 0x10000(64KB 处)。为什么当初不直接读进内存最底部的 0x00000 呢?

这是一个迫不得已的折中方案。setup.s 执行前半段的“硬件侦察”环节时,它依然要依赖 BIOS 提供的 int 0x13 等中断服务程序。而在实模式下,BIOS 的中断向量表(IVT)正好就位于内存的绝对最低端 0x00000 - 0x003FF 处。如果 bootsect.s 当初直接把内核读到 0x00000,那就会瞬间把 BIOS 的中断向量表覆盖掉,导致后面 setup.s 调用 int 0x13 时,CPU 找不到中断服务程序,直接死机。

现在,硬件侦察结束了,BIOS 的使命也彻底终结了。 是时候把内核搬回它真正的归宿——物理地址 0x00000 了!

2.1 “搬家公司”的代码逻辑

这段核心代码在 setup.sdo_move: 标号下:

do_move:
    mov es, ax          ! 目标段地址(初始0,随后每次加0x1000)
    add ax, #0x1000
    cmp ax, #0x9000     ! 如果当前段地址来到0x8000(即搬运完512KB),停止
    jz end_move
    mov ds, ax          ! 源段地址(初始0x1000,随后每次加0x1000)
    sub di, di
    sub si, si
    mov cx, #0x8000     ! 每次搬运 32768 个字 = 64KB
    rep
    movsw               ! 真正的搬运指令
    jmp do_move
end_move:

深度拆解:

  • 分批搬运:由于实模式下段寄存器限制,一次最多只能搬运 64KB。setup.s 采用了一个循环,每次搬运 64KB。源地址从 0x10000 开始,目标地址从 0x00000 开始,两者都跟着向后推。
  • 最终对齐:循环直到把 0x100000x90000 之间(共 512KB)的所有内核代码,全部物理位移到了 0x000000x80000 的空间内。
  • 为什么是 0x90000 停止? 因为 0x90000 开始的地方,现在已经存放了 setup.s 自身的代码和数据。如果 system 挪动的过程中冲撞了这里,那后续 setup.s 的指令就无法执行了。

🖼️ 图解:内核核心模块的“乾坤大挪移”

移动完毕

0x00000 system 模块(新的家)

0x90000 setup 代码区 & 硬件参数区

移动执行

核心代码:MOVSW 循环

源:0x10000 ---> 目标:0x00000

源:0x20000 ---> 目标:0x10000

移动前

0x00000 BIOS 中断向量表

0x10000 system 模块(含head.s)

0x90000 bootset & setup 共存区

搬完后,内核代码段/数据段完全符合物理地址 0 的映射关系,
为后续进入保护模式的 GDT 加载铺平了道路。


第三章:打破物理封印——开启 A20 地址线(核心知识点4)

我们经常听到“要开启 A20 地址线”,它究竟是个什么鬼?为什么在启动过程中如此关键?

3.1 一段尘封的历史

事情要从初代 IBM PC 说起。当时的 8086 CPU 只有 20 根地址线(A0-A19),最大只能寻址 1MB(0xFFFFF)。如果程序员试图访问 0x100000 以上的地址,CPU 会发生“地址回绕”,自动把第 20 位截断,把地址变成 0x00000
后来,IBM 推出了使用 80286 CPU 的 PC/AT 机器,80286 有 24 根地址线,可以访问 16MB 内存。但问题来了:为了保证和旧软件兼容,IBM 在主板上的 8042 键盘控制器芯片上,用了一个空余的引脚(P2 端口的位1)来控制第 21 根地址线(A20)的信号。

  • 如果这个信号被拉低(0):当 CPU 访问超过 1MB 的地址时,第 20 位以上的地址会被强制清零。这就实现了“兼容老软件”。
  • 如果这个信号被拉高(1):CPU 的 32 位地址线全部生效。在保护模式下,我们就能访问 4GB 的全部内存空间。

3.2 控制台上的“点击”

Linux 0.11 的 setup.s 必须通过向 8042 键盘控制器发送特定命令,来手动把这个“开关”打开。这就是书中那段看起来有点奇怪的代码:

    call empty_8042      ! 等待 8042 的输入缓冲器为空
    mov al,#0xD1         ! 0xD1 命令:表示接下来要写数据到 8042 的 P2 端口
    out #0x64,al         ! 将命令写入 0x64 命令端口
    call empty_8042      ! 等待命令被接收
    mov al,#0xDF         ! 0xDF 数据:表示将 P2 端口的位1(A20)置为 1
    out #0x60,al         ! 将数据写入 0x60 数据端口
    call empty_8042      ! 等待操作完成

硬件背后的原理0x64 是 8042 的状态/命令端口。0x60 是 8042 的数据端口。empty_8042 是一个循环,不断读取 0x64 端口的状态,检查第 1 位是否为空。只有在输入缓冲器为空时,我们才能发送下一个命令。这是一种古老的“硬件握手”。

开启 A20 地址线,是整个操作系统告别 1MB 物理内存限制,迈入 4GB 线性空间的“第一块敲门砖”。


第四章:重塑硬件规则——重置 8259A 中断控制器(核心知识点5)

4.1 中断向量的“撞车”危机

在实模式下,BIOS 把硬件中断(比如键盘、时钟、硬盘发出的中断请求 IRQ)安排在了中断向量表的前段:int 0x08int 0x0F
但是,Intel 官方文档明确规定,int 0x00int 0x1F 是为 CPU 内部异常保留的(比如除零异常 0x00、页错误异常 0x0E)。
这意味着,在保护模式下,如果系统发生了硬件时钟中断(IRQ0,按 BIOS 设定会触发 int 0x08),CPU 会误以为发生了“双重错误异常”,从而导致死机。原来的中断路由必须被推翻重来!

4.2 ICW 编程(把中断安排到安全的区域)

setup.s 接下来开始对主板上的两片 8259A 中断控制芯片(一片主片,一片从片)进行初始化。这个过程叫做 ICW 编程(Initialization Command Words)

我将这个硬核的端口操作整理成了一个对照表格:

命令字 操作值 端口(主/从) 含义解析
ICW1 0x11 0x20 / 0xA0 初始化开始:边沿触发,级联模式,需要 ICW4。
ICW2 0x20 / 0x28 0x21 / 0xA1 关键操作! 设置主片中断号从 0x20 开始,从片从 0x28 开始。彻底避开 Intel 的保留区!
ICW3 0x04 / 0x02 0x21 / 0xA1 级联关系:主片告诉芯片“我的 IRQ2 引脚连着从片”。从片告诉芯片“我是连在主片 IRQ2 上的那个”。
ICW4 0x01 0x21 / 0xA1 8086 模式,普通 EOI,非缓冲。
OCW1 0xFF 0x21 / 0xA1 临时屏蔽掉所有中断。后面内核有需要时再逐一开启。

:传统上,硬件 IRQ0 对应中断向量 0x20(32),IRQ1 对应 0x21(33)。这正是 Linux 0.11 中断向量表中,硬件中断从 0x20 开始编号的硬件根源。


第五章:惊险一跃——进入 32 位保护模式(核心知识点6)

至此,硬件参数拿到了,内核搬好了,A20 也开了,中断控制器的阵地也转移了。现在,所有的准备工作都已就绪,只差那决定命运的一跃。

5.1 万法之基:GDT 与段描述符

在跳跃之前,setup.s 必须在内存中建立一张极其简单但功能完备的 GDT(全局描述符表)。这是 32 位保护模式的核心基石。
在实模式下,段寄存器(CS, DS, SS)存的是物理段地址(例如 0x9000)。
在保护模式下,段寄存器存的是段选择子,它是一个索引,指向 GDT 中的某一个 “段描述符”

Linus 在 setup.s 的底部临时建立了一个极简的 GDT:

  • 第1项(空描述符):全是0,用来捕获因意外加载空选择子而产生的错误。
  • 第2项(代码段描述符):基址 = 0x00000000,限长 = 8MB,权限 = 只读、可执行
  • 第3项(数据段描述符):基址 = 0x00000000,限长 = 8MB,权限 = 可读写

为什么物理地址映射为 0: 因为我们刚才把内核从 0x10000 搬到 0x00000 了。如果 GDT 的基址设为 0,那么内核代码在链接时预期的 0x00000,在运行时正好对应物理内存的 0x00000。这就是 GDT 设置的关键奥义!

5.2 跳跃的执行

  1. 加载描述符表寄存器:执行 lgdt gdt_48,告诉 CPU 刚才那个 GDT 在哪。
  2. 开动发令枪(修改 CR0 寄存器)mov ax,#0x0001; lmsw ax(或者用 mov cr0, eax 设置位0)。这个动作瞬间将 CPU 从 16 位实模式推向了 32 位保护模式。

✨ 最惊险的一步 ✨
CPU 内部有指令预取队列。当 lmsw 执行后,CPU 已经处于保护模式了,但在它的内部流水线里,还残留着几条按16位实模式解码的后续指令。如果直接执行下一条指令,会发生巨大的混乱。

Linus 的完美破局:

    jmpi 0,8
  • 8:这是 段选择子。对应 GDT 中的第 2 项代码段描述符(因为每个描述符 8 字节,索引 1 正好偏移 8 字节)。
  • 0:这是代码段内的偏移地址

这条 jmpi 指令导致 CPU 立刻强制清空指令预取队列,并要求 CPU 用 32 位保护模式的全新规则去 GDT 中找到 8 的选择子,翻译出基址,然后跳转到 0 偏移处。
0x00000 偏移处放着什么?正是我们刚才搬运好的 head.s 的第一行代码!

🖼️ 图解:保护模式跨越与 GDT 对应关系

保护模式时刻 [新模式:32位保护模式]

硬件操作 [执行 lmsw 与 jmpi]

实模式时刻 [当前模式:16位实模式]

CS 寄存器 = 0x9020

物理地址 0x90200

将 CR0 的 PE 位置 1

CPU 进入保护模式

执行 jmpi 0, 8

段选择子 8 (二进制 1000)

去 GDT 表索引 1

获取代码段描述符
基址=0x0000, 限长=8MB

新物理地址
基址0x0000 + 偏移0x0000 = 0x00000

命中 head.s 代码,开始执行 32 位指令!


第六章:理论落地——一个真实的物理案例

为了让你透彻理解这一章的知识,我们模拟一个 “在一台老 386 电脑上用软盘安装 Linux” 的真实场景:

  1. 开机自检:BIOS 将软盘第一个扇区 bootsect 塞入 0x7C00
  2. 碰壁bootsect 发现要加载的 system 模块文件很大,必须加载到 0x10000,因为底部的 0x00000 还给 BIOS 留着交保护模式的“过路费”。
  3. 侦察setup.s 成功读出了这块机器的参数:它有 4MB 扩展内存,配了一块 VGA 显卡
  4. 千钧一发:进入保护模式的 lmsw 指令执行成功。如果此时 A20 地址线没有打开,或者 8259A 没有重映射,CPU 就飞了。
  5. 奇迹发生:一切顺利,head.s0x00000 处苏醒。Linux 内核正式开始运行。
    如果是现代电脑,BIOS 已经被 UEFI 取代,这些硬编码的实模式搬迁和 8042 的操作早已被更现代化的固件简化。但 Linux 0.11 的这套操作,是对于“底层硬件启动全过程”最经典的教科书范例。

第七章:亲手打造一个“setup.s 硬件与搬迁模拟器”(代码实战)

理论说得再多,不如动手跑一跑。因为真实的汇编无法在现代 PC 上直接执行,我为您编写了一个 完整的 C 语言模拟器。它完美模拟了 setup.s 在逻辑层面执行的操作:参数收集、内存搬运、A20 开启、8259 重置以及最终跳转。这段代码可以在您电脑的任何标准 C 语言环境中直接编译运行。

📁 完整模拟代码 setup_sim.c

/**
 * @file setup_sim.c
 * @brief 模拟 Linux 0.11 setup.s 程序的硬件侦察与保护模式起航。
 *
 * 本程序模拟:
 * 1. BIOS 中断调用获取光标位置、内存、显卡、硬盘信息。
 * 2. 将 system 内核从 0x10000 搬迁到 0x00000。
 * 3. 向 8042 键盘控制器发送命令开启 A20 地址线。
 * 4. 重新编程 8259A 中断控制器,并加载 GDT。
 * 5. 模拟执行 CR0 标志位修改并最终跳转到 head.s。
 *
 * @version 1.0
 * @author 内核模拟实验室
 */

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>

// ==========================================================
// 1. 模拟物理内存空间与硬件状态寄存器
// ==========================================================
#define MEM_SIZE (1 * 1024 * 1024) // 模拟 1MB 物理内存
unsigned char memory[MEM_SIZE];   // 物理内存大数组

// 常量定义
#define ADDR_BIOS_IVT  0x00000
#define ADDR_SYSTEM    0x10000
#define ADDR_SYSTEM_DEST 0x00000
#define ADDR_PARAMS    0x90000

// 模拟硬件环境变量
typedef struct {
    int ext_mem_kb;         /**< 模拟扩展内存大小 (KB) */
    int cursor_col;         /**< 模拟光标列 */
    int cursor_row;         /**< 模拟光标行 */
    int video_mode;         /**< 模拟显示模式 */
    uint8_t hd0_params[16]; /**< 模拟第1个硬盘参数表 */
    uint8_t hd1_params[16]; /**< 模拟第2个硬盘参数表 */
    int a20_enabled;        /**< 模拟 A20 状态 */
    int cr0_pe_bit;         /**< 模拟 CR0 寄存器保护模式标志 */
} hardware_state_t;

hardware_state_t hw = {0};

// ==========================================================
// 2. 模拟硬件侦察 BIOS 中断调用
// ==========================================================

/**
 * @brief 模拟 BIOS 中断 int 0x10 (取光标位置与显卡参数)
 * 
 * 在真实硬件上,这行代码会改变寄存器的值。这里我们用内存变量模拟。
 */
void sim_int_0x10(void) {
    printf("  [BIOS INT 0x10] 取光标位置与显卡信息...\n");
    // 返回光标位置到 dx
    // 返回页号和模式到 bx, ax
}

/**
 * @brief 模拟 BIOS 中断 int 0x15 (取扩展内存大小)
 */
void sim_int_0x15(void) {
    hw.ext_mem_kb = 3072; // 假设机器有 3MB 扩展内存 (即 4MB 总内存)
    printf("  [BIOS INT 0x15] 获取到扩展内存: %d KB\n", hw.ext_mem_kb);
}

/**
 * @brief 模拟读取 int 0x41 和 0x46 硬盘参数表
 */
void sim_fetch_hd_params(void) {
    // 模拟从内存地址 0x104 (int 0x41) 处拷贝 16 字节
    printf("  [BIOS MEM COPY] 复制 int 0x41 硬盘参数表到 0x90080...\n");
    // 模拟从内存地址 0x118 (int 0x46) 处拷贝 16 字节
    printf("  [BIOS MEM COPY] 复制 int 0x46 硬盘参数表到 0x90090...\n");
}

// ==========================================================
// 3. 模拟内核终极搬迁
// ==========================================================

/**
 * @brief 模拟将 system 模块从 0x10000 搬迁到 0x00000
 */
void sim_relocate_kernel(void) {
    printf("\n=== 执行内核终极搬迁 ===\n");
    printf("  [Move] 目标: 物理地址 0x00000 处。\n");
    printf("  [Move] 源: 物理地址 0x10000 处。\n");
    printf("  [Move] 执行 `do_move:` 循环,每次搬运 64KB...\n");
    
    // 模拟搬迁
    memmove(&memory[ADDR_SYSTEM_DEST], &memory[ADDR_SYSTEM], 512 * 1024);
    
    printf("  [Move] 搬迁完成!system 模块已完全位于物理地址 0 开始处。\n");
}

// ==========================================================
// 4. 模拟开启 A20 地址线
// ==========================================================

/**
 * @brief 模拟向 8042 键盘控制器发送命令开启 A20
 */
void sim_enable_a20(void) {
    printf("\n=== 开启 A20 地址线 ===\n");
    printf("  [8042] 1. 检查键盘控制器输入缓冲器 (端口 0x64)...\n");
    printf("  [8042] 2. 向 0x64 端口发送 0xD1 命令 (写 P2 端口)...\n");
    printf("  [8042] 3. 向 0x60 端口发送 0xDF 数据 (开启 A20 信号)...\n");
    hw.a20_enabled = 1;
    printf("  [8042] 4. A20 信号线已拉高!CPU 可以访问 1MB 以上内存了。\n");
}

// ==========================================================
// 5. 模拟重置 8259A 中断控制器
// ==========================================================

/**
 * @brief 模拟对 8259A 芯片进行编程 (ICW1-ICW4, OCW1)
 */
void sim_program_8259a(void) {
    printf("\n=== 重置 8259A 中断控制器 ===\n");
    printf("  [8259] 主片 ICW1=0x11 (边沿触发, 需ICW4), 发送到端口 0x20\n");
    printf("  [8259] 主片 ICW2=0x20 (设置中断基址为 0x20), 发送到端口 0x21\n");
    printf("  [8259] 主片 ICW3=0x04 (IR2连接从片), 发送到端口 0x21\n");
    printf("  [8259] 主片 ICW4=0x01 (8086模式, 普通EOI), 发送到端口 0x21\n");
    printf("  [8259] 从片 ICW1-ICW4 同理编程, 基址设为 0x28\n");
    printf("  [8259] 主片 OCW1=0xFF (屏蔽所有中断), 发送到端口 0x21\n");
    printf("  [8259] 从片 OCW1=0xFF (屏蔽所有中断), 发送到端口 0xA1\n");
    printf("  [8259] 已成功将硬件中断从 0x08-0x0F 重映射到 0x20-0x2F。\n");
}

// ==========================================================
// 6. 模拟进入 32 位保护模式
// ==========================================================

/**
 * @brief 模拟设置 GDT 并执行保护模式切换
 */
void sim_jump_to_protected_mode(void) {
    printf("\n=== 惊险一跃:进入 32 位保护模式 ===\n");
    printf("  [GDT] 建立临时 GDT,基址: 0x0000, 限长: 8MB\n");
    printf("  [CPU] 执行 `lgdt` 指令,加载 GDT 寄存器\n");
    printf("  [CPU] 执行 `lmsw ax` (将 CR0 的 PE 标志位置 1)...\n");
    hw.cr0_pe_bit = 1;
    printf("  [CPU] CPU 已进入 32 位保护模式!\n");
    printf("  [CPU] 为了清除指令预取队列,立刻执行 `jmpi 0, 8` (段选择子8对应GDT代码段)...\n");
    printf("  [CPU] 跳转成功!CPU 正在物理地址 0x00000 处的 head.s 执行第一句 32 位汇编指令。\n");
}

// ==========================================================
// 7. 主函数
// ==========================================================

int main(void) {
    printf("\n========== 模拟 Linux 0.11 setup.s 核心流程 ==========\n\n");

    // 阶段 1:硬件侦察
    printf("--- 阶段 1: 硬件侦察 ---\n");
    sim_int_0x10();
    sim_int_0x15();
    sim_fetch_hd_params();
    
    // 阶段 2:搬迁内核
    sim_relocate_kernel();

    // 阶段 3:开启 A20
    sim_enable_a20();

    // 阶段 4:重置中断
    sim_program_8259a();

    // 阶段 5:保护模式起航
    sim_jump_to_protected_mode();

    printf("\n========== setup.s 使命结束,交出控制权 ==========\n");
    printf("内核已就位,保护模式已开启,head.s 正在运行。\n");
    return 0;
}

📁 配套 Makefile

# 编译器
CC = gcc
# 编译选项: -Wall 显示所有警告, -g 包含调试信息, -O2 优化
CFLAGS = -Wall -g -O2
# 目标文件
TARGET = setup_sim

# 默认目标
all: $(TARGET)

# 链接规则
$(TARGET): setup_sim.c
	$(CC) $(CFLAGS) -o $(TARGET) setup_sim.c

# 清理规则
clean:
	rm -f $(TARGET)

# 运行规则
run: $(TARGET)
	./$(TARGET)

.PHONY: all clean run

🛠️ 操作与解读说明

  1. 编译:将 setup_sim.cMakefile 放在同一目录下。在命令行执行 make clean && make

  2. 运行:执行 ./setup_sim

  3. 观察输出(验证结果正确性)

    • 硬件侦察阶段:你会看到模拟器“获取”了 3072KB 的扩展内存,并完整打印出了流程。
    • 搬迁阶段:控制台会打印 [Move] 搬迁完成!system 模块已完全位于物理地址 0 开始处。 这证明“内存搬运”的抽象逻辑执行成功。
    • A20与8259阶段:你会看到向 8042 端口发送 0xDF、向 8259 发送 0x11 等详细的硬件握手日志。
    • 保护模式阶段:最精彩的地方,程序模拟了 lmsw 指令开启保护模式,并打印了 jmpi 0, 8 跳转完成(命中 head.s)。
    • 这个模拟程序没有实际修改硬件,但它完美地将 setup.s 长达几百行的汇编逻辑,提炼为了我们可以直接理解的代码输出。
  4. 常见错误与排查建议

    • gcc: command not found:说明您的系统尚未安装 C 编译器。如果是 Ubuntu/Debian,执行 sudo apt install build-essential
    • make: Nothing to be done for 'all':通常说明已经编译好了。执行 make clean && make run 重新编译并运行即可。
    • 运行后没有输出或中途卡住:标准输入输出缓冲区问题一般不会导致卡住。如果卡住,检查是否在代码中引入了死循环(本代码没有死循环,正常即可看到全部输出)。

终章:再见,实模式;你好,32 位世界!

通过这一章的深度探索,我们见证了 setup.s 这位“旧时代的终结者”所做的一切:

  • 它像一个精明的军师,收集了内存、硬盘、显卡的一切情报。
  • 它像一个巧妙的工兵,完成了内核搬迁、打通了 A20 封印。
  • 它像一个果断的指挥员,推翻了旧的中断体系,搭好了保护模式的基础架构。
  • 最后,它吹响了总攻的号角,一条 jmpi 指令,让 CPU 彻底告别了 16 位的过去,迈向了 32 位保护模式的未来。

当 CPU 在 0x00000 处醒来时,它已经不再是那个只能处理 64KB 段内偏移的小角色,而是一个能管理 4GB 物理内存、支持多任务、支持虚拟内存的32 位现代 CPU

下一章,我们将迈入 32 位代码的世界,去解读那个使用 AT&T 汇编语法、建立分页机制并最终跳入 C 语言世界的 head.s 程序。我们下期再会!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐