admin 管理员组

文章数量: 1184232

前言:通过阅读并理解操作系统源码中的代码作用和应用场景,可以快速学会64位操作系统开发的相关知识点。在这里感谢田宇先生的开源贡献精神,他的想法,与我不谋而合,作为白帽黑客,对于底层原理的技术探索与研究,是必要的一个发展过程!
操作系统开发的技术探索:基于Linux内核的64位操作系统(MINE)

本文章仅提供学习,切勿将其用于不法手段!

引言

在第一篇文章中,我们梳理了引导扇区的 基本流程 核心组件 。本文将聚焦引导程序与 FAT12文件系统 的深度交互,解析其如何精准定位内核文件,并探讨引导流程的 优化方向 扩展能力 。通过对代码细节的拆解,我们将揭示x86引导过程中“文件系统适配”的底层逻辑,以及如何通过设计改进提升引导程序的健壮性与效率。

一、FAT12文件系统:引导程序的“寻宝地图”

引导程序的核心任务是 从软盘中找到并加载内核文件 ,而FAT12文件系统就是这张“寻宝地图”。要理解引导程序的文件搜索逻辑,必须先拆解FAT12的核心结构。

1.1 FAT12文件系统的核心组件

FAT12是DOS时代经典的文件系统,专为软盘设计,其核心结构包括:

  • 引导扇区(Boot Sector)​ ​:存储文件系统元数据(如每扇区字节数、簇大小、FAT表位置);
  • FAT表(File Allocation Table)​ ​:记录每个簇的使用状态与下一个簇的编号(“簇链”);
  • 根目录区(Root Directory)​ ​:存储所有文件的元数据(文件名、起始簇号、文件大小);
  • 数据区(Data Area)​ ​:存储文件的实际内容(按簇划分)。

1.2 根目录项:文件的“身份证”

引导程序遍历根目录区时,本质是在查找 匹配的文件名 。每个根目录项是 32字节的固定结构 ,其格式如下(以“LOADER BIN”为例):

偏移 长度 描述 示例值(“LOADER BIN”)
0x00 8 文件名(ASCII,补空格) LOADER BIN → 实际存储为 LOADER BIN (11字节,后补空格)
0x08 3 扩展名(ASCII) 空(因为是.COM文件?不,这里是 BIN ,扩展名占3字节,所以完整文件名是 LOADER.BIN ?不对,代码中是 LOADER BIN ,11字节,所以文件名是 LOADER ,扩展名是 BIN ?哦,代码中的 LoaderFileName "LOADER BIN",0 ,11字节,对应根目录项的0x00-0x0A偏移,其中0x00-0x07是文件名 LOADER (8字节),0x08-0x0A是扩展名 BIN (3字节),剩下的0x0B是属性字节。
0x0B 1 文件属性(隐藏、只读等) 0x20(普通文件)
0x0C 10 保留字段 全0
0x16 2 创建时间 0
0x18 2 创建日期 0
0x1A 2 起始簇号 文件第一个数据簇的编号
0x1C 4 文件大小 内核文件的大小(字节)

1.3 簇链追踪:从起始簇到文件末尾

引导程序找到文件名后,需通过 FAT表 追踪文件的“簇链”:

  1. 从根目录项获取 起始簇号 ​(如 0x1A 偏移的值);
  2. 查询FAT表,获取该簇的 下一个簇号 ​(FAT12中每个条目占12位);
  3. 重复步骤2,直至遇到 0xFFF (簇链结束标志);
  4. 将每个簇对应的扇区数据读取到内存。

二、引导程序的“性能瓶颈”与优化策略

尽管引导程序的逻辑清晰,但在实际运行中仍存在 效率问题 ​(如多次磁盘I/O、冗余计算)。本节将针对这些瓶颈提出优化方案。

2.1 问题1:根目录遍历的“全扫描”

原程序遍历根目录区的 所有扇区 ​( RootDirSectors=14 ),即使文件早已找到。这种“全扫描”方式在软盘容量较大时(如1.44MB)会浪费大量时间。

优化方案:提前终止遍历

在找到文件名后,立即跳出循环,避免不必要的扇区读取。修改 Lable_Search_In_Root_Dir_Begin 的逻辑:

Lable_Search_In_Root_Dir_Begin:
	cmp	word	[RootDirSizeForLoop],	0
	jz	Label_No_LoaderBin
	dec	word	[RootDirSizeForLoop]	
	mov	ax,	00h
	mov	es,	ax
	mov	bx,	8000h
	mov	ax,	[SectorNo]
	mov	cl,	1
	call	Func_ReadOneSector
	mov	si,	LoaderFileName
	mov	di,	8000h
	cld
	mov	dx,	10h
	
Label_Search_For_LoaderBin:
	cmp	dx,	0
	jz	Label_Goto_Next_Sector_In_Root_Dir
	dec	dx
	mov	cx,	11
Label_Cmp_FileName:
	cmp	cx,	0
	jz	Label_FileName_Found  ; 找到文件名,直接跳出循环!
	dec	cx
	lodsb	
	cmp	al,	byte	[es:di]
	jz	Label_Go_On
	jmp	Label_Different
; ... 其他逻辑不变 ...

优化效果 ​:找到文件后立即终止根目录遍历,减少磁盘I/O次数。

2.2 问题2:FAT表读取的“重复加载”

原程序每次查询FAT条目时,都会重新读取FAT表的扇区( Func_GetFATEntry 中调用 Func_ReadOneSector )。实际上,FAT表的前几个扇区已包含大部分簇的信息,无需重复读取。

优化方案:缓存FAT表数据

在引导程序初始化时,将FAT1表的前 2个扇区 ​(覆盖前 2×512×12=12KB 的簇信息)读入内存缓存,后续查询直接从缓存中获取:

; 在Label_Start后添加:缓存FAT1表前2扇区
mov	ax,	00h
	mov	es,	ax
	mov	bx,	8000h  ; 缓存地址:ES:8000h
	mov	ax,	SectorNumOfFAT1Start  ; FAT1起始扇区=1
	mov	cx,	2  ; 读取2扇区
	call	Func_ReadOneSector  ; 读取FAT1前2扇区到ES:8000h
; 修改Func_GetFATEntry:从缓存中读取
Func_GetFATEntry:
	push	es
	push	bx
	push	ax
	mov	ax,	00
	mov	es,	ax
	pop	ax
	mov	byte	[Odd],	0
	mov	bx,	3
	mul	bx
	mov	bx,	2
	div	bx
	cmp	dx,	0
	jz	Label_Even
	mov	byte	[Odd],	1
Label_Even:
	xor	dx,	dx
	mov	bx,	[BPB_BytesPerSec]
	div	bx
	push	dx
	; 从缓存中读取:ES=0x0000,BX=8000h + 偏移量
	mov	si,	ax  ; SI=偏移量(扇区内偏移)
	mov	ax,	[es:bx+si]  ; 直接从缓存读取FAT条目
	cmp	byte	[Odd],	1
	jnz	Label_Even_2
	shr	ax,	4
Label_Even_2:
	and	ax,	0fffh
	pop	bx
	pop	es
	ret

优化效果 ​:避免重复读取FAT表,减少磁盘I/O次数约50%。

三、引导程序的“健壮性”增强:错误处理与恢复

原程序在找不到loader.bin时,仅显示错误信息并死循环( jmp $ )。实际应用中,引导程序需具备 错误恢复能力 ,比如提示用户重新插入软盘,或尝试加载备用内核。

3.1 改进错误处理:提示用户操作

修改 Label_No_LoaderBin 的逻辑,显示更友好的提示,并等待用户按键:

Label_No_LoaderBin:
	mov	ax,	1301h
	mov	bx,	008ch
	mov	dx,	0100h
	mov	cx,	21
	push	ax
	mov	ax,	ds
	mov	es,	ax
	pop	ax
	mov	bp,	NoLoaderMessage
	int	10h
; 等待用户按键
mov	ah,	00h  ; BIOS中断0x16:键盘输入
int	16h  ; 等待按键,返回按键码在AL
; 重新初始化,尝试重新搜索
mov	word	[RootDirSizeForLoop],	RootDirSectors
mov	word	[SectorNo],	SectorNumOfRootDirStart
jmp	Lable_Search_In_Root_Dir_Begin

3.2 支持备用内核:多文件加载

引导程序可扩展为 支持多个内核文件 ​(如 LOADER.BIN KERNEL.BIN ),若首选文件找不到,自动尝试加载备用文件:

; 定义备用文件名
BackupLoaderFileName:	db	"KERNEL  BIN",0
; 在Label_No_LoaderBin后添加:尝试加载备用文件
Label_No_LoaderBin:
	; ... 显示错误信息 ...
	; 尝试加载备用文件
	mov	si,	BackupLoaderFileName
	mov	di,	8000h
	cld
	mov	dx,	10h
	jmp	Label_Search_For_LoaderBin  ; 重新搜索备用文件

四、引导程序与内核的“交接仪式”:参数传递

引导程序加载内核后,需向内核传递 硬件环境信息 ​(如内存布局、设备参数),确保内核能正确初始化。

4.1 传递参数的方式

x86引导程序通常通过 内存区域 向内核传递参数。本引导程序可将硬件信息存储在 BaseOfLoader 之前的内存区域:

; 在Label_File_Loaded前添加:准备内核参数
mov	ax,	0x0000  ; 参数区域地址:0x0000:0x0000
mov	ds,	ax
mov	es,	ax
mov	di,	0x0000  ; 参数偏移地址
; 传递栈顶地址
mov	ax,	BaseOfStack
	mov	[es:di],	ax
	inc	di
	inc	di
; 传递软盘驱动器号
mov	al,	[BS_DrvNum]
	mov	[es:di],	al
	inc	di
; 传递内核加载地址
mov	ax,	BaseOfLoader
	mov	[es:di],	ax
	inc	di
	mov	ax,	OffsetOfLoader
	mov	[es:di],	ax
; 跳转到内核,传递参数指针
push	es  ; 参数区域段地址
push	di  ; 参数区域偏移地址
jmp	BaseOfLoader:OffsetOfLoader

4.2 内核接收参数

内核的入口函数需从栈中获取引导程序传递的参数:

; 假设内核入口函数为_start
void _start() {
    // 从栈中获取参数指针
    unsigned int *param_ptr = (unsigned int *)__builtin_return_address(0);
    unsigned int stack_top = param_ptr[0];
    unsigned int drv_num = param_ptr[1];
    unsigned int loader_base = param_ptr[2];
    unsigned int loader_offset = param_ptr[3];
    // 初始化栈
    __asm__ volatile("mov %0, %%esp" : : "r"(stack_top));
    // 初始化硬件
    init_hardware(drv_num);
    // 加载文件系统
    init_filesystem(loader_base, loader_offset);
    // 启动用户程序
    start_user_program();
}

五、结语

引导扇区虽小,却是连接硬件与操作系统的“关键纽带”。通过对FAT12文件系统的深度解析、引导流程的优化,以及健壮性的增强,我们可以打造出更高效、可靠的引导程序。

对于操作系统开发者而言,理解引导程序的工作原理,不仅能解决“系统无法启动”的问题,更能为设计 定制化引导加载程序 ​(如UEFI引导)奠定基础。未来,随着硬件的发展(如NVMe固态硬盘、UEFI标准),引导程序也将面临新的挑战——但核心逻辑(初始化、文件搜索、内核移交)始终不变。

延伸阅读 ​:

  1. 《操作系统真相还原》——引导部分;
  2. FAT12文件系统规范(微软官方文档);
  3. Bochs调试指南——引导扇区调试技巧。

注:本文仅用于教育目的,实际渗透测试必须获得合法授权。未经授权的黑客行为是违法的。

本文标签: 系统 编程 文件系统