您的位置:首页 > 其它

如何编写自己的操作系统(2)

2011-11-26 23:01 330 查看
这一节我们详细介绍Boot4.asm这个汇编程序。

1、程序设定

;*********************************************
;Boot1.asm
;- A Simple Bootloader
;*********************************************
 org 0; Why 0x0? The original is 0x7c00 http://www.docin.com/p-13154518.html
 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、 开始执行

start:
 jmp main

第一行的start是汇编程序开始执行的地方,程序从这里开始执行。第2行表示跳转到main标记执行。

3、简单的FAT12文件系统

由于我们需要把文件存储在软盘上,所以需要在软盘的第一个扇区上写入一些信息,来表明如何对这个软盘进行的进行管理。就像我们有一个很大的空仓库,我们需要在里面弄出一些隔间,以便于我们管理这个仓库中存储的东西。这些信息就用来描述这个软盘上的文件系统。这些信息如下:

;*********************************************
;BIOS Parameter Block
;*********************************************
; BPB Begins 3 bytes from start. We do a far jump, which is 3 bytes in size.
; If you use a short jump, add a "nop" after it to offset the 3rd byte.
 bpbOEMdb "My OS "; OEM identifier (Cannot exceed 8 bytes!)
 bpbBytesPerSector:DW 512
 bpbSectorsPerCluster: DB 1
 bpbReservedSectors: DW 1
 bpbNumberOfFATs: DB 2
 bpbRootEntries: DW 224
 bpbTotalSectors: DW 2880
 bpbMedia: DB 0xf8;; 0xF1
 bpbSectorsPerFAT: DW 9
 bpbSectorsPerTrack: DW 18
 bpbHeadsPerCylinder: DW 2
 bpbHiddenSectors: DD 0
 bpbTotalSectorsBig: DD 0
 bsDriveNumber: DB 0
 bsUnused: DB 0
 bsExtBootSignature: DB 0x29
 bsSerialNumber:DD 0xa0a1a2a3; will be overwritten
 bsVolumeLabel: DB "MOS FLOPPY "
 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寄存器中。代码如下:

[code];***************************************
;Prints a string
;DS=>SI: 0 terminated string
;***************************************
Print:
 lodsb; load next byte from string from SI to AL
 oral, al; Does AL=0?
 jzPrintDone; Yep, null terminator found-bail out
 movah,0eh; Nope-Print the character
 int10h
 jmpPrint; Repeat until null terminator found
PrintDone:
 ret; we are done, so return
[/code]
第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接着从软盘中读取另一份文件(程序)加载到内存中,这个程序的大小就没有限制了,可以做更多的事情,设定计算机的环境,加载真正的操作系统。

从软盘中把一个程序加载到内存的代码如下所示:


[code];************************************************;
; Reads a series of sectors
; Input:
; CX=>Number of sectors to read
; AX=>Starting sector (logical block addressing)
; ES:BX=>Buffer to read to
; Changed:
; DI, SI, AX, CX, BX
;************************************************;
ReadSectors:
 .MAIN:
 mov di, 0x0005; five retries for error
 .SECTORLOOP:
 push ax
 push bx
 push cx
 call LBACHS; compute absoluteTrack, absoluteSector, absoluteHead
 mov ah, 0x02; BIOS read sector
 mov al, 0x01; read one sector
 mov ch, BYTE [absoluteTrack]
 mov cl, BYTE [absoluteSector]
 mov dh, BYTE [absoluteHead]
 mov dl, BYTE [bsDriveNumber]
 int 0x13; invoke BIOS
 jnc .SUCCESS; test for read error. CF=0 then jump
 xor ax, ax; BIOS reset disk
 int 0x13
 dec di
 pop cx
 pop bx
 pop ax
 jnz .SECTORLOOP
 int 0x18
 .SUCCESS:
 mov si, msgProgress
 call Print
 pop cx
 pop bx
 pop ax
 add bx, WORD [bpbBytesPerSector]; queue next buffer
 inc ax; queue next sector
 loop .MAIN; read next sector. Controlled by CX, If CX=0, then stop
 ret
[/code]
这里用到了中断指令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转换成软盘的逻辑扇区地址

代码如下:


[code];************************************************;
; Convert Cluster to LBA
; Input:
; AX=>the cluster to be changed
; Changed:
; AX, CX
; Return:
; AX=>sector number
; LBA = (cluster - 2) * sectors per cluster
;************************************************;
ClusterLBA:
 sub ax, 0x0002; zero base cluster number
 xor cx, cx
 mov cl, BYTE [bpbSectorsPerCluster]; convert byte to word
 mul cx
 add ax, WORD [datasector]; base data sector
 ret
[/code]
代码中的第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

其代码如下:


[code];************************************************;
; Convert LBA to CHS
; Input:
; AX=>LBA Address to convert
; Changed:
; DX, AX
; Return:
; BYTE [absoluteSector], BYTE [absoluteHead], BYTE [absoluteTrack]
;
; absolute sector = (logical sector % sectors per track) + 1
; absolute head = (logical sector / sectors per track) MOD number of heads
; absolute track= logical sector / (sectors per track * number of heads)
;
;************************************************;
LBACHS:
 xor dx, dx; prepare dx:ax for operation
 div WORD [bpbSectorsPerTrack]
 inc dl; adjust for sector 0
 mov BYTE [absoluteSector], dl
 xor dx, dx
 div WORD [bpbHeadsPerCylinder]
 mov BYTE [absoluteHead], dl
 mov BYTE [absoluteTrack], al
 ret
[/code]
第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入口


[code];*********************************************
;Bootloader Entry Point
;*********************************************
main:
 
;-----------------------------------------------------
; code located at 0000:7c00, adjust segment registers
;-----------------------------------------------------
 
 cli
 mov ax, 0x07c0; setup registers to point to our segment. s*16+off = address
 mov ds, ax
 mov es, ax
 mov fs, ax
 mov gs, ax
 
;-----------------------------------------------------
; create stack
;-----------------------------------------------------
 
 mov ax, 0x0000; set the stack
 mov ss, ax
 mov sp, 0xffff
 sti; restore interrupts
 
;-----------------------------------------------------
; display loading message
;-----------------------------------------------------
 
 mov si, msgLoading; "Loading Boot Image "
 call Print
[/code]
第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中存储的一些数值的意义如下:

Value marks free cluster : 0x00
Value marks Reserved cluster : 0x01
This cluster is in use--the value represents next cluster : 0x002 through 0xFEF
Reserved values : 0xFF0 through 0xFF6
Value marks bad clusters : 0xFF7
Value marks this cluster as the last in the file : 0xFF8 through 0xFFF
FAT12文件系统中一般有两个FAT表,第二个和第一个完全一样,一般用不到。

Root Directory也是一个表,这个表中的每个元素的大小为32bytes,每个元素的信息如下:

Bytes 0-7 : DOS File name (Padded with spaces)
Bytes 8-10 : DOS File extension (Padded with spaces)
Bytes 11 : File attributes. This is a bit pattern:

Bit 0 : Read Only
Bit 1 : Hidden
Bit 2 : System
Bit 3 : Volume Label
Bit 4 : This is a subdirectory
Bit 5 : Archive
Bit 6 : Device (Internal use)
Bit 6 : Unused

Bytes 12 : Unused
Bytes 13 : Create time in ms
Bytes 14-15 : Created time, using the following format:

Bit 0-4 : Seconds (0-29)
Bit 5-10 : Minutes (0-59)
Bit 11-15 : Hours (0-23)

Bytes 16-17 : Created year in the following format:

Bit 0-4 : Year (0=1980; 127=2107
Bit 5-8 : Month (1=January; 12=December)
Bit 9-15 : Hours (0-23)

Bytes 18-19 : Last access date (Uses same format as above)
Bytes 20-21 : EA Index (Used in OS/2 and NT, dont worry about it)
Bytes 22-23 : Last Modified time (See byte 14-15 for format)
Bytes 24-25 : Last modified date (See bytes 16-17 for format)
Bytes 26-27 : First Cluster
Bytes 28-31 : File Size
黑体标注的是比较重要的部分。注意bytes 0 – bytes 10是文件名,FAT12系统的文件名只能是11 bytes,不能多也不能少。最后几个字节指出了这个文件的第一个Cluster的位置,并且给出了这个文件的大小。

在多介绍一些cluster的事情。我们前面说过,软盘中一个扇区的大小只能是512B。如果一个文件大于这个数值,就要存储在多个扇区中,这样一些扇区的集合就是一个Cluster。在BPB(即第3部分的文件系统信息)中指定了每个Cluster使用几个扇区。

要想把一个文件从软盘中加载到内存,首先需要知道这个文件的存储位置。由于软盘中的所有文件信息都存储在Root Directory这个表中,所以我们首先要把这个表读取出来。代码如下:


[code];-----------------------------------------------------
; load root directory table
;-----------------------------------------------------
 
 LOAD_ROOT:
 
; compute size of root directory and store in "cx"
 
 xor cx, cx
 xor dx, dx
 mov ax, 0x0020; 32 bytes directory entry
 mul WORD [bpbRootEntries]; total size of directory. bpbTotalSectors = 2880
 div WORD [bpbBytesPerSector]; sectors used by directory. ax is the consult
 xchg ax, cx; now cx is the result, ax is 0x0000
 
; compute location of root directory and store in "ax"
 
 mov al, BYTE [bpbNumberOfFATs]
 mul WORD [bpbSectorsPerFAT]
 add ax, WORD[bpbReservedSectors]
 mov WORD [datasector], ax; base of root directory
 add WORD [datasector], cx; ?
 
; read root directory into memory (7c00:0200)
 
 mov bx, 0x0200
 call ReadSectors
[/code]
第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来找到我们要从软盘中读取的文件。代码如下:


[code];------------------------------------------------
; Find stage 2
;------------------------------------------------
; browse root directory for binary image
 
 mov cx, WORD [bpbRootEntries]
 mov di, 0x0200
 
.LOOP:
 push cx
 mov cx, 0x000b; eleven character name
 mov si, ImageName; image name to find
 push di
 rep cmpsb; test for entry match
 pop di
 je LOAD_FAT; if found, "DI" is the pointer to ImageName in the Root Directory
 pop cx
 add di, 0x0020; queue next directory entry. Each entry in Root Directory is 32 bytes (0x20)
 loop .LOOP; cx = bpbRootEntries, check "cx" times.
 jmp FAILURE
[/code]
第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加载到内存中,来查找这个表确定我们所要加载的文件究竟在何处。代码如下:


[code];----------------------------------------------
; load FAT
;----------------------------------------------
LOAD_FAT:
; save starting cluster of boot image
 mov si, msgCRLF
 call Print
 mov dx, WORD [di + 0x001a]; di contains starting address of entry. Just refrence byte 26 (0x1A) of entry
 mov WORD [cluster], dx; file's first cluster
 
; compute size of FAT and store in "cx"
 xor ax, ax
 mov al, BYTE [bpbNumberOfFATs]
 mul WORD [bpbSectorsPerFAT]
 mov cx, ax
 
; compute location of FAT and store in "ax"
 mov ax, WORD [bpbReservedSectors]; adjust for bootsector
 
; read FAT into memory (07c0:0200)
 mov bx, 0x0200
 call ReadSectors
[/code]
根据第9小节的表,我们知道bytes 26 - 27是这个文件的第一个cluster的编号。现在我们先把这个内容提取出来。第11、12两行代码完成这个功能。最后这个信息放到了“cluster”这个变量中。

剩下的内容和加载Root Directory Table的时候差不多,就不再介绍了。

最后把FAT读入到内存的0x07c0:0x0200处,把刚才的Root Directory Table覆盖了。

12、把软盘中的文件加载到内存

现在我们把软盘中的ImageName所指示的文件加载到内存中。代码如下:


[code]; read image file into memory (0050:0000)
 mov si, msgCRLF
 call Print
 mov ax, 0x0050
 mov es, ax
 mov bx, 0x0000
 push bx
 
;----------------------------------------------
; load stage 2
;----------------------------------------------
LOAD_IMAGE:
 
 mov ax, WORD [cluster]; cluster to read. File's first cluster
 pop bx; buffer to read into. ES:BX. es=0x0050
 call ClusterLBA; convert cluster to LBA
 xor cx, cx
 mov cl, BYTE [bpbSectorsPerCluster]
 call ReadSectors
 push bx; next buffer to read to
 
; compute next cluster
 mov ax, WORD [cluster]; identify current cluster
 mov cx, ax; copy current cluster
 mov dx, ax
 shr dx, 0x0001; divide by two
 add cx, dx; sum for (3/2)
 mov bx, 0x0200; location of FAT in memory
 add bx, cx; index into FAT
 mov dx, WORD [bx]; read two bytes from FAT
 test ax, 0x0001
 jnz .ODD_CLUSTER
 
.EVEN_CLUSTER:
 and dx, 0000111111111111b; take low twelve bits
 jmp .DONE
 
.ODD_CLUSTER:
 shr dx, 0x0004; take high twelve bits
 
.DONE:
 mov WORD [cluster], dx; store new cluster
 cmp dx, 0x0ff0; test for end of file
 jb LOAD_IMAGE
[/code]
到现在为止,内存中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处,现在我们要跳转到这个地址开始执行这里的代码。这个程序如下:

[code] DONE:
 mov si, msgCRLF
 call Print
 push WORD 0x0050
 push WORD 0x0000
 retf; jmp to 0x0050:0000 to excute
[/code]
第5、6两行先把两个地址压入到栈中。

第7行的RETF是一个长跳转指令,它从栈中弹出两个元素,依次放入到IP和CS中。这样我们使用CS:IP进行寻址的时候就跳转到了0x0050:0x0000处。

有关ImageName所指示的文件的代码我们以后再介绍。

14、错误处理

代码如下:

[code] FAILURE:
 mov si, msgFailure
 call Print
 mov ah, 0x00
 int 0x16; a wait keypress
 int 0x19; warm boot computer
[/code]
在第11小节用到了这个错误处理。

15、数据定义

我们前面用到了一些msgFailure、cluster等数据,都在这里定义。它们仅仅是一个地址,存储了一些东西。代码如下:

[code] absoluteSector db 0x00
absoluteHead db 0x00
absoluteTrack db 0x00
datasector dw 0x0000
cluster dw 0x0000
 ImageName db "KRNLDRSYS"
msgLoading db 0x0d, 0x0a, "Loading Boot Image ", 0x0d, 0x0a, 0x00
msgCRLF db 0x0d, 0x0a, 0x00
msgProgress db ".", 0x00
msgFailure db 0x0d, 0x0a, "ERROR : Press Any Key to Reboot", 0x0a, 0x00
[/code]
第1 - 6行的数据在程序运行时都改变了它们的值。后面的数据的值在程序运行时没有发生改变。

16、补足512 Bytes

对于我们这个文件,Boot4.asm,它需要被编译成一个大小恰好为512B的文件,放到软盘的第一个扇区上,当BIOS启动时就可以检测到这段代码并且把这个代码加载到内存的0x7c00处。所以,我们的代码要保证编译之后的文件(Boot4.bin)大小恰好为512B。

并且,这个文件的最后两个字节一定要是0xaa55,这样,BIOS才能识别出这个程序是一个可以启动的程序。

代码如下:


[code] TIMES 510-($-$$) db 0; confirm the compiled bin file is 512B
 dw 0xaa55; the bootable special character
[/code]
第1行的times指令是复制某个东西多少次。times之后紧跟的参数是复制的次数。我们的程序编译好之后要求为512B,除去最后两个字节的特殊标记,还剩下510 B。$ 表示当前指令所在的地址。$$ 表示程序的起始地址。第1行的指令表示向后填充那么多个0 Byte的意思。

好了,到现在为止,我们的Boot4.asm总算介绍完了。后面我们会再介绍ImageName所指示的那个文件是如何编写的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: