bootsect.s,setup.s,head.s的主要功能和对系统状态的影响、作用

bootsect.s ------ 搬运工(实模式,512字节)

主要功能:把内核从磁盘搬进内存。它是 BIOS 加载的第一个扇区,只能靠自己完成一切加载工作。

动作 目的
自我搬移 0x7C00 → 0x90000 腾出不被遮挡的位置
设栈到 0x9FF00 有了可用的函数调用栈
int 13h 读 setup(2KB)→ 0x90200 紧跟在自己后面
int 13h 逐磁道读内核 → 0x10000 小心绕开 64KB DMA 边界
关软盘电机、定根设备号 留下干净、确定的硬件状态

对状态的影响:内存里有了 setup 和完整内核,屏幕打出 "Loading system ..."。但 CPU 仍在实模式、BIOS 仍可用、地址仍受限 1MB------它只是个搬运工,不改变任何"运行规则"。

setup.s ------ 转型师(实模式 → 保护模式)

主要功能:最后一次用 BIOS 抢救硬件参数,然后把系统从 16 位实模式推入 32 位保护模式。

动作 对状态的影响
探测并保存光标/内存/显卡/硬盘参数 → 0x90000 BIOS 即将失效,参数"留档"供内核随时取用
内核整体下移 0x10000 → 0x00000 中断向量表不再需要,物理 0 处就位(head.s 要求在这)
加载临时 GDT(代码 0x08 / 数据 0x10)、空 IDT 有了合法的段翻译规则;任何中断=崩溃(靠屏蔽保命)
开 A20 地址线 突破 1MB 回绕,4GB 空间真正可达
重编程 8259A:硬件中断 0x080x0F → 0x200x2F 消除与 CPU 异常号(除零0、缺页14...)的冲突
lmsw 置 PE 位 + jmpi 0,8 质变:进入 32 位保护模式

对状态的影响 :这是一次"换世界"的跳跃------段寄存器含义从裸数字变成选择子、BIOS 从此永久不可用、中断全部关闭(cli + 屏蔽字 0xFF)。但分页未开、IDT 是空壳,系统只是"换了个骨架",还不能接待中断。

head.s ------ 开国元勋(保护模式,物理 0 处)

主要功能 :在保护模式里安家建章------建立正式的 IDT/GDT、开启分页,然后把权力交给第一个 C 函数 main()。

动作 对状态的影响
重载段寄存器(0x10)、设内核栈 从"临时票据"换到内核自己的正式段描述符
setup_idt:256 项全指 ignore_int 任何中断不再崩溃,只是打印 "Unknown interrupt"(占位,之后 trap_init 换真货)
setup_gdt:正式 GDT(16MB 代码/数据段) 段限长从 8MB 扩到 16MB,表也搬进内核自己的内存
检查 A20、数学协处理器(287/387) 硬件环境确认:浮点交给 FPU 或软件模拟
setup_paging:页目录+4 页表,恒等映射 16MB,置 CR0.PG 质变:分页开启,虚拟地址机制就位(当前线性=物理)
压入假参数(0,0,0)+假返回地址,jmp setup_paging + ret 借 ret 弹出 main 地址------CPU 进入 main(),汇编时代结束

对状态的影响:中断/异常有了占位处理、分页机制上线、栈就位------运行规则全部建立。副作用是一个精妙的自毁设计:head.s 开头的代码执行完后被页目录覆盖,因为页目录恰好也必须在物理 0。


系统状态演进总表

维度 BIOS 交权后 bootsect 后 setup 后 head 后
CPU 模式 实模式 实模式 32位保护模式 保护模式 + 分页
地址翻译 段×16+偏移 同左 选择子查GDT 再经页表(恒等映射)
可寻址空间 1MB 1MB 8MB(段限长) 16MB
中断 BIOS向量表可用 同左 全屏蔽,IDT为空 IDT就绪(全指ignore_int),仍关
BIOS 可用? ✓ ✓ ✗ 永久失效 ✗
内核位置 不在内存 0x10000 0x00000 0x00000(底部被页表占据)
下一步 --- 交给 setup 交给 head.s 交给 main() → C 世界

一句话概括:bootsect 负责"把家当搬进来",setup 负责"换国籍(换模式)",head 负责"立宪(立规则)"------三者接力后,操作系统才有资格开始运行 main()。

一、为什么必须有一个"第一扇区"(硬约束①)

BIOS 的加载协议写死了两件事:

  1. **只加载启动设备的第一个扇区(512 字节)**到 0x7C00;
  2. 检查偏移 510 处的 0xAA55 魔数,不对就不启动。

这个约束属于 IBM PC 的硬件规范,任何 OS 都绕不开。所以第一个程序被硬性封顶在 512 字节。看看现实尺寸:

复制代码
bootsect.s  汇编后 = 510 字节 + 0xAA55   ← 已经顶满一个扇区
setup.s     汇编后 ≈ 2KB(4个扇区)
─────────────────────────────────────
合计 ≈ 2.5KB,是 512 字节限制的 5 倍 ------ 装不下,没有商量余地

二、鸡生蛋问题(硬约束②)

有人会想:那我写一个"大程序",让它前 512 字节先把剩余部分从磁盘读进来,不就合并了?

这正是关键:任何超出第一扇区的代码,都必须先由第一扇区里的代码加载才能执行。所以无论怎么组织,运行时结构必然是两级:

复制代码
第一级(≤512B,自包含):设栈 → 读盘 → 把"下一级"弄进内存 → 跳过去
第二级(任意大):       做真正的复杂工作

把源码写成一个文件,只是把两级"写在同一页纸上"------运行时它们仍是先后加载、先后执行的两段程序。这就是形式合并:源码合了,架构没合。

三、BIOS 依赖的单向门(硬约束③,也是 setup≠head 的原因)

两阶段代码的工作性质有一条不可逾越的分界线:

加载阶段 转换阶段
需要 BIOS(int 13h 读盘、int 15h 查内存、int 10h 查显卡) ✓ 必须用 ✗ 保护模式下 BIOS 永久失效
CPU 模式 实模式 必须切换到保护模式

所以分工天然是:趁 BIOS 还活着把所有它知道的事做完(查参数、读内核),然后过河拆桥、烧桥进入新世界。setup.s 就是"桥上"的那段代码------查完参数、开 A20、重设 8259A、拉下模式切换的闸门。

有人会问:能不能把 setup 的工作并进 head.s?不能。head.s 是 32 位保护模式代码,执行它之前必须先完成模式切换,而模式切换前又必须先做完 BIOS 查询------这个先后顺序是硬件决定的单向门,切不开。

四、分开的三个正收益(即使能合并也不该合)

1. 生命周期错开,内存回收干净

复制代码
0x90000: bootsect 的家 → bootsect 一跳走就作废 → setup 立刻征用为参数保存区
0x90200: setup 的家    → head.s 重建 GDT 后作废 → 之后的初始化复用

每级代码"用完即弃",它的地盘马上被下一级回收。若合成一体,这段自举代码反而要在内存里多占地方更久。

2. 职责单一,错误域隔离

bootsect 是磁盘 I/O 专家:读出错就复位重试、死循环(注释原话"手工重启");setup 则假定读盘已成功,专心做模式转换。混在一起后,512 字节里要同时处理两种错误模型,可读性和可靠性都变差。

3. 可替换性------这条界线被现代 Linux 保留了 25 年

最有力的证据:现代 Linux 把 bootsect.s 整个删掉了 (由 GRUB 等引导器接管),而 setup 的职责演化为 arch/x86/boot/ 的 setup 阶段,一直活着。之所以能这样替换,正是因为当年定下了清晰的两级契约(引导器 → 2KB 实模式 setup → 保护模式入口)。如果当年混成一体,今天就没法单独换掉一半。

五、三种假想合并方案盘点

方案 可行性 结论
A. 全并入一个 512 字节扇区 ❌ 2.5KB 装不进 512B,物理不可能
B. 一个源文件,前 512B 做自加载 ⭕ 形式可行 运行时仍是两阶段,只是书面上合并;自加载代码反而多占宝贵空间,纯负收益
C. setup 并入 head.s ❌ 模式单向门:BIOS 查询/A20/8259A 必须在实模式做完,32 位代码做不了
D. 维持现状 ✅ 现代内核演化同样保留了这条界线

六、类比收尾

这套结构就是多级火箭:

复制代码
bootsect = 一级助推器 ------ 只负责"离架"(把后续部件送上天),燃料烧完即抛
setup    = 二级助推器 ------ 负责"穿出大气层"(跨过模式边界),任务完成即抛  
head.s   = 入轨 + 点火主引擎 ------ 此后 main() 接管一切

一、int N 这条指令本身只做三件事

首先要破除一个误解:int 13h 指令本身不会"读盘",它只是一次受控跳转。CPU 执行它时:

复制代码
1. FLAGS、CS、EIP 依次压栈(保存现场)
2. 到中断向量表 IVT(物理 0x000~0x3FF)里取第 N 项:
   地址 = N × 4,低字是 IP、高字是 CS
3. 清 IF/TF,跳到 CS:IP ------ 剩下的全是"人"(处理程序)的代码,iret 返回

所以"int 13h 怎么实现读盘"的完整答案是:IVT0x13 里存着 BIOS 写好的一个函数的地址,那个函数里是一段真正的磁盘驱动程序 。实测中 0x4C 处就是 0xF000:0xE3FE------F 段是 BIOS ROM 映射区,那里躺着几百行 SeaBIOS 的汇编/C 代码。

而 IVT 这张表是谁填的?BIOS 自己在 POST(开机自检)时填的 。实测 0xFFFF0(CPU 上电执行的第一条指令)就是一个 ljmp 0xF000:0xE05B------跳进 POST,POST 检测硬件、填写 BDA(0x400~0x4FF 的 BIOS 数据区)和 IVT,最后 int 19h 加载引导扇区。你的内核是从这条链的末端接手的。

二、int 13h(读盘):一段真正的磁盘驱动程序

这是三个里最复杂的。AH=2 读扇区时,BIOS 内部要做的事:

复制代码
1. CHS 换算:把调用者给的 磁道/磁头/扇区 整理成控制器要求的参数
   (bootsect.s 读内核时绕 64KB 边界的计算,就是配合这一层的约束)
2. 软盘路径:
   - 编程 DMA 控制器 8237A 通道2(设内存地址、传输字节数)
   - 打开软盘电机(端口 0x3F2------head.s 里 kill_motor 关的就是它)
   - 向软盘控制器 FDC(uPD765) 端口 0x3F4/0x3F5 发命令字节序列
   - 睡眠等待 FDC 完成中断(IRQ6)
   - 读回 7 个结果状态字节,判错,AH=错误码
3. 硬盘路径:直接对 IDE 端口 0x1F0~0x1F7 依次写
   扇区数/扇区号/柱面/驱动器头号,最后写命令寄存器 0x1F7 = 0x20(READ)

本质 :BIOS 中断就是"厂商替你预写好的设备驱动"。这也解释了为什么 Linux 0.11 一进内核就抛弃 BIOS------因为内核要写自己的驱动:hd.c 直接操作 0x1F0 端口发 ATA 命令(和上面硬盘路径一模一样的事)、floppy.c 自己编程 FDC。BIOS 能用,但太慢太不透明,真内核都自己来。

三、int 15h/88(查内存):最"水"的一个------查的是考试答案

AH=88 返回扩展内存大小(KB)。它的"实现"朴素到令人发笑:

复制代码
POST 阶段(开机时,一次性干完):
  - 通过 CMOS(端口 0x70 写索引、0x71 读写数据)读出固件记录的内存配置
  - 或对内存做写-读回测试逐段数出来的实际容量
  - 结论存进 BIOS 内部变量
int 15h/88 处理程序:
  - 把那个存好的数字放进 AX,iret 返回。完。

所以 Linux 0.11 存到 0x90002 的"15232",不是调用时现测的,而是POST 几秒钟前数好的结论。这也正是它的局限(只到 64MB、数字可能不准),现代内核改用 int 15h 的 0xE820 子功能(返回真实内存映射表)------但那已经是 90 年代中期的事了。

四、int 10h(显卡):半是"数据搬运工",半是真驱动

实测暴露了它的秘密------处理程序第一条实质指令就是把 DS 指向 0x0040(BDA)。因为许多子功能的"实现"就是查表:

子功能 实际干了什么
AH=03 读光标 从 BDA 0x450+页号×2 读出(行列),实测当前值 0x0800 = 第8行第0列
AH=0F 读模式 从 BDA 0x449 读模式(实测 0x03=80×25彩色文本)、0x44A 读列数(实测 0x50=80)
AH=13h 写字符串 把"字符+属性"两个字节一组写进 0xB8000 文本缓冲区,再更新 BDA 光标
AH=12h EGA检测 这才真碰硬件:操作显卡寄存器端口 0x3B4~0x3DF

0xB8000 的原理值得单独说:文本模式下,屏幕上每个字符对应显存里 2 个字节------字符 ASCII 码 + 属性字节 (高4位背景色、低4位前景色)。实测开头 4 个字 0753 0765 0761 0742:属性 0x07(黑底灰字)+ 'S''e''a''B',正是 SeaBIOS 刚打印的开机横幅。BIOS"打印"和 Linux 0.11 的 console.c 打印,干的是完全相同的事:往 0xB8000 搬字节。你在 QEMU 窗口里看到的一切文字,物理上都是那块内存。

五、三张"幕后基础设施"总结

数据结构 位置 谁写的 作用
IVT 中断向量表 0x000~0x3FF POST 填 int N → 处理程序地址的目录
BDA BIOS数据区 0x400~0x4FF POST/各handler维护 光标、模式、列数、键盘缓冲、时间滴答...
BIOS ROM/选项ROM 0xC0000(显卡)、0xE0000~0xFFFFF 厂商烧录 所有 int 处理程序的本体

六、在你的调试环境里复现这次实验

复制代码
gdb 连接后(停在 [1/4] bootsect 时,IVT 还是完整的):

(gdb) x/2wx 0x4c          ← 看 int 13h 的处理程序在哪(IP, CS)
(gdb) x/2wx 0x40          ← int 10h 的(你会看到 CS=0xC000,显卡ROM)
(gdb) set $ip=*(unsigned short*)0x4c
(gdb) set $cs=*(unsigned short*)0x4e
(gdb) x/10i $cs*16+$ip    ← 反汇编 BIOS 的磁盘驱动!
(gdb) x/1xh 0x449         ← 显卡模式
(gdb) x/1xw 0x450         ← 光标位置(走完引导后再看,会被 bootsect 的 int 10h 改掉)
(gdb) x/40xh 0xb8000      ← 显存里的字符,看 BIOS 给你留了什么话

一个阅读提示:x/i 反汇编 0xF000/0xC000 区的 16 位代码时同样会出现"32位误读"的合并指令怪相(比如实测 jmp 0x10d41a 其实是段内近跳转),以字节视图 x/10bx 对照为准。

一句话总结 :int N 只是"按目录打电话",目录(IVT)是 POST 填的,接线员(处理程序)住在 BIOS ROM 里,他们干活的方式不神秘------读 BDA 里的小本本、往 0x70/0x71/0x1F0/0x3F5 这些硬件端口写字、往 0xB8000 搬字符。Linux 0.11 读完参数就和他们分道扬镳,自己把这些端口活全接管了。

相关推荐
夜雪一千1 小时前
Python URL解码详解:单层、双重URL解码实战与踩坑指南
linux·服务器·python
咯哦哦哦哦1 小时前
配置VNC sever 6.11.0版本 linux(激活码)
linux·运维·服务器
傲世仙尊1 小时前
序列化与自定义协议-把结构体拆成字节流再拼回来
linux·服务器·网络·arm开发
꯭自꯭闭꯭2 小时前
DM主备集群以及读写分离集群搭建
linux·运维·数据库
倔强的石头1062 小时前
【Linux指南】动静态库系列(九):动态库如何进入进程地址空间:从磁盘 .so 到共享内存映射
java·linux·服务器
脚踏实地,坚持不懈!2 小时前
Linux 内核源码解析:从 secondary_startup_64 到 pick_eevdf 的完整调用栈分析
android·linux·arm开发
xbzb2 小时前
Linux iSCSI 存储部署与 CHAP 认证完全指南
linux·服务器·iscsi·chap·共享硬盘
IT大白鼠2 小时前
彭大帅的AI运维助手——自然语言管理 Linux 集群与网络设备——第 5 篇 · 多模态与虚拟化AI 能「看」图:多模态与虚拟化管理
linux·运维·人工智能
IT大白鼠3 小时前
彭大帅的AI运维助手——自然语言管理 Linux 集群与网络设备——第 2 篇 · 安全守规矩的 AI:分级安全管控是灵魂
linux·运维·人工智能