一台32位Intel的电脑就可以了,使用Windows操作系统。然后下载一些软件安装上:
| 软件名 | 下载地址 | 说明 |
| NASM | http://nasm.sourceforge.net/ | nasm –f bin Boot4.asm –o Boot4.bin |
| PartCopy | http://www.brokenthorn.com/Resources/Programs/pcopy02.zip | partcopy Boot4.bin 0 200 -f0 |
|
VFD - Virtual Floppy Drive |
http://sourceforge.net/projects/vfd/ | |
|
Bochs Emulator |
http://bochs.sourceforge.net/ |
NASM和PartCopy需要设置一下环境变量,在Path中添加指向其.exe目录的文件夹即可。其它问题如果有什么不明白的,可以参看这里。
2、Bootloader
好了,我们直接进入启动程序。整个程序如下:
这个程序经过NASM编译之后会形成一个大小恰好为512B的文件,我们使用下面的命令来编译这个名为Boot4.asm的文件(为什么叫Boot4.asm?因为测试这个程序时正好是这个编号:)
nasm -f bin Boot4.asm -o Boot4.bin
启动VFD,创建一个虚拟软盘,命名为A盘。然后使用PartCopy把Boot4.bin这个文件拷贝到软盘的第一个扇区:
partcopy Boot4.bin 0 200 –f0
现在在软盘的第一个扇区就是我们的这个启动程序。计算机启动时会按顺序检查BIOS设定的所有启动设备(比如按照软驱、光驱、磁盘的顺序来检测是否在其中有可以启动的设备)。在这里,我们把Boot4.bin写入了磁盘的第一个扇区(磁盘的每个扇区为512B),并且这个文件的末尾为0xaa55,这个特殊的字节序列表示这是一个可以启动的文件。BIOS就把这个文件放到内存的 0x7c00:0 这个地址,去执行这个文件。大致过程可以参看这里(计算机按下电源后发生了什么)。
有关这个汇编程序Boot4.asm的详细解释我们后面再做。下面我们编写一个超级简单的操作系统Stage2.asm
3、一个超级简单的操作系统Stage2.asm
不用任何解释,直接给出这个操作系统的代码。它的主要功能就是在屏幕上打印出 “Preparing to load operating system...”这个字符串
之所以把这个文件叫做Stage2.asm,是因为这是系统启动的第二个阶段,这个操作系统是由Boot4.bin从磁盘中读取出来并且加载到内存中的这个文件会被加载到0x7c00:0x0200这个内存地址上。现在我们使用NASM把这个文件编译成一个二进制文件:
nasm -f bin Stage2.asm KRNLDR.SYS
之所以把它编译成为KRNLDR.SYS,是因为在Boot4.asm中,我们设定了 ImageName db "KRNLDR SYS" 这个语句。现在只要知道有这么回事就可以了。这个文件名不能随便改。
下面我们把KRNLDR.SYS拷贝到磁盘A中:
copy KRNLDR.SYS A:\
这时候,检查A盘,就会发现里面多出了一个KRNLDR.SYS这个文件。
4、设置模拟器
下面我们使用Bochs这个模拟器来模拟系统的启动。首先安装这个模拟器。然后建立一个文件,名字叫做bochsrc.bxrc,里面的内容为:
保存好后,运行这个文件,就可以看到模拟器启动了。最后稳定之后的界面应该是这个样子的:
好了,到现在为止,我们的操作系统就已经运行完成了,打印出了一个字符串。如果你忘了把KRNLDR.SYS文件拷贝到A盘,它还会提示你出错。
按一下这个界面上面的Power键,就可以结束这次模拟了。
好了,有关代码的具体介绍,我们后面再进行。
这一节我们详细介绍Boot4.asm这个汇编程序。
1、程序设定
1: ;*********************************************
asm
3: ; - A Simple Bootloader
4: ;*********************************************
5:
//www.docin.com/p-13154518.html
7: bits 16
第1到4行为注释。
第6行的代码org 0表示在对Boot4.asm进行编译时,所有的内存寻址都会以0x0为起点开始寻找。在这里这个命令不写也可以。有时候我们会看到“org 0x7c00”这样的命令,它表示在汇编的时候对于内存寻址指令都要加上一个0x7c00的偏移。有关org命令的详细问题可以参看:NASM-ORG指令深入理解。
org指令指出程序将要被加载到内存的起始地址。org指令只会在编译期影响到内存寻址指令的编译(编译器会把所有程序用到的段内偏移地址自动加上org后面的数值),而其自身并不会被编译成机器码。
比如有一个“mov si, msg”的指令,如果不加org 0x7c00,那么msg只会被编译成它的原始地址(即在.bin文件中的地址)。加上org 0x7c00之后,编译器会把msg之后再加上0x7c00的值放到mov指令中去。看不明白的还是看上面的链接吧。
第7行的指令告诉编译器我们是在16位下进行编码的。"BITS“指令是用来指定NASM产生的代码是被设计运行在16位模式还是运行在32位模式的处理器上。由于机器刚启动时是运行在16位的实模式下,所以我们要设定这个编译选项。
2、 开始执行
1: start:
2: jmp main
第一行的start是汇编程序开始执行的地方,程序从这里开始执行。第2行表示跳转到main标记执行。
3、简单的FAT12文件系统
由于我们需要把文件存储在软盘上,所以需要在软盘的第一个扇区上写入一些信息,来表明如何对这个软盘进行的进行管理。就像我们有一个很大的空仓库,我们需要在里面弄出一些隔间,以便于我们管理这个仓库中存储的东西。这些信息就用来描述这个软盘上的文件系统。这些信息如下:
1: ;*********************************************
2: ; BIOS Parameter Block
3: ;*********************************************
4:
do a far jump, which is 3 bytes in size.
after it to offset the 3rd byte.
7:
; OEM identifier (Cannot exceed 8 bytes!)
9: bpbBytesPerSector: DW 512
10: bpbSectorsPerCluster: DB 1
11: bpbReservedSectors: DW 1
12: bpbNumberOfFATs: DB 2
13: bpbRootEntries: DW 224
14: bpbTotalSectors: DW 2880
15: bpbMedia: DB 0xf8 ;; 0xF1
16: bpbSectorsPerFAT: DW 9
17: bpbSectorsPerTrack: DW 18
18: bpbHeadsPerCylinder: DW 2
19: bpbHiddenSectors: DD 0
20: bpbTotalSectorsBig: DD 0
21: bsDriveNumber: DB 0
22: bsUnused: DB 0
23: bsExtBootSignature: DB 0x29
24: bsSerialNumber: DD 0xa0a1a2a3 ; will be overwritten
这里我们需要简单了解一些软盘的物理结构。
如上图所示,一个软盘可能有多个盘片,每个盘片可能上下两面都能存储信息,这样一个盘片就对应着两个读取头(Head)。我们把每个盘面划分成一个一个的同心圆环,每个圆环就是一个“轨道”(或者叫“磁道”,英文名为Track,就是上图中每个盘面上红色的部分)。然后把每个“轨道”划分成一个一个的“扇区”(英文为sector),如上图的黑色数字所示。每个轨道可以划分出18个扇区,每个扇区的大小不多不少正好是512 Bytes。“柱面”(英文cylinder)则是各个盘面上同一半径上的轨道的集合。
软盘一般只有两个Head,有的还可能只有一个。整个磁盘的扇区最多为2880个。
多个连续的扇区可以组成一个“集合”(Cluster),作为比较大的空间划分。
下面我们来简单解释一下这个文件系统。从名字上就可以看出他们的含义,我们只解释一些比较难懂的。
第11行:Reseved Sectors表明有几个扇区不被包含在FAT12文件系统中。一般来说每个软盘都有一个启动扇区,即bootsector,这里面存储着bootloader,用来启动操作系统。这个启动扇区一般不会被包含在FAT12文件系统中。所以此处的数值为1.
第12行:FAT即File Allocation Table。这个表用来指示FAT12文件系统中存储了哪些数据。FAT12文件系统中都有2个FATs
第23 - 26行是软盘的版本信息。后面两个字符串必须是11B 和 8B,不能多也不能少。
更加详细的解释请参看:http://www.brokenthorn.com/Resources/OSDev5.html
4、打印字符串
这一个程序段用来打印一个以0结尾的字符串,这个字符串的地址被放在SI寄存器中。代码如下:
1: ;***************************************
2: ; Prints a string
3: ; DS=>SI: 0 terminated string
4: ;***************************************
5:
6: Print:
7: lodsb ; load next byte from string from SI to AL
8: or al, al ; Does AL=0?
9: jz PrintDone ; Yep, null terminator found-bail out
10: mov ah, 0eh ; Nope-Print the character
int 10h
12: jmp Print ; Repeat until null terminator found
13: PrintDone:
14: ret ; we are done, so return
第7行的LODSB指令从SI中复制一个字节到AL中,然后SI移动到字符串的下一个字节。这个指令的全称可能是load string byte。
这段代码中有一个中断调用,int 10h。在实模式下,BIOS程序会在内存的开始部分建立一个中断向量表,所有的中断指令都会使用这个向量表。建立这个表的过程可以参看这里。中断0x10的各个参数如下:
|
INT 0x10 - VIDEO TELETYPE OUTPUT AH = 0x0E AL = Character to write BH - Page Number (Should be 0) BL = Foreground color (Graphics Modes Only) |
有了这些参数,在看上面的程序就非常简单了。我们首先把SI的一个字节放到AL中,等待打印。然后检测AL中的字符是否为0,如果不为0,就把AH中放入0x0e,然后执行中断指令0x10,这样就可以把AL中的字符打印在屏幕上了。
5、从软盘中读取内容
操纵系统的启动需要两个部分。第一部分由BIOS把软盘第一个扇区的bootloader加载到内存0x7c00处,然后执行这个bootloader。由于软盘的第一个扇区只能有512B的大小,所以这个bootloader不能执行很多功能。这个bootloader接着从软盘中读取另一份文件(程序)加载到内存中,这个程序的大小就没有限制了,可以做更多的事情,设定计算机的环境,加载真正的操作系统。
从软盘中把一个程序加载到内存的代码如下所示:
1: ;************************************************;
2: ; Reads a series of sectors
3: ; Input:
4: ; CX=>Number of sectors to read
5: ; AX=>Starting sector (logical block addressing)
6: ; ES:BX=>Buffer to read to
7: ; Changed:
8: ; DI, SI, AX, CX, BX
9: ;************************************************;
10:
11: ReadSectors:
12: .MAIN:
for error
14: .SECTORLOOP:
15: push ax
16: push bx
17: push cx
18: call LBACHS ; compute absoluteTrack, absoluteSector, absoluteHead
19: mov ah, 0x02 ; BIOS read sector
20: mov al, 0x01 ; read one sector
21: mov ch, BYTE [absoluteTrack]
22: mov cl, BYTE [absoluteSector]
23: mov dh, BYTE [absoluteHead]
24: mov dl, BYTE [bsDriveNumber]
int 0x13 ; invoke BIOS
for read error. CF=0 then jump
27: xor ax, ax ; BIOS reset disk
int 0x13
29: dec di
30: pop cx
31: pop bx
32: pop ax
33: jnz .SECTORLOOP
int 0x18
35: .SUCCESS:
36: mov si, msgProgress
37: call Print
38: pop cx
39: pop bx
40: pop ax
41: add bx, WORD [bpbBytesPerSector] ; queue next buffer
42: inc ax ; queue next sector
43: loop .MAIN ; read next sector. Controlled by CX, If CX=0, then stop
44: ret
这里用到了中断指令int 0x13。这个指令可以有两个功能,一个功能是reset the floppy disk,把软盘的磁头重新定位到软盘的开始地方。另一个功能是读取软盘的扇区,把他们读到内存中。这两个功能的参数设置分别如下:
|
INT 0x13/AH=0x0 - DISK : RESET DISK SYSTEM AH = 0x0 DL = Drive to Reset Returns: AH = Status Code CF (Carry Flag) is clear if success, it is set if failure |
INT 0x13/AH=0x02 - DISK : READ SECTOR(S) INTO MEMORY AH = 0x02 AL = Number of sectors to read CH = Low eight bits of cylinder number CL = Sector Number (Bits 0-5). Bits 6-7 are for hard disks only DH = Head Number DL = Drive Number (Bit 7 set for hard disks) ES:BX = Buffer to read sectors to Returns: AH = Status Code AL = Number of sectors read CF = set if failure, cleared is successfull |
第19 - 25行对应着读取扇区的中断调用。第27 - 28行对应着重新定位软盘的中断调用。
注意第13行、29行、33、34行,对于每次读取扇区,13行设定了一个错误次数,超过这个次数就不再读扇区了。第29行对DI减一,这里已经出现了读取扇区的错误。当DI减到0的时候,就不再执行33行的跳转指令,执行34行的中断操作。
如果读取成功,就在屏幕上打印一个消息,然后接着读取下一个扇区。第41行、42行执行这个操作。
第18行所调用的函数 call LBACHS,是把对软盘的逻辑寻址方式转换成物理寻址方式。LBA表示的是Logical Block Addressing,CHS表示的是Cylinder/Head/Sector (CHS) addressing。本小节所介绍的ReadSectors这个函数所接受的AX中存放的是软盘的逻辑地址,所以这里要做一个转换,把这个逻辑地址转换成相应的物理地址,在第21 - 24行用到。具体的介绍我们在后面进行。
更改:我在第11行和12行之间加上了一句“dec cx”,结果仍然正确。因为我检查这段程序时发现读取的次数要比CX中的数值大1。不知道这样改动是否有什么问题。
6、把Cluster转换成软盘的逻辑扇区地址
代码如下:
1: ;************************************************;
2: ; Convert Cluster to LBA
3: ; Input:
4: ; AX=>the cluster to be changed
5: ; Changed:
6: ; AX, CX
7: ; Return:
8: ; AX=>sector number
9: ; LBA = (cluster - 2) * sectors per cluster
10: ;************************************************;
11:
12: ClusterLBA:
13: sub ax, 0x0002 ; zero base cluster number
14: xor cx, cx
15: mov cl, BYTE [bpbSectorsPerCluster] ; convert byte to word
16: mul cx
17: add ax, WORD [datasector] ; base data sector
18: ret
代码中的第9行就是这种转换的公式,这个函数就是实现了这个公式。我们下面简要介绍一下软盘的逻辑扇区与Cluster的关系,以及逻辑扇区与CHS的关系。
我们可以想象把软盘的所有扇区放到一个长长的带子上,第一个扇区的标号为0,以后的扇区标号依次增加1,直至最后一个扇区。这样的描述方式是一种逻辑上的描述方式,它被称作LBA(Logical Blocking Addressing)。实际上软盘是通过柱面(Cylinder)、磁头(Head)、扇区(Sector)这几个值来确定的,被称作CHS寻址方式。我们想要访问软盘上的一个扇区,最终是要通过CHS方式来访问的。但是LBA可以转换成对应的CHS,所以我们通常也用逻辑扇区来表示一个扇区。这种转换的具体过程看下一小节。
为了存储比较大的文件,通常把借个连续的逻辑扇区合在一起组成一个Cluster。FAT12中的每个Cluster中只含有一个Sector。并且Cluster的编号是从2开始的,第一个Cluster的编号就是2,它是从Data Area开始的。所以把一个Cluster编号转换成逻辑扇区编号时,首先要减去2,最后还要加上datasector的起始地址。
有关FAT12的介绍可以参看第9小节。FAT12文件系统更加详细的介绍参看:An overview of FAT12。
7、把逻辑扇区转换成CHS
其代码如下:
1: ;************************************************;
2: ; Convert LBA to CHS
3: ; Input:
4: ; AX=>LBA Address to convert
5: ; Changed:
6: ; DX, AX
7: ; Return:
8: ; BYTE [absoluteSector], BYTE [absoluteHead], BYTE [absoluteTrack]
9: ;
10: ; absolute sector = (logical sector % sectors per track) + 1
11: ; absolute head = (logical sector / sectors per track) MOD number of heads
12: ; absolute track = logical sector / (sectors per track * number of heads)
13: ;
14: ;************************************************;
15:
16: LBACHS:
for operation
18: div WORD [bpbSectorsPerTrack]
for sector 0
20: mov BYTE [absoluteSector], dl
21: xor dx, dx
22: div WORD [bpbHeadsPerCylinder]
23: mov BYTE [absoluteHead], dl
24: mov BYTE [absoluteTrack], al
25: ret
第10 - 12行的三个公式就是转换公式,这个函数就是实现这个公式。我们现在AX中放入将要转换的逻辑地址,然后调用这个函数,就会把相应的物理地址放到相应的几个变量中。
这里需要注意的就是除法的使用。第18行是一个除法,计算AX / [bpbSectorsPerTrack]的值,商放在AX中,余数放在DX中。这样19行的结果就是absolute sector的值。然后再看第22行,用此时AX中的值除以bpbHeadsPerCylinder,商放在AX中,余数放在DX中。这样第23、24行正好计算出absolute head 和 absolute track。
经过这种运算之后的物理地址就可以在第5部分中用来读取软盘中的内容了。
8、Bootloader入口
1: ;*********************************************
2: ; Bootloader Entry Point
3: ;*********************************************
4:
5: main:
6:
7: ;-----------------------------------------------------
8: ; code located at 0000:7c00, adjust segment registers
9: ;-----------------------------------------------------
10:
11: cli
12: mov ax, 0x07c0 ; setup registers to point to our segment. s*16+off = address
13: mov ds, ax
14: mov es, ax
15: mov fs, ax
16: mov gs, ax
17:
18: ;-----------------------------------------------------
19: ; create stack
20: ;-----------------------------------------------------
21:
22: mov ax, 0x0000 ; set the stack
23: mov ss, ax
24: mov sp, 0xffff
25: sti ; restore interrupts
26:
27: ;-----------------------------------------------------
message
29: ;-----------------------------------------------------
30:
32: call Print
第2部分所介绍的跳转指令直接会跳转到这这里的第5行进行执行。
这里需要注意的就是第12行。由于我们的程序会被BIOS加载到内存的0x7c00处,而我们在开始时使用的是org 0,并没有对这个文件中的寻址在编译时指定偏移量,所以此处要设定各个段寄存器用以进行寻址。在16位实模式下的寻址方式是Segment:Offset,它所指示的实际地址是Segment*16+Offset。我们在这里设定所有的段寄存器的值为0x07c0,在进行寻址的时候,真实地址就会是0x7c00+Offset。我们在这个程序中的所有寻址都只是指定了Offset,当这个程序被加载到内存的0x7c00处的时候,就可以进行正确的寻址了。
9、加载root directory table
以下几节我们介绍如何把软盘中的一个文件读入到内存中。我们首先看一下FAT12文件系统在软盘上的结构:
第一个扇区就是Boot Sector,我们把我们自己写的bootloader(即Boot4.bin)就放在这里面。有关FAT12文件系统的一些配置信息也在这个扇区中存储着。
第3部分的第11行代码bpbReservedSectors描述了FAT12文件系统的Extra Reserved Sectors。
File Allocation Table (FAT)是一个类似于数组的数据结构,数组中每个元素的大小为12bit,里面存储的是一些Cluster的地址信息。由于这个大小只有12bit,所以总过cluster的个数不会超过4096个。这12bit中存储的一些数值的意义如下:
|
FAT12文件系统中一般有两个FAT表,第二个和第一个完全一样,一般用不到。
Root Directory也是一个表,这个表中的每个元素的大小为32bytes,每个元素的信息如下:
|
黑体标注的是比较重要的部分。注意bytes 0 – bytes 10是文件名,FAT12系统的文件名只能是11 bytes,不能多也不能少。最后几个字节指出了这个文件的第一个Cluster的位置,并且给出了这个文件的大小。
在多介绍一些cluster的事情。我们前面说过,软盘中一个扇区的大小只能是512B。如果一个文件大于这个数值,就要存储在多个扇区中,这样一些扇区的集合就是一个Cluster。在BPB(即第3部分的文件系统信息)中指定了每个Cluster使用几个扇区。
要想把一个文件从软盘中加载到内存,首先需要知道这个文件的存储位置。由于软盘中的所有文件信息都存储在Root Directory这个表中,所以我们首先要把这个表读取出来。代码如下:
1: ;-----------------------------------------------------
2: ; load root directory table
3: ;-----------------------------------------------------
4:
5: LOAD_ROOT:
6:
8:
9: xor cx, cx
10: xor dx, dx
11: mov ax, 0x0020 ; 32 bytes directory entry
12: mul WORD [bpbRootEntries] ; total size of directory. bpbTotalSectors = 2880
13: div WORD [bpbBytesPerSector] ; sectors used by directory. ax is the consult
14: xchg ax, cx ; now cx is the result, ax is 0x0000
15:
17:
18: mov al, BYTE [bpbNumberOfFATs]
19: mul WORD [bpbSectorsPerFAT]
20: add ax, WORD[bpbReservedSectors]
21: mov WORD [datasector], ax ; base of root directory
22: add WORD [datasector], cx ; ?
23:
24: ; read root directory into memory (7c00:0200)
25:
26: mov bx, 0x0200
27: call ReadSectors
第7 - 14行计算这个表的大小。bpbRootEntries中存储的是这个表中一共有多少个Entries,即有多少个32Bytes的元素。每当我们向软盘中加入或者删除文件时,Windows系统会自动帮我们改变这些数值。这段代码计算出这个表占用多少个扇区,把这个数值存储在CX中。
第16 - 20行计算这个表的起始地址。从本小节刚开始的那个图上,可以看出这个表的位置正好在Reserved Sectors和 FATs之后。这三块所占用的扇区的总数恰好是Root Directory的起始地址(其实我有些不太明白Boot Sector为什么没有加进来)。
第21、22行计算datasector的起始地址。存储起来。
第24 - 27行从软盘上读取这个Root Directory Table。注意第26行设置BX为0x0200,在ReadSectors这个程序中,我们把从软盘读到的文件放到内存的ES:BX处。注意在第8部分我们已经设置了ES为0x07c0,此处又设置了BX为0x0200。这样,Root Directory Table就会被读到内存的0x07c0:0x0200处,真实地址为0x7c00+0x0200。注意到我们的bootloader(即Boot4.bin)会被加载到内存的0x7c00处,而bootloader的大小不多不少只能是512B(用十六进制表示即0x200)。所以在内存中,bootloader的程序和Root Directory Table这两块内容是紧接在一起的,它们没有相互覆盖。
此时Root Directory Table就已经放到了内存的0x07c0:0x0200处。
更改:我在第20行和21行之间加上一句“inc ax”,结果仍然正确。加上这一句是为了把Boot Sector的那个扇区也加进来。结果还是和原来一样,就是不知道会不会有什么潜在的问题。
10、查找所要加载的文件
现在我们要查找Root Directory Table来找到我们要从软盘中读取的文件。代码如下:
1: ;------------------------------------------------
2: ; Find stage 2
3: ;------------------------------------------------
4:
for binary image
6:
7: mov cx, WORD [bpbRootEntries]
8: mov di, 0x0200
9:
10: .LOOP:
11: push cx
12: mov cx, 0x000b ; eleven character name
13: mov si, ImageName ; image name to find
14: push di
for entry match
16: pop di
is the pointer to ImageName in the Root Directory
18: pop cx
19: add di, 0x0020 ; queue next directory entry. Each entry in Root Directory is 32 bytes (0x20)
times.
21: jmp FAILURE
第15行的代码最重要。cmpsb用来比较[DS:SI]和[ES:DI]中的一个byte的内容是否一样。我们前面已经设定了DS和ES都为0x07c0,第13行设定SI为ImageName的偏移地址,第8行设定了DI的地址为0x0200。这样,[DS:SI]的内容就是我们所要查找的文件名,[ES:DI]就是Root Directory Table中第一个Entry的文件名。rep是一个重复指令,表示它后面的指令要重复CX次,第12行设定了CX为11(因为FAT12系统的文件名只能为11Bytes)。查找到对应的文件名后,就用地17行的指令跳转出去。否则就继续查找Root Directory Table的下一个Entry。第21行是执行出错信息。
如果找到了文件名ImageName所对应Root Directory Table中的条目,DI中就会存储指向这个条目的数值(是一个Offset,使用ES:DI可以知道在内存的真实地址)。
注意第7行,方括号表示的是对其中的内容进行寻址。其中的地址都是Offset,需要配合ES或者DS等段寄存器中存储的Segment来进行寻址。在16为实模式下的寻址方式为Segment:Offset,真实地址为Segment*16+Offset。
11、把FAT加载到内存
现在我们已经在Root Directory Table中找到了我们所要加载的文件所对应的信息。现在我们要把FAT加载到内存中,来查找这个表确定我们所要加载的文件究竟在何处。代码如下:
1: ;----------------------------------------------
2: ; load FAT
3: ;----------------------------------------------
4:
5: LOAD_FAT:
6:
7: ; save starting cluster of boot image
8:
9: mov si, msgCRLF
10: call Print
11: mov dx, WORD [di + 0x001a] ; di contains starting address of entry. Just refrence byte 26 (0x1A) of entry
12: mov WORD [cluster], dx ; file's first cluster
13:
15:
16: xor ax, ax
17: mov al, BYTE [bpbNumberOfFATs]
18: mul WORD [bpbSectorsPerFAT]
19: mov cx, ax
20:
22:
for bootsector
24:
25: ; read FAT into memory (07c0:0200)
26:
27: mov bx, 0x0200
28: call ReadSectors
根据第9小节的表,我们知道bytes 26 - 27是这个文件的第一个cluster的编号。现在我们先把这个内容提取出来。第11、12两行代码完成这个功能。最后这个信息放到了“cluster”这个变量中。
剩下的内容和加载Root Directory Table的时候差不多,就不再介绍了。
最后把FAT读入到内存的0x07c0:0x0200处,把刚才的Root Directory Table覆盖了。
12、把软盘中的文件加载到内存
现在我们把软盘中的ImageName所指示的文件加载到内存中。代码如下:
1: ; read image file into memory (0050:0000)
2:
3: mov si, msgCRLF
4: call Print
5: mov ax, 0x0050
6: mov es, ax
7: mov bx, 0x0000
8: push bx
9:
10: ;----------------------------------------------
11: ; load stage 2
12: ;----------------------------------------------
13:
14: LOAD_IMAGE:
15:
16: mov ax, WORD [cluster] ; cluster to read. File's first cluster
17: pop bx ; buffer to read into. ES:BX. es=0x0050
18: call ClusterLBA ; convert cluster to LBA
19: xor cx, cx
20: mov cl, BYTE [bpbSectorsPerCluster]
21: call ReadSectors
22: push bx ; next buffer to read to
23:
24: ; compute next cluster
25:
26: mov ax, WORD [cluster] ; identify current cluster
27: mov cx, ax ; copy current cluster
28: mov dx, ax
29: shr dx, 0x0001 ; divide by two
for (3/2)
31: mov bx, 0x0200 ; location of FAT in memory
32: add bx, cx ; index into FAT
33: mov dx, WORD [bx] ; read two bytes from FAT
34: test ax, 0x0001
35: jnz .ODD_CLUSTER
36:
37: .EVEN_CLUSTER:
38:
39: and dx, 0000111111111111b ; take low twelve bits
40: jmp .DONE
41:
42: .ODD_CLUSTER:
43:
44: shr dx, 0x0004 ; take high twelve bits
45:
46: .DONE:
47:
new cluster
for end of file
50: jb LOAD_IMAGE
到现在为止,内存中0x07c0:0000的地址(即0x7c00)上存储的是bootloader的程序(即我们编写的Boot4.bin),0x07c0:0x0200上存储的是FAT表,0x0处存放的是IVT中断向量表(参看这里)。现在我们要从软盘中读取一个文件,把这个文件放到内存的0x0050:0x0000地址上。由于调用ReadSectors函数时会使用ES:BX进行内存寻址,把从软盘读到的文件放到这个内存地址上,所以我们要先设置ES为0x0050,BX为0x0000。第5 - 8行完成了这个功能。
下面我们就要从软盘中读取这个文件的第一个Cluster中的内容。前面我们已经把软盘中存储这个文件的第一个Cluster的编号放到了“cluster”这个变量中。第16行读取这个变量,第18行把Cluster编号转变成逻辑扇区的编号,第21行根据这个逻辑扇区的编号读取一个Cluster的内容放到ES:BX所指示的内存中。此时的BX指向下一个将要加载文件的内存偏移量。22行把这个值压栈。
第24 - 48行计算这个文件的下一个Cluster的编号。我们下面详细介绍这部分功能。
FAT表中每一项大小为12bit。这个表的前两项(第0项和第1项)是用作特殊用途的。从编号为2的那一项(第三项)开始表示每一个Cluster,它们的编号是一一对应的。我们前面已经计算出了这个文件(ImageName所指示的文件)的第一个Cluster编号,我们首先要在FAT表中找到与之对应的那一项(12bit)。
由于我们已经把FAT表放到了0x07c0:0x0200处,所以我们要以此为基准找出所求项的地址。cluster*12/8 就是这一项在FAT表中的偏移量(Bytes)。然后我们读取2 Bytes的数据。如果这个cluster是偶数,那么我们就只取这16位数据的低12位。如果是奇数,那么我们就只取这16位数据的高12位。原因请看下图:
假定FAT的结构如图中灰色部分所示, 每个方格代表12个bit。下面的亮色部分表示的是FAT表的每一个Byte。通过对比,我们可以看出,当Cluster是偶数时,cluster*12/8计算出来的整数正好和某个Byte在低地址的地方(左侧)重合(如左侧的深黄色箭头所示),这样,当我们读取2个Bytes的时候,就会在高地址的地方多读出一些,所以我们只取低12位。如果Cluster是奇数,计算出的结果则如右侧的深黄色箭头所示,我们需要保留高地址上的12位。
当我们在FAT表中找到与当前“cluster”对应正确的那一项时,就可以读取里面的数据。这个数据就代表着下一个这个文件的下一个cluster的位置。我们就可以接着读取下一个Cluster中的数据了。
第49行比较当前的FAT数据是否小于0x0ff0,如果大于或等于这个数值,说明到达了文件的结尾,就不再继续读了。
更改:我把这段代码的第17、18行互换,结果仍然正确。因为我觉得“pop bx”是和“call ReadSectors”一伙的。这个改动应该不会有什么问题。
13、执行Stage2
前面我们已经把ImageName所指示的文件读入到了内存0x0050:0x0000处,现在我们要跳转到这个地址开始执行这里的代码。这个程序如下:
1: DONE:
2:
3: mov si, msgCRLF
4: call Print
5: push WORD 0x0050
6: push WORD 0x0000
7: retf ; jmp to 0x0050:0000 to excute
第5、6两行先把两个地址压入到栈中。
第7行的RETF是一个长跳转指令,它从栈中弹出两个元素,依次放入到IP和CS中。这样我们使用CS:IP进行寻址的时候就跳转到了0x0050:0x0000处。
有关ImageName所指示的文件的代码我们以后再介绍。
14、错误处理
代码如下:
1: FAILURE:
2:
3: mov si, msgFailure
4: call Print
5: mov ah, 0x00
int 0x16 ; a wait keypress
int 0x19 ; warm boot computer
在第11小节用到了这个错误处理。
15、数据定义
我们前面用到了一些msgFailure、cluster等数据,都在这里定义。它们仅仅是一个地址,存储了一些东西。代码如下:
1: absoluteSector db 0x00
2: absoluteHead db 0x00
3: absoluteTrack db 0x00
4:
5: datasector dw 0x0000
6: cluster dw 0x0000
, 0x0d, 0x0a, 0x00
9: msgCRLF db 0x0d, 0x0a, 0x00
, 0x00
, 0x0a, 0x00
第1 - 6行的数据在程序运行时都改变了它们的值。后面的数据的值在程序运行时没有发生改变。
16、补足512 Bytes
对于我们这个文件,Boot4.asm,它需要被编译成一个大小恰好为512B的文件,放到软盘的第一个扇区上,当BIOS启动时就可以检测到这段代码并且把这个代码加载到内存的0x7c00处。所以,我们的代码要保证编译之后的文件(Boot4.bin)大小恰好为512B。
并且,这个文件的最后两个字节一定要是0xaa55,这样,BIOS才能识别出这个程序是一个可以启动的程序。
代码如下:
1: TIMES 510-($-$$) db 0 ; confirm the compiled bin file is 512B
2: dw 0xaa55 ; the bootable special character
第1行的times指令是复制某个东西多少次。times之后紧跟的参数是复制的次数。我们的程序编译好之后要求为512B,除去最后两个字节的特殊标记,还剩下510 B。$ 表示当前指令所在的地址。$$ 表示程序的起始地址。第1行的指令表示向后填充那么多个0 Byte的意思。
好了,到现在为止,我们的Boot4.asm总算介绍完了。后面我们会再介绍ImageName所指示的那个文件是如何编写的。
这里我们简单介绍一下Stage2.asm这个程序。
整个程序代码如下:
1: ; Note: Here, we are executed like a normal COM program, but we are still in
this loader to set up 32 bit mode and basic exception
3: ; handling
4:
5: ; This loaded program will be our 32 bit kernal.
6:
do not have the limitation of 512 bytes here, so we can add anything we
8: ; want here!
9:
10: org 0x0 ; offset to 0, we will set segments later
11: bits 16 ; we are still in real mode
12:
13: ; we are loaded at linear address 0x10000
14:
15: jmp main
16:
17: ;*********************************
18: ; Prints a String
19: ; DS=>SI: 0 terminated string
20: ;*********************************
21:
22: Print:
23: lodsb
24: or al, al
25: jz PrintDone
26: mov ah, 0eh
int 10h
28: jmp Print
29: PrintDone:
30: ret
31:
32: ;********************************
33: ; Second Stage Loader Entry Point
34: ;********************************
35:
36: main:
37: cli
38: push cs
39: pop ds
40: ; xor ax, ax ; org 0x0500
41: ; mov ds, ax ; org 0x0500
42:
43: mov si, Msg
44: call Print
45:
46: cli
47: hlt
48:
49: ;********************************
section
51: ;********************************
52:
,13,10,0
这个程序非常简单,我们就不全部介绍了。只介绍第10行的这一句话。
由于我们在调用Print这个函数的时候,会使用DS:SI 来进行寻址,第10行把整个程序的偏移地址设为0,而在第38、39行重新设定了DS,所以不会产生问题(注意,Stage2.asm编译之后的二进制文件被加载到内存的0x0500处(即0x0050:0x0000))。
如果我们把第10行改成“org 0x0500”,那么就要在main函数中把DS设置为0,这样才能正确的打印出字符。