news 2026/7/3 1:22:03

第03章 引导启动程序(1):0x7C00到0x90000——解密bootsect.s的“搬家魔术”

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第03章 引导启动程序(1):0x7C00到0x90000——解密bootsect.s的“搬家魔术”

引言:接过BIOS递来的“火种”

想象一下,你刚刚按下了电脑的电源键。

在一片漆黑的物理世界里,CPU(中央处理器)复位,它的程序计数器被强行设置为0xFFFF0。这里驻留着电脑主板上一块“永久烧录”的芯片——ROM BIOS(基本输入输出系统)

BIOS 开始了一段忙碌的“寻宝之旅”。它要进行 POST(加电自检),检查内存和硬件是否完好。随后,它的终极任务是:找到一个可以引导操作系统的设备。无论是软盘、硬盘还是光盘,BIOS 会读取该设备的第0个磁道、第0个磁头、第1个扇区(整整512字节的“引导扇区”),并毫不客气地将其原封不动地拷贝到物理内存的0x7C00位置。

当 BIOS 完成拷贝后,它通过一条跳转指令,把 CPU 的执行权正式交给了这片0x7C00处的代码。bootsect.s隆重登场,接过了 BIOS 递给整个操作系统的第一支“火种”。

就在这一刻,我们正式进入了《Linux 0.11 内核完全注释》第三章节的核心。今天我们要共同啃下的,正是这段古老、硬核、却又散发着极简主义美学的汇编代码。

第一章:三大神技的开篇——bootsect.s到底长什么样?

对照书中的bootsect.s源码,你会发现它极尽克制。它只有短短不到 300 行汇编代码,编译后刚好512 字节,被严格限制在一个磁盘扇区的大小内。

这 512 字节的代码,要完成四个匪夷所思的任务:

  1. “乾坤大挪移”:把自己从0x7C00搬到0x90000
  2. 请来“二当家”:从磁盘读取下一个文件setup.s(仅4个扇区,2KB),放在自己新家的后面。
  3. 扛起“大当家”:把整个内核代码system(上百 KB)读取到内存安全的位置。
  4. 交接棒:把 CPU 指挥棒交给setup.s,功成身退。

这就像是你作为一个排头兵,BIOS 把你空投到了敌人的阵地(0x7C00),然后你第一个任务不是打仗,而是立刻把自己转移到一个安全的大本营,并且接连帮后续的大部队(setupsystem)搭建好跳板,最后安然退场。

下面,我们将按照bootsect.s的实际执行流程,一层层揭开它“魔术”般的面纱。

第二章:惊险的“搬家”——从 0x7C00 到 0x90000

2.1 为什么要“搬家”?

当我们还在讨论寻址时,代码其实正处于一个极其危险的境地。内存的最底端0x000000x50000甚至更大的区域,在未来将会被 Linux 的核心内核system模块所占据。如果bootsect.s一直赖在0x7C00不走,一会儿内核被从软盘加载进来时,就会直接把bootsect.s覆盖掉,导致系统立刻崩溃。

为此,Linus 选择了向内存的高处迁移:从0x7C00(大约 31KB 处)挪到0x90000(大约 576KB 处)。这个距离内核的最终目的地足够远,不会被波及。

2.2 汇编级别看“搬家”

bootsect.s的最前面,有这样一段极简至极的指令:

start: mov ax,#BOOTSEG ! BOOTSEG 定义为 0x07c0 mov ds,ax ! ds 段寄存器指向 0x7C00 mov ax,#INITSEG ! INITSEG 定义为 0x9000 mov es,ax ! es 段寄存器指向 0x90000 mov cx,#256 ! 共复制 256 个“字” (1 字 = 2 字节,256*2=512 字节) sub si,si ! 源索引 si = 0 (即 ds:si = 0x7C00:0x0000) sub di,di ! 目标索引 di = 0 (即 es:di = 0x90000:0x0000) rep ! 重复执行下一条指令,直到 cx 为 0 movw ! 也就是 movs 指令,从一个内存地址搬移到另一个 jmpi go,INITSEG ! 段间跳转。跳到 0x9000:go 处继续执行

深度解析:

  • REP MOVSW的魔法:这是实模式下最经典的“内存复制大法”。rep是重复前缀,movsw是传送一个字(Word)。每一次执行,CPU 都会把DS:SI指向的内存里的 2 个字节,复制到ES:DI指向的内存里,然后自动把SIDI加上 2,同时把循环计数器CX减去 1。
  • 为什么是 256?bootsect.s整个代码正好是 512 字节。每次传一个字(2字节),所以循环256次就能搬完。
  • 最后的jmpi go,INITSEG:这段代码极其关键。因为前一条movw虽然把数据搬到了0x90000,但CPU 的代码段寄存器(CS)和指令指针(EIP)仍然指向0x7C00。如果不执行这条跨段跳转,CS值没变,后续的代码依然会去0x7C00处寻找并执行(而那早已是空白)。通过这条指令,CPU 物理上把执行环境彻底切换到了0x90000

为了让你更直观地理解,我绘制了bootsect.s执行初期,CPU 内存视角的变化图:

阶段3_世界切换 [第三阶段:长跳转接管]

执行 jmpi go, INITSEG

CPU 将 CS 改为 0x9000

CPU 从物理地址 0x90000 的 'go' 标号继续执行

阶段2_自复制 [第二阶段:bootsect.s 执行自复制]

源地址 DS:SI = 0x7C00

复制 256个字 (512字节)

目标地址 ES:DI = 0x90000

阶段1_BIOS [第一阶段:BIOS加载完成]

物理内存地址 0x7C00

存放 bootsect.s 程序 (512字节)

CPU 开始执行 0x7C00 处代码

阶段1_BIOS

阶段2_自复制

阶段3_世界切换

注意:0x7C00 和 0x90000 相差近 576KB!
这个空间足够后续加载
setup 和 system 模块了。

第三章:请来“二当家”——加载setup.s

当 CPU 在0x90000处的go:标号醒来时,它首先设置好了各个段寄存器:

go: mov ax,cs mov ds,ax mov es,ax mov ss,ax ! 初始化 堆栈段寄存器 mov sp,#0xFF00 ! 堆栈指针指向 0x9FF00(足够大)

为什么要这么急切地设置SSSP堆栈?
因为紧接着下面就有操作了!bootsect.s接下来的任务是要从软盘中读取数据。为了实现这个任务,它调用了 BIOS 的底层磁盘中断int 0x13。在调用期间,CPU 需要在堆栈里临时保存寄存器状态。如果这个时候堆栈都没有初始化,pushpop指令将会把数据写到随机内存地址,导致系统死机。

准备好堆栈后,二当家的召唤令开始了:

load_setup: mov dx,#0x0000 ! 驱动器 0 (A 驱), 磁头 0 mov cx,#0x0002 ! 扇区 2, 磁道 0 mov bx,#0x0200 ! 地址偏移 = 512 (即 0x90200) mov ax,#0x0200+SETUPLEN ! AH=0x02 (读磁盘), AL=4 (读 4 个扇区) int 0x13 ! 调用 BIOS 中断,干活! jnc ok_load_setup ! 如果成功(CF标志位为0),跳走 ... ! 如果失败,复位软驱并重试... ok_load_setup:

深度解析:

  1. 中断int 0x13:这是实模式下操作磁盘的唯一途径。AX=0x0200+SETUPLEN表示我们要执行“读扇区”功能(AH=0x02),并且要连续读AL = 4个扇区。
  2. 读到哪里?根据前面设定的ES:BX = 0x9000:0x0200,物理地址就是0x90200
    • 请记住这个地址。我们刚刚搬到了0x90000,占据 0-512 字节(0x90000~0x901FF)。现在setup.s被加载到0x90200,紧挨着bootsect的新家。
  3. jnc ok_load_setupjnc是“Jump if Not Carry”,如果磁盘读取没有出错,CPU 进位标志位(CF)为 0,程序直接跳转到下一步。如果读取出错,就会往下执行复位软驱并无限重试。

第四章:摸清家底——获取“每磁道扇区数”

在加载setup.s之后,有一段看似微小的代码,实则关系到系统能否成功读取内核。

软盘知识小科普:一张 1.44MB 的软盘,有 80 个磁道(柱面),每个磁道有 2 个磁头(0 和 1),每个磁道每个磁头下有 18 个扇区(每扇区 512 字节)。常见的 1.2MB 软盘,每个磁道只有 15 个扇区。

如果不告诉内核“这盘软盘每道有多少扇区”,后面去读取巨大的system模块时,读取程序会因为不知道一个磁道转到哪里结束,导致读出来的数据错位。所以bootsect.s必须“借问”一下 BIOS。

mov dl,#0x00 ! 驱动器 A mov ax,#0x0800 ! AH=0x08 (取驱动器参数) int 0x13 ! 调用 BIOS mov ch,#0x00 seg cs ! 告诉 CPU:下一条取数,要从 CS 指向的段内存取(因为此时 DS 变了) mov sectors,cx ! 把 CX 寄存器保存到变量 sectors 中 (每磁道扇区数)

核心难点:seg cs伪指令
这行汇编非常经典。当调用int 0x13取出磁盘参数后,返回结果保存在CX寄存器中(CL低6位保存每磁道扇区数,CH保存最大磁道号)。但是此时DS寄存器已经被刚才的修改给弄乱了,Linus 为了保证安全,使用了seg cs这条指令。它告诉 CPU:“接下来的一条mov指令,虽然你想从DS里读取数据,但我命令你强制从CS(代码段)里读取变量sectors。”这是汇编程序员和硬件斗智斗勇的铁证。

第五章:交互与终极搬运——加载system模块

1. 屏幕上的声援:Loading system...
磁盘读取是一个非常慢的过程,如果屏幕上毫无反应,用户可能会以为电脑死机了。于是,bootsect.s调用了一行极其罕见的 BIOS 视频中断:

mov cx,#24 ! 字符串长度 24 mov bp,#msg1 ! 字符串内存地址 mov ax,#0x1301 ! AH=0x13 (显示字符串), AL=0x01 (光标跟随) int 0x10

调用int 0x10后,PC 的屏幕上立刻出现了Loading system...这行字。这是人类与操作系统内核第一次有了“交互”。

2. 搬动“大当家”:read_it子程序
真正的考验来了。system模块包含了 Linux 0.11 所有核心代码(编译后大约 120KB ~ 200KB 不等)。如果按一次读一个扇区(512字节)去读,要读几百次,极其缓慢。所以 Linus 写了一个复杂的read_it子程序,它尽可能按“磁道”为单位整条读取

由于这个子程序极长且极其复杂,我们在这里不展开全部汇编代码,而是将其核心逻辑提炼成一个三段式流程:

  1. 检查 64KB 边界:实模式下,段内偏移最高只能到 64KB(0xFFFF)。如果当前读取的扇区位置加上数据长度会跨越 64KB 边界,这段代码必须分段处理,否则会出现内存回绕,覆盖掉之前读好的数据。
  2. 读取磁道read_track内部再次调用int 0x13,利用之前获取的每道扇区数,尽最大可能把当前磁道的剩余扇区一次性全读入内存。
  3. 循环与接力:读完一个磁道后,自动切换当前磁头(从0切到1)或磁道。如果当前 64KB 段内存满了,自动将ES段寄存器增加0x1000(指向下一个 64KB 内存位置),继续读取。

经过这个极其严密又冗余的循环,Linux 内核的骨架被成功放置到了物理内存0x10000(64KB)地址开始的地方

第六章:移交指挥棒——确定根设备与跳转

system模块全部加载完毕后,还剩下最后一点小尾巴:

  • 确定根文件系统在哪里root_dev变量。如果编译内核时指定了ROOT_DEV(比如0x306对应第2个硬盘的第1个分区),就直接使用。如果没有指定,bootsect.s会使用刚刚读取到的每磁道扇区数来判断:如果每道 15 扇区,那是 1.2MB 软驱;如果每道 18 扇区,那是 1.44MB 软驱。据此推断出根设备号。
  • 最后的告别
jmpi 0,SETUPSEG ! SETUPSEG 是 0x9020

CPU 再次执行跨段跳转。CS 变为0x9020,IP 变为0x0000。物理地址指向0x90200——那里静静地躺着刚刚被加载进来的setup.s

至此,bootsect.s光荣完成使命,被覆盖在内存中的历史尘埃里。而setup.s,接过了控制权,准备迎接更惊心动魄的挑战:开启 A20 线、进入 32 位保护模式。

第七章:亲手操作——“搬迁模拟器”代码实战

为了让你亲眼看到这段“从0x7C00搬家的代码”在机器内部如何运作,我专门为你写了一套C 语言纯软件模拟器。这个程序完全脱离真实硬件,只需在常规的 Linux/Mac/Windows 终端里编译运行,就能“重演”bootsect.s的整个生命周期。

7.1 完整代码boot_sim.c

/** * @file boot_sim.c * @brief 模拟 Linux 0.11 bootsetc.s 程序的“自我搬家”与内核加载过程。 * * 本程序用纯 C 语言在用户空间模拟了物理内存的搬运和磁盘扇区的读取。 * 旨在直观展现 bootsetc.s 从 0x7C00 复制到 0x90000 的“魔术”过程。 * * 编译:gcc -o boot_sim boot_sim.c * 运行:./boot_sim */#include<stdio.h>#include<string.h>#include<unistd.h>// ==========================================================// 1. 模拟物理内存空间// ==========================================================#defineMEM_SIZE(1*1024*1024)// 模拟 1MB 物理内存unsignedcharmemory[MEM_SIZE];// 物理内存大数组// 定义关键物理地址常量 (与 0.11 内核完全一致)#defineADDR_BIOS_7C000x7C00#defineADDR_INIT_SEG0x90000#defineADDR_SETUP_SEG0x90200#defineADDR_SYSTEM0x10000// ==========================================================// 2. 模拟 BIOS 引导加载// ==========================================================/** * @brief 模拟 BIOS 启动引导阶段 * * BIOS 从启动盘(软盘/硬盘)的第1扇区读取 512 字节,放到 0x7C00。 */voidsimulate_bios_boot(void){printf("[BIOS] 电源开启,执行 POST 自检...\n");printf("[BIOS] 找到引导设备,读取第1个扇区 (bootsect.s)...\n");printf("[BIOS] 将 512 字节 boot sector 加载到物理内存 0x7C00...\n");// 为了模拟,我们在 0x7C00 处写入一串识别码constchar*boot_magic="[BIOS这里放了bootsect.s]";memcpy(&memory[ADDR_BIOS_7C00],boot_magic,strlen(boot_magic)+1);printf("[BIOS] 跳转到 0x7C00,执行权移交给 bootset.s!\n\n");}// ==========================================================// 3. 模拟 bootset.s 执行过程// ==========================================================/** * @brief 模拟 bootset.s 的自复制 (rep movsw) * * 将自身从 0x7C00 复制到 0x90000。 */voidsimulate_self_copy(void){printf("=== bootset.s 开始执行 ===\n");printf("[bootset] 1. 检测到自己在 0x7C00,这里不安全!必须搬家。\n");printf("[bootset] 2. 将 DS:SI 指向 0x7C00,ES:DI 指向 0x90000。\n");printf("[bootset] 3. 执行 REP MOVSW (共复制 256 个字 = 512 字节)。\n");// 模拟复制memcpy(&memory[ADDR_INIT_SEG],&memory[ADDR_BIOS_7C00],512);printf("[bootset] 4. 自复制完成!执行 jmpi go, INITSEG...\n");printf("[bootset] 5. CPU 成功切换到 0x90000 地址处继续执行。\n\n");}/** * @brief 模拟加载 setup.s * * 使用 int 0x13 中断,读取磁盘第 2~5 扇区到 0x90200。 */voidsimulate_load_setup(void){printf("=== bootset.s 加载 setup.s 模块 ===\n");printf("[bootset] 调用 BIOS 中断 int 0x13 (功能号 0x02, 读磁盘)。\n");printf("[bootset] 读取磁盘第 2~5 扇区 (共 4个扇区,2KB)。\n");printf("[bootset] 目标内存地址: 0x90200\n");// 模拟将 setup 数据写入内存constchar*setup_data="[这里是 setup.s 的代码和数据]";memcpy(&memory[ADDR_SETUP_SEG],setup_data,strlen(setup_data)+1);printf("[bootset] 读取成功!setup.s 已就位。\n\n");}/** * @brief 模拟获取驱动器参数与打印信息 */voidsimulate_disk_params_and_msg(void){printf("=== bootset.s 获取磁盘参数与用户交互 ===\n");printf("[bootset] 调用 int 0x13 功能号 0x08 获取驱动器参数...\n");printf("[bootset] 报告:当前驱动器类型为 1.44MB,每磁道 18 个扇区。\n");printf("[bootset] 调用 int 0x10 功能号 0x13,在屏幕打印: ");printf("\033[1;32mLoading system...\033[0m\n\n");// 模拟控制台彩色输出}/** * @brief 模拟加载庞大的 system 内核模块 (read_it 子程序) */voidsimulate_load_system(void){printf("=== bootset.s 加载 system 内核模块 ===\n");printf("[bootset] 调用 read_it 子程序,开始从磁盘加载 system 模块...\n");printf("[bootset] 为了加速,尽可能整条磁道读取。\n");printf("[bootset] 读取完成!system 模块被加载到了内存 0x10000 处。\n\n");}/** * @brief 模拟确定根设备号并移交控制权 */voidsimulate_jump_to_setup(void){printf("=== bootset.s 收尾并移交 ===\n");printf("[bootset] 识别根文件系统设备:根据每磁道扇区数判断为 1.44MB A盘。\n");printf("[bootset] 设定设备号 ROOT_DEV = 0x021C。\n");printf("[bootset] 执行最后一步:jmpi 0,SETUPSEG (跳转到 0x90200)\n");printf("[bootset] bootset 使命结束,被覆盖,告别舞台!\n\n");}// ==========================================================// 4. 主程序// ==========================================================intmain(void){printf("\n========== 模拟 Linux 0.11 bootset.s 启动过程 ==========\n\n");// 1. BIOS 阶段simulate_bios_boot();// 2. bootset 自复制simulate_self_copy();// 3. 加载 setupsimulate_load_setup();// 4. 获取参数 & 打印 Loadingsimulate_disk_params_and_msg();// 5. 加载 systemsimulate_load_system();// 6. 确定根设备并移交控制权simulate_jump_to_setup();// 7. 模拟进入 setup.sprintf("========== 进入下一阶段:setup.s 开始执行 ==========\n");printf("验证:0x90000 处现在保存的内容是: '%s'\n",&memory[ADDR_INIT_SEG]);printf("验证:0x90200 处现在保存的内容是: '%s'\n",&memory[ADDR_SETUP_SEG]);printf("\n========== 模拟结束 ==========\n");return0;}

7.2 配套 Makefile

# 编译器 CC = gcc # 编译选项: -Wall 显示所有警告, -g 包含调试信息, -O2 优化 CFLAGS = -Wall -g -O2 # 目标文件 TARGET = boot_sim # 默认目标 all: $(TARGET) # 链接规则 $(TARGET): boot_sim.c $(CC) $(CFLAGS) -o $(TARGET) boot_sim.c # 清理规则 clean: rm -f $(TARGET) # 运行规则 run: $(TARGET) ./$(TARGET) .PHONY: all clean run

7.3 操作与解读说明

  1. 编译:将boot_sim.cMakefile放在同一个目录下。在终端中执行make clean && make
  2. 运行:执行./boot_sim
  3. 观察结果
    • [BIOS] 将 512 字节 boot sector 加载到物理内存 0x7C00...:这是模拟BIOS放入了初始代码。
    • [bootset] 3. 执行 REP MOVSW (共复制 256 个字 = 512 字节)。[bootset] 5. CPU 成功切换到 0x90000 地址处继续执行。:你会看到程序自己给自己“搬了个家”。
    • [bootset] 调用 BIOS 中断 int 0x13 (功能号 0x02, 读磁盘)。[bootset] 读取磁盘第 2~5 扇区:模拟了把二当家setup.s请到了0x90200
    • 最后,程序会打印出最终在0x900000x90200内存地址处存放的内容,证明引导程序不仅搬了自己,还把后续的代码也安放好了。

运行这个模拟器,你会真切地感受到,一个操作系统不是“凭空被解压”的,而是由极其聪慧的引导程序,像搭积木一样,一块块从底层拼接到内存里的。

终章:承上启下的历史巨轮

回顾本节,我们看到了bootsect.s在极其有限的 512 字节内,完成了“自移动”、“读取setup”、“读取system”这三件看似不可能的任务。

它没有用到任何高深的算法,完全依靠对底层硬件(BIOS 中断)、内存寻址(实模式段偏移)和磁盘结构(磁道、磁头、扇区)的深刻理解。这是一种极致的“手工艺”。无论后来的 UEFI 引导、GRUB2 现代引导程序多么高级,它们底层的逻辑(“把数据从存储设备搬运到内存,然后把执行权移交”)都脱胎于这段诞生于 1991 年的区区 512 字节代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/3 1:11:27

LangChain快速入门-01概述

文章目录什么是 LangChainLangChain包及核心模块划分1. LangChain所包含的包2.LangChain核心模块划分2.1 Model I/O2.2 Chains2.3 Retrieval2.4 Agents什么是 LangChain LangChain是2022年10月&#xff0c;由哈佛大学的Harrison Chase&#xff08;哈里森蔡斯&#xff09;发起研…

作者头像 李华
网站建设 2026/7/3 1:09:36

95.基于 PLC 扫描周期原理!西门子 S7-1200 实现带软硬件互锁、防短路保护、自锁保持的电机正反转控制系统

摘要 PLC(可编程逻辑控制器)是工业自动化的核心大脑。本文从底层硬件原理出发,逐步拆解PLC的扫描周期、梯形图逻辑、指令表,并通过一个完整的电机正反转控制案例,展示从接线、编程到调试的全流程。全文无冗余,代码可直接运行于西门子S7-1200或三菱FX系列,帮助读者建立从…

作者头像 李华
网站建设 2026/7/3 1:09:31

匹夫细说C#:庖丁解牛迭代器,那些藏在幕后的秘密

在匹夫的上一篇文章《匹夫细说C#&#xff1a;不是“栈类型”的值类型&#xff0c;从生命周期聊存储位置》的最后&#xff0c;匹夫以总结和后记的方式涉及到一部分迭代器的知识。但是觉得还是不够过瘾&#xff0c;很多需要说清楚的内容还是含糊不清&#xff0c;所以这周就专门写…

作者头像 李华
网站建设 2026/7/3 1:07:58

DIN DIEN DSIN 简述

用户行为两大特性 多样性&#xff08;Diversity&#xff09;&#xff1a; 一个用户今天买衣服&#xff0c;明天买零食&#xff0c;后天买电器&#xff0c;兴趣非常广泛。、 局部激活&#xff08;Local Activation&#xff09;&#xff1a; 虽然用户兴趣广泛&#xff0c;但在预测…

作者头像 李华
网站建设 2026/7/3 1:01:27

Python 自动化之文件批量整理——重命名、分类归档、清理重复

电脑用久了&#xff0c;桌面和下载文件夹就是重灾区——“新建文件夹 (1)” “新建文件夹 (2)” “最终版” “最终版2”……用 Python 几行代码就能批量整理干净。 一、批量重命名 1. 统一命名规则 import osdef batch_rename(directory, prefix"file", start1, dig…

作者头像 李华