zoukankan      html  css  js  c++  java
  • 如何编写自己的操作系统(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所指示的那个文件是如何编写的。

  • 相关阅读:
    Cannot find a free socket for the debugger
    如何让myeclipse左边选中文件后自动关联右边树
    myeclipse反编译安装 jd-gui.exe下载
    MyEclipse报错Access restriction: The type BASE64Encoder is not accessible due to restriction on required library
    如何使用JAVA请求HTTPS
    如何使用JAVA请求HTTP
    SVN提交代码时报405 Method Not Allowed
    对称加密和非对称加密
    ORA-12505, TNS:listener does not currently know of SID given in connect descriptor
    调整Linux操作系统时区-centos7
  • 原文地址:https://www.cnblogs.com/wangshuo/p/2264701.html
Copyright © 2011-2022 走看看