1、准备工作
一台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
好了,我们直接进入启动程序。整个程序如下:
;********************************************* ; Boot1.asm ; - A Simple Bootloader ;********************************************* org 0 bits 16 start: jmp main ;********************************************* ; 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. bpbOEM db "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 " ;********************************************* ; Prints a string ; DS=>SI: 0 terminated string ; Changed Register ; AX, SI ;********************************************* Print: lodsb or al, al jz PrintDone mov ah, 0eh int 10h jmp Print PrintDone: ret ;************************************************; ; 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 ;************************************************; ; Convert CHS 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 ;************************************************; ; 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 ;********************************************* ; 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 ;----------------------------------------------------- ; 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 ;------------------------------------------------ ; 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 ;---------------------------------------------- ; 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 (7c00:0200) mov bx, 0x0200 call ReadSectors ; 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 ; 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 DONE: mov si, msgCRLF call Print push WORD 0x0050 push WORD 0x0000 retf ; jmp to 0x0050:0000 to excute (MAY BE) FAILURE: mov si, msgFailure call Print mov ah, 0x00 int 0x16 ; a wait keypress int 0x19 ; warm boot computer absoluteSector db 0x00 absoluteHead db 0x00 absoluteTrack db 0x00 datasector dw 0x0000 cluster dw 0x0000 ImageName db "KRNLDR SYS" 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 TIMES 510-($-$$) db 0 ; confirm the compiled bin file is 512B dw 0xaa55 ; the bootable special character
这个程序经过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...”这个字符串
; Note: Here, we are executed like a normal COM program, but we are still in ; Ring 0. We will use this loader to set up 32 bit mode and basic exception ; handling ; This loaded program will be our 32 bit kernal. ; We do not have the limitation of 512 bytes here, so we can add anything we ; want here! org 0x0 ; offset to 0, we will set segments later bits 16 ; we are still in real mode ; we are loaded at linear address 0x10000 jmp main ;********************************* ; Prints a String ; DS=>SI: 0 terminated string ;********************************* Print: lodsb or al, al jz PrintDone mov ah, 0eh int 10h jmp Print PrintDone: ret ;******************************** ; Second Stage Loader Entry Point ;******************************** main: cli push cs pop ds mov si, Msg call Print cli hlt ;******************************** ; Data section ;******************************** Msg db "Preparing to load operating system...",13,10,0
之所以把这个文件叫做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,里面的内容为:
# ROM and VGA BIOS images --------------------------------------------- romimage: file=$BXSHARE/BIOS-bochs-latest vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest # boot from floppy using our disk image ------------------------------- floppya: 1_44=a:, status=inserted # Boot from drive A # logging and reporting ----------------------------------------------- #log: OSDev.log # All errors and info logs will output to OSDev.log #error: action=report #info: action=report
保存好后,运行这个文件,就可以看到模拟器启动了。最后稳定之后的界面应该是这个样子的:
好了,到现在为止,我们的操作系统就已经运行完成了,打印出了一个字符串。如果你忘了把KRNLDR.SYS文件拷贝到A盘,它还会提示你出错。
按一下这个界面上面的Power键,就可以结束这次模拟了。
好了,有关代码的具体介绍,我们后面再进行。
这一节我们详细介绍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 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的关系。
我们可以想象把软盘的所有扇区放到一个长长的带子上,第一个扇区的标号为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所指示的那个文件是如何编写的。
这里我们简单介绍一下Stage2.asm这个程序。
整个程序代码如下:
1: ; Note: Here, we are executed like a normal COM program, but we are still in
2: ; Ring 0. We will use 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:
7: ; We 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
27: 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: ;********************************
50: ; Data section
51: ;********************************
52:
53: Msg db "Preparing to load operating system...",13,10,0
这个程序非常简单,我们就不全部介绍了。只介绍第10行的这一句话。
由于我们在调用Print这个函数的时候,会使用DS:SI 来进行寻址,第10行把整个程序的偏移地址设为0,而在第38、39行重新设定了DS,所以不会产生问题(注意,Stage2.asm编译之后的二进制文件被加载到内存的0x0500处(即0x0050:0x0000))。
如果我们把第10行改成“org 0x0500”,那么就要在main函数中把DS设置为0,这样才能正确的打印出字符。