Linux 0.11 源码探秘:setup.s 里那些 BIOS 中断调用,到底在给内核准备什么‘见面礼’?
Linux 0.11 启动探案录:BIOS 中断如何为内核铺路
当按下电源键的那一刻,一台 x86 计算机的启动过程就像一场精心策划的接力赛。BIOS 完成自检后,将接力棒交给 bootsect.s,再由 setup.s 接手——这个不到 512 字节的汇编程序,却在 Linux 0.11 启动过程中扮演着关键"情报官"的角色。今天,我们就化身计算机侦探,解密 setup.s 中那些神秘的 BIOS 中断调用,看看它们究竟为即将登场的内核准备了哪些关键"见面礼"。
1. 犯罪现场:实模式下的硬件侦察
在保护模式这个"高级社会"到来之前,内核必须依靠实模式下 BIOS 提供的"线人网络"来收集硬件情报。setup.s 开场就是一连串的 BIOS 中断调用,每个中断都像一位特定领域的线人,提供着不可或缺的硬件信息。
1.1 光标位置:控制台的第一个坐标
mov ah,#0x03 ; 读取光标位置功能号 xor bh,bh ; 页号清零 int 0x10 ; 调用BIOS显示中断 mov [0],dx ; 将结果存入0x90000这段看似简单的代码隐藏着几个关键细节:
- int 0x10是 BIOS 的显示服务中断,当 AH=0x03 时专门用于获取光标位置
- 返回的 DX 寄存器中,DH 存储行号,DL 存储列号(典型的 80x25 文本模式)
- 存储位置 0x90000(DS:0000)将在后续控制台初始化时被内核读取
提示:在实模式下,[0] 表示 DS:0000,而 setup.s 开始时已将 DS 设为 0x9000,因此实际物理地址是 0x90000。
1.2 内存探测:规划未来的疆域
mov ah,#0x88 ; 获取扩展内存大小 int 0x15 ; 调用BIOS内存服务 mov [2],ax ; 结果存入0x90002这个调用返回的是 1MB 以上的扩展内存大小(单位 KB),存储在 AX 寄存器中。值得注意的是:
- 该值将被用于初始化内存管理数据结构
- 在早期的 80286 系统上,最大只能检测到 16MB 内存
- 结果存储在 0x90002 处,与光标位置相邻
内存布局在此时开始成形:
| 内存地址 | 存储内容 | 用途 |
|---|---|---|
| 0x90000 | 光标位置(DX) | 控制台初始化 |
| 0x90002 | 扩展内存大小(AX) | 内存管理初始化 |
2. 硬件档案:建立设备数据库
除了基本的内存和显示信息,setup.s 还收集了各类硬件设备的详细参数,这些数据将成为内核驱动初始化的基础。
2.1 显示系统:显卡的身份证明
mov ah,#0x0f ; 获取当前显示模式 int 0x10 mov [4],bx ; BH=显示页号存入0x90004 mov [6],ax ; AL=显示模式,AH=字符列数存入0x90006后续还有更详细的 EGA/VGA 检测:
mov ah,#0x12 ; 获取EGA/VGA信息 mov bl,#0x10 int 0x10 mov [8],ax ; 视频参数存入0x90008 mov [10],bx ; 视频配置存入0x9000A mov [12],cx ; 视频配置存入0x9000C这些信息对内核至关重要:
- 确定文本模式还是图形模式
- 了解屏幕分辨率和色彩深度
- 为帧缓冲区分配正确的内存区域
2.2 存储设备:硬盘的解剖图
最复杂的要数硬盘参数获取:
; 获取第一硬盘参数表 mov ax,#0x0000 mov ds,ax lds si,[4*0x41] ; 从中断向量表获取硬盘1参数表地址 mov ax,#INITSEG mov es,ax mov di,#0x0080 mov cx,#0x10 rep movsb ; 复制16字节到0x90080同样方法获取第二硬盘参数(从中断向量 0x46 处)。这些参数包括:
- 柱面数、磁头数、每磁道扇区数
- 写预补偿、磁头着陆区
- 控制字节等
这些精确的物理参数是后续磁盘驱动初始化的基础,没有它们,内核甚至无法找到自己的根文件系统。
3. 情报处理:为内核搬家做准备
收集完所有硬件信息后,setup.s 开始执行一项关键任务:将系统代码从 0x10000 移动到 0x0000。这是一次精妙的内存搬运操作。
3.1 关闭中断:行动前的静默
cli ; 禁止中断这个简单的指令却至关重要:
- 移动内存时会覆盖 BIOS 的中断向量表
- 任何中断都会导致不可预测的结果
- 直到新的保护模式中断描述符表(IDT)就位前,中断必须保持关闭
3.2 内存搬运:系统代码的迁徙
mov ax,#0x0000 cld ; 清除方向标志,确保正向移动 do_move: mov es,ax ; 目标段地址 add ax,#0x1000 cmp ax,#0x9000 jz end_move mov ds,ax ; 源段地址 sub di,di ; 目标偏移清零 sub si,si ; 源偏移清零 mov cx,#0x8000 rep movsw ; 移动64KB数据 jmp do_move end_move:这段代码实现了:
- 将 0x10000-0x8ffff 的内容移动到 0x0000-0x7ffff
- 每次移动 64KB(0x8000 字)
- 使用 rep movsw 高效搬运数据
移动后的内存布局将变成:
+------------------+ 0x00000 | 系统代码 | | (原在0x10000) | +------------------+ 0x80000 | 空闲 | +------------------+ 0x90000 | 硬件参数存储区 | +------------------+ 0x90200 | setup.s代码 | +------------------+4. 交接准备:保护模式的入场券
在完成所有准备工作后,setup.s 最后也是最重要的任务是为进入保护模式铺路。这包括:
4.1 加载全局描述符表(GDT)
end_move: lidt idt_48 ; 加载空IDT lgdt gdt_48 ; 加载GDTGDT 是保护模式的基石,定义了内存段的权限和属性。Linux 0.11 的初始 GDT 包含:
- 空描述符(必须)
- 代码段描述符(基址 0,界限 16MB)
- 数据段描述符(基址 0,界限 16MB)
4.2 开启保护模式的最后步骤
- 打开 A20 地址线,突破 1MB 内存限制
- 设置 CR0 寄存器的 PE 位,切换到保护模式
- 执行远跳转,刷新指令队列
mov ax,#0x0001 ; 保护模式PE位 lmsw ax ; 加载机器状态字 jmpi 0,8 ; 跳转到保护模式代码段这个跳转中的 8 是代码段选择子,指向 GDT 中的代码段描述符。从此,CPU 进入 32 位保护模式,内核正式接管系统。
回顾整个 setup.s 的工作,它就像一位尽职的管家,在主人(内核)到来前:
- 调查清楚所有硬件情况(内存、显示、磁盘)
- 把系统文件搬到合适的位置
- 准备好保护模式所需的所有文档(GDT、IDT)
- 最后优雅地交出控制权
没有这些精心准备的"见面礼",内核将如同盲人摸象,无法有效管理系统资源。这也是为什么三十多年后,我们仍然要研究这些启动代码——它们展现了计算机从物理硬件到智能系统的神奇一跃。
