代码块一:
org 0x7c00
BaseOfStack equ 0x7c00
BaseOfLoader equ 0x1000
OffsetOfLoader equ 0x00
RootDirSectors equ 14
SectorNumOfRootDirStart equ 19
SectorNumOfFAT1Start equ 1
SectorBalance equ 17
jmp short Label_Start
nop
BS_OEMName db 'MINEboot'
BPB_BytesPerSec dw 512
BPB_SecPerClus db 1
BPB_RsvdSecCnt dw 1
BPB_NumFATs db 2
BPB_RootEntCnt dw 224
BPB_TotSec16 dw 2880
BPB_Media db 0xf0
BPB_FATSz16 dw 9
BPB_SecPerTrk dw 18
BPB_NumHeads dw 2
BPB_HiddSec dd 0
BPB_TotSec32 dd 0
BS_DrvNum db 0
BS_Reserved1 db 0
BS_BootSig db 0x29
BS_VolID dd 0
BS_VolLab db 'boot loader'
BS_FileSysType db 'FAT12 '
这段程序中的代码BaseOfLoader equ 0x1000和offsetofLoader equ 0x00组合成了Loader程序的起始物理地址,这个组合必须经过实模式的地址变换公式才能生成物理地址,即BaseofLoader<<4 +offsetofLoader = 0x10000。
代码RootDirSectors equ 14定义了根目录占用的扇区数,这个数值是根据FAT12文件系统提供的信息经过计算而得,即(BPB_RootEntCnt * 32 + BPB_BytesPerSec - 1)/BPB_BytesPersec = ( 224×32 + 512 - 1)/ 512 = 14。
等价语句sectorNumofRootDirstart equ 19定义了根目录的起始扇区号,这个数值也是通过计算而得,即保留扇区数+FAT表扇区数*FAT表份数=1 + 9 * 2= 19,因为扇区编号的计数值从O开始,故根目录的起始扇区号为19。
程序SectorNumOfFAT1Start equ 1代表了FAT1表的起始扇区号,在FAT1表前面只有一个保留扇区(引导扇区),而且它的扇区编号是0,那么FAT1表的起始扇区号理应为1。
汇编代码sectorBalance equ 17用于平衡文件(或者目录)的起始簇号与数据区起始簇号的差值。更通俗点说,因为数据区对应的有效簇号是2(FAT[2]),为了正确计算出FAT表项对应的数据区起始扇区号,则必须将FAT表项值减2,或者将数据区的起始簇号/扇区号减2(仅在每簇由一个扇区组成时可用)。本程序暂时采用一种更取巧的方法是,将根目录起始扇区号减2(19-2=17),进而间接把数据区的起始扇区号(数据区起始扇区号=根目录起始扇区号+根目录所占扇区数)减2。
代码块二:
;======= read one sector from floppy
Func_ReadOneSector:
push bp
mov bp, sp
sub esp, 2
mov byte [bp - 2], cl
push bx
mov bl, [BPB_SecPerTrk]
div bl
inc ah
mov cl, ah
mov dh, al
shr al, 1
mov ch, al
and dh, 1
pop bx
mov dl, [BS_DrvNum]
Label_Go_On_Reading:
mov ah, 2
mov al, byte [bp - 2]
int 13h
jc Label_Go_On_Reading
add esp, 2
pop bp
ret
代码中的Func_ReadOnesector模块负责实现软盘读取功能,它借助BIOS中断服务程序INT 13h的主功能号AH=02h实现软盘扇区的读取操作,该中断服务程序的各寄存器参数说明如下。
INT 13h,AH=02h 功能:读取磁盘扇区。
- AL=读入的扇区数(必须非0);
- CH=磁道号(柱面号)的低8位;
- CL=扇区号1~63 ( bit 0~5 ),磁道号(柱面号)的高2位( bit 6~7,只对硬盘有效);
- DH=磁头号;
- DL=驱动器号(如果操作的是硬盘驱动器,bit 7必须被置位);
- ES:BX-一>数据缓冲区。
模块Func_ReadOnesector仅仅是对BIOS中断服务程序的再次封装,以简化读取磁盘扇区的操作过程,进而在调用Func_ReadOnesector模块时,只需传递下列参数到对应的寄存器中,即可实现磁盘扇区的读取操作。模块Func_ReadOnesector详细参数说明如下。
模块Func_Readonesector功能:读取磁盘扇区。
- AX=待读取的磁盘起始扇区号;
- CL=读入的扇区数量;
- ES:BX=>目标缓冲区起始地址。
因为Func_ReadOnesector模块传入的磁盘扇区号是LBA (Logical Block Address,逻辑块寻址)格式的,而INT 13h,AH=O2h中断服务程序只能受理CHS ( Cylinder /Head/Sector,柱面/磁头/扇区)格式的磁盘扇区号,那么必须将LBA格式转换为CHS格式,通过下列公式可将LBA格式转换为CHS格式。
模块Func_ReadOnesector在读取软盘之前,会先保存栈帧寄存器和栈寄存器的数值,从栈中开辟两个字节的存储空间(将栈指针向下移动两个字节),由于此时代码bp - 2与ESP寄存器均指向同一内存地址,所以CL寄存器的值就保存在刚开辟的栈空间里。而后,使用AX寄存器(待读取的磁盘起始扇区号)除以BL寄存器(每磁道扇区数),计算出目标磁道号(商:AL寄存器)和目标磁道内的起始扇区号(余数:AH寄存器),考虑到磁道内的起始扇区号从1开始计数,故此将余数值加1,即inc ah。紧接着,再按照上公式计算出磁道号(也叫柱面号)与磁头号,将计算结果保存在对应寄存器内。最后,执行INT 13h中断服务程序从软盘扇区读取数据到内存中,当数据读取成功(CF标志位被复位)后恢复调用现场。
代码块三:
;======= search loader.bin
mov word [SectorNo], SectorNumOfRootDirStart
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
Label_Go_On:
inc di
jmp Label_Cmp_FileName
Label_Different:
and di, 0ffe0h
add di, 20h
mov si, LoaderFileName
jmp Label_Search_For_LoaderBin
Label_Goto_Next_Sector_In_Root_Dir:
add word [SectorNo], 1
jmp Lable_Search_In_Root_Dir_Begin
通过这段代码能够从根目录中搜索出引导加载程序(文件名为loader.bin )。在程序执行初期,程序会先保存根目录的起始扇区号,并依据根目录占用磁盘扇区数来确定需要搜索的扇区数,并从根目录中读入一个扇区的数据到缓冲区;接下来,遍历读入缓冲区中的每个目录项,寻找与目标文件名字符串( “LOADER BIN" , 0 )相匹配的目录项,其中DX寄存器记录着每个扇区可容纳的目录项个数(512/32= 16=0x10 ),CX寄存器记录着目录项的文件名长度(文件名长度为11B,包括文件名和扩展名,但不包含分隔符“.”)。在比对每个目录项文件名的过程中,使用了汇编指令LODSB,该命令的加载方向与DF标志位有关,因此在使用此命令时需用CLD指令清DF标志位。
以下是Intel官方白皮书对LODSB/LODSW/LODSD/LODSQ指令的概括描述。
1.该命令可从DS:(R|E)SI寄存器指定的内存地址中读取数据到AL/AX/EAX/RAX寄存器。
2.当数据载入到AL/AX/EAX/RAX寄存器后,(RE)SI寄存器将会依据R|EFLAGs标志寄存器的DF标志位自动增加或减少载人的数据长度(1/2/4/8字节)。当DF=0时,(RE)SI寄存器将会自动增加;反之,(R|E)SI寄存器将会自动减少。
一旦发现完全匹配的字符串,则跳转到Label_FileName_Found处执行;如果没有找到,那么就执行其后的Label_No_LoaderBin模块,进而在屏幕上显示提示信息,通知用户引导加载程序不存在。
特别注意,因为FAT12文件系统的文件名是不区分大小写字母的,即使将小写字母命名的文件复制到FAT12文件系统内,文件系统也会为其创建大写字母的文件名和目录项。而小写字母文件名只作为其显示名,真正的数据内容皆保存在大写字母对应的目录项。所以这里应该搜索大写字母的文件名字符串。
代码块四:
;======= display on screen : ERROR:No LOADER Found
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
jmp $
这段代码借助BIOS中断处理程序INT 10h ,将字符串ERROR :No LOADER Found显示到屏幕的第1行第0列上。
代码块五:
;======= get FAT Entry
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
mov bx, 8000h
add ax, SectorNumOfFAT1Start
mov cl, 2
call Func_ReadOneSector
pop dx
add bx, dx
mov ax, [es:bx]
cmp byte [Odd], 1
jnz Label_Even_2
shr ax, 4
Label_Even_2:
and ax, 0fffh
pop bx
pop es
ret
此前已经提及FAT12文件系统的每个FAT表项占用12 bit,即每三个字节存储两个FAT表项,由此看来,FAT表项的存储位置是具有奇偶性的。使用Func_GetFATEntry模块可根据当前FAT表项索引出下一个FAT表项,该模块的寄存器参数说明如下。
模块Func_GetFATEntry 功能:根据当前FAT表项索引出下一个FAT表项。
- AH=FAT表项号(输入参数/输出参数)。
这段程序首先会保存FAT表项号,并将奇偶标志变量(变量[odd])置0。因为每个FAT表项占1.5 B,所以将FAT表项乘以3除以2(扩大1.5倍)来判读余数的奇偶性并保存在[odd]中(奇数为1,偶数为0 ),再将计算结果除以每扇区字节数,商值为FAT表项的偏移扇区号,余数值为FAT表项在扇区中的偏移位置。接着,通过Func_Readonesector模块连续读入两个扇区的数据,此举的目的是为了解决FAT表项横跨两个扇区的问题。最后,根据奇偶标志变量进一步处理奇偶项错位问题,即奇数项向右移动4位。有能力的读者可自行将FAT12文件系统替换为FAT16文件系统,这样可以简化FAT表项的索引过程。
在完成Func_ReadOnesector和Func_GetFATEntry模块后,就可借助这两个模块把loader.bin文件内的数据从软盘扇区读取到指定地址中。代码清单3-10实现了从FAT12文件系统中加载loader.bin文件到内存的过程。
代码块六:
;======= found loader.bin name in root director struct
Label_FileName_Found:
mov ax, RootDirSectors
and di, 0ffe0h
add di, 01ah
mov cx, word [es:di]
push cx
add cx, ax
add cx, SectorBalance
mov ax, BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader
mov ax, cx
Label_Go_On_Loading_File:
push ax
push bx
mov ah, 0eh
mov al, '.'
mov bl, 0fh
int 10h
pop bx
pop ax
mov cl, 1
call Func_ReadOneSector
pop ax
call Func_GetFATEntry
cmp ax, 0fffh
jz Label_File_Loaded
push ax
mov dx, RootDirSectors
add ax, dx
add ax, SectorBalance
add bx, [BPB_BytesPerSec]
jmp Label_Go_On_Loading_File
Label_File_Loaded:
jmp $
在Label_FileName_Found模块中,程序会先取得目录项DIR_FstClus字段的数值,并通过配置ES寄存器和BX寄存器来指定loader.bin程序在内存中的起始地址,再根据loader.bin程序的起始簇号计算出其对应的扇区号。为了增强人机交互效果,此处还使用BIOS中断服务程序INT 10h在屏幕上显示一个字符' .'。接着,每读入一个扇区的数据就通过Func_GetFATEntry模块取得下一个FAT表项,并跳转至Label_Go_on_Loading_File处继续读入下一个簇的数据,如此往复,直至Func_GetFATEntry模块返回的FAT表项值是offfh为止。当loader.bin文件的数据全部读取到内存后,跳转至Label_File_Loaded处准备执行loader.bin程序。
这段代码使用了BIOS中断服务程序INT10h的主功能号AH=0Eh在屏幕上显示一个字符。详细寄存器参数说明如下。
INT 10h,AH=OEh 功能:在屏幕上显示一个字符。
- AL=待显示字符;
- BL=前景色。
代码块七:
;======= tmp variable
RootDirSizeForLoop dw RootDirSectors
SectorNo dw 0
Odd db 0
;======= display messages
StartBootMessage: db "Start Boot"
NoLoaderMessage: db "ERROR:No LOADER Found"
LoaderFileName: db "LOADER BIN",0
这三个变量用于保存程序运行时的临时数据,上文已经讲解了它们的使用过程,此处不再过多讲述。
上述字符串均是屏幕上显示的日志信息。值得说明的是,NASM编译器中的单引号与双引号作用相同,并非如标准C语言中规定的:双引号会在字符串结尾处自动添加字符· \0',而在NASM编译器中必须自行添加。不过,本程序使用的BIOS中断服务程序必须明确提供显示的字符串的长度,不需要判读字符串结尾处的字符'\0 '。
目前,我们还未进入Loader引导加载程序的开发环节,所以在Label_File_Loaded处使用代码jmp s,让程序死循环在此处。
完整无注释代码
boot.am
org 0x7c00
BaseOfStack equ 0x7c00
BaseOfLoader equ 0x1000
OffsetOfLoader equ 0x00
RootDirSectors equ 14
SectorNumOfRootDirStart equ 19
SectorNumOfFAT1Start equ 1
SectorBalance equ 17
jmp short Label_Start
nop
BS_OEMName db 'MINEboot'
BPB_BytesPerSec dw 512
BPB_SecPerClus db 1
BPB_RsvdSecCnt dw 1
BPB_NumFATs db 2
BPB_RootEntCnt dw 224
BPB_TotSec16 dw 2880
BPB_Media db 0xf0
BPB_FATSz16 dw 9
BPB_SecPerTrk dw 18
BPB_NumHeads dw 2
BPB_HiddSec dd 0
BPB_TotSec32 dd 0
BS_DrvNum db 0
BS_Reserved1 db 0
BS_BootSig db 0x29
BS_VolID dd 0
BS_VolLab db 'boot loader'
BS_FileSysType db 'FAT12 '
Label_Start:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
;======= clear screen
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 0184fh
int 10h
;======= set focus
mov ax, 0200h
mov bx, 0000h
mov dx, 0000h
int 10h
;======= display on screen : Start Booting......
mov ax, 1301h
mov bx, 000fh
mov dx, 0000h
mov cx, 10
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartBootMessage
int 10h
;======= reset floppy
xor ah, ah
xor dl, dl
int 13h
;======= search loader.bin
mov word [SectorNo], SectorNumOfRootDirStart
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
Label_Go_On:
inc di
jmp Label_Cmp_FileName
Label_Different:
and di, 0ffe0h
add di, 20h
mov si, LoaderFileName
jmp Label_Search_For_LoaderBin
Label_Goto_Next_Sector_In_Root_Dir:
add word [SectorNo], 1
jmp Lable_Search_In_Root_Dir_Begin
;======= display on screen : ERROR:No LOADER Found
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
jmp $
;======= found loader.bin name in root director struct
Label_FileName_Found:
mov ax, RootDirSectors
and di, 0ffe0h
add di, 01ah
mov cx, word [es:di]
push cx
add cx, ax
add cx, SectorBalance
mov ax, BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader
mov ax, cx
Label_Go_On_Loading_File:
push ax
push bx
mov ah, 0eh
mov al, '.'
mov bl, 0fh
int 10h
pop bx
pop ax
mov cl, 1
call Func_ReadOneSector
pop ax
call Func_GetFATEntry
cmp ax, 0fffh
jz Label_File_Loaded
push ax
mov dx, RootDirSectors
add ax, dx
add ax, SectorBalance
add bx, [BPB_BytesPerSec]
jmp Label_Go_On_Loading_File
; Label_File_Loaded:
; jmp $
;======= read one sector from floppy
Func_ReadOneSector:
push bp
mov bp, sp
sub esp, 2
mov byte [bp - 2], cl
push bx
mov bl, [BPB_SecPerTrk]
div bl
inc ah
mov cl, ah
mov dh, al
shr al, 1
mov ch, al
and dh, 1
pop bx
mov dl, [BS_DrvNum]
Label_Go_On_Reading:
mov ah, 2
mov al, byte [bp - 2]
int 13h
jc Label_Go_On_Reading
add esp, 2
pop bp
ret
;======= get FAT Entry
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
mov bx, 8000h
add ax, SectorNumOfFAT1Start
mov cl, 2
call Func_ReadOneSector
pop dx
add bx, dx
mov ax, [es:bx]
cmp byte [Odd], 1
jnz Label_Even_2
shr ax, 4
Label_Even_2:
and ax, 0fffh
pop bx
pop es
ret
;======= tmp variable
RootDirSizeForLoop dw RootDirSectors
SectorNo dw 0
Odd db 0
;======= display messages
StartBootMessage: db "Start Boot"
NoLoaderMessage: db "ERROR:No LOADER Found"
LoaderFileName: db "LOADER BIN",0
;=========================
Label_File_Loaded:
jmp BaseOfLoader:OffsetOfLoader
;======= fill zero until whole sector
times 510 - ($ - $$) db 0
dw 0xaa55
简单的loader.asm代码
org 10000h
mov ax,cs
mov ds,ax
mov es,ax
mov ax,0x00
mov ss,ax
mov sp,0x7c00
;========== 显示 Start Loader....
mov ax,1301h
mov bx,000fh
mov dx,0200h
mov cx,12
push ax
mov ax,ds
mov es,ax
pop ax
mov bp,StartLoaderMessage
int 10h
jmp $
;=============
StartLoaderMessage : db "Start Loader"
摘自:《一个64位操作系统的设计与实现》