这一节我们详细介绍Boot4.asm这个汇编程序。
1、程序设定
1: ;*********************************************
2: ; Boot1.asm
3: ; - A Simple Bootloader
4: ;*********************************************
5:
6: org 0 ; Why 0x0? The original is 0x7c00 http://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:
5: ; BPB Begins 3 bytes from start. We do a far jump, which is 3 bytes in size.
6: ; If you use a short jump, add a "nop" after it to offset the 3rd byte.
7:
8: bpbOEM db "My OS " ; 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
25: bsVolumeLabel: DB "MOS FLOPPY "
26: bsFileSystem: DB "FAT12 "
这里我们需要简单了解一些软盘的物理结构。
如上图所示,一个软盘可能有多个盘片,每个盘片可能上下两面都能存储信息,这样一个盘片就对应着两个读取头(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
11: 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 |
有了这些参数,在看上面的程序就非常简单了。我们首先把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:
13: mov di, 0x0005 ; five retries 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]
25: int 0x13 ; invoke BIOS
26: jnc .SUCCESS ; test for read error. CF=0 then jump
27: xor ax, ax ; BIOS reset disk
28: int 0x13
29: dec di
30: pop cx
31: pop bx
32: pop ax
33: jnz .SECTORLOOP
34: 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 Returns: |
INT 0x13/AH=0x02 - DISK : READ SECTOR(S) INTO MEMORY Returns: |
第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:
17: xor dx, dx ; prepare dx:ax for operation
18: div WORD [bpbSectorsPerTrack]
19: inc dl ; adjust 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: ;-----------------------------------------------------
28: ; display loading message
29: ;-----------------------------------------------------
30:
31: mov si, msgLoading ; "Loading Boot Image "
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:
7: ; compute size of root directory and store in "cx"
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:
16: ; compute location of root directory and store in "ax"
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:
5: ; browse root directory 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
15: rep cmpsb ; test for entry match
16: pop di
17: je LOAD_FAT ; if found, "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)
20: loop .LOOP ; cx = bpbRootEntries, check "cx" 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:
14: ; compute size of FAT and store in "cx"
15:
16: xor ax, ax
17: mov al, BYTE [bpbNumberOfFATs]
18: mul WORD [bpbSectorsPerFAT]
19: mov cx, ax
20:
21: ; compute location of FAT and store in "ax"
22:
23: mov ax, WORD [bpbReservedSectors] ; adjust 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
30: add cx, dx ; sum 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:
48: mov WORD [cluster], dx ; store new cluster
49: cmp dx, 0x0ff0 ; test 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
6: int 0x16 ; a wait keypress
7: 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
7: ImageName db "KRNLDR SYS"
8: msgLoading db 0x0d, 0x0a, "Loading Boot Image ", 0x0d, 0x0a, 0x00
9: msgCRLF db 0x0d, 0x0a, 0x00
10: msgProgress db ".", 0x00
11: msgFailure db 0x0d, 0x0a, "ERROR : Press Any Key to Reboot", 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所指示的那个文件是如何编写的。