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

      这一节我们详细介绍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   "

      这里我们需要简单了解一些软盘的物理结构。

image   

      如上图所示,一个软盘可能有多个盘片,每个盘片可能上下两面都能存储信息,这样一个盘片就对应着两个读取头(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
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:
  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
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的关系。

image

      我们可以想象把软盘的所有扇区放到一个长长的带子上,第一个扇区的标号为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文件系统在软盘上的结构:

image

      第一个扇区就是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这个表中,所以我们首先要把这个表读取出来。代码如下:

   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位。原因请看下图:

image

      假定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所指示的那个文件是如何编写的。

原文地址:https://www.cnblogs.com/wangshuo/p/2264701.html