一、代码
启动的过程中,引导扇区boot sector(boot.asm,07c00h开始,最多512bytes)负责把加载器Loader(loader.asm,长度不受限制)载入内存并且把控制权交给她。Loader再加载操作系统内核之前,还要作准备保护模式等一系列工作,就很可能超过512bytes了。
loader.asm代码在这里只是虚晃一枪,其具体功能编写放在“第五章 内核雏形”,不再赘述。
boot.asm中灰色部分为代码“主线”,基本思路为:
(1)LABEL_SEARCH_IN_ROOT_DIR_BEGIN~~~LABEL_FILENAME_FOUND:在FAT12文件系统的“根目录区”中找到"loader.bin"对应的条目。
执行后[es:di]指向该条目内部的一个字节,而非条目的开始,根据P105“根目录分区的条目格式”可知,[es:di]指向条目中的位置如下图所示:
[DIR_NAME] [DIR_ATTR] [保 留] [DIR_WrtTime] [DIR_WrtDate] [DIR_FstClus] [DIR_FileSize]
长度(单位:byte): 8+3 1 10 2 2 2 4
LOADER_ _BIN * ********** ** ** ** ****
|
di: 是字符'N'后面的那个字节
(2)LABEL_FILENAME_FOUND子程序:后面ReadSector中会用到int 13中断读磁盘,故在此准备3个寄存器内容——
ax <-- (全局)扇区号
es <-- BaseOfLoader (09000h)
bx <-- OffsetOfLoader (0100h)
在ReadSector中,会将(全局)扇区号ax 转换成 “柱面号ch”,“磁头号(即盘面号)dh”,“起始扇区号cl”,从而定位到磁盘上的一个扇区。另外,在ReadSector中另行指定“cl为待读扇区数”。int 13h的2号功能(ah=2)会将磁盘上的这若干个扇区读到[es:bx]开始的内存中。
(3)LABEL_GOON_LOADING_FILE~~~jmp BaseOfLoader:OffsetOfLoader之前:注意到根据“根目录区条目”只能找到“loader.bin”在磁盘上的“第一个扇区”,因此本部分在FAT表中顺藤摸瓜地找到loader.bin在磁盘上的“后续所有扇区号(这里也是簇号)”,并通过call ReadSector载入内存。
(4)jmp BaseOfLoader:OffsetOfLoader这条指令:(前面已经将磁盘中若干个扇区上的loader.bin文件载入BaseOfLoader:OffsetOfLoader开始的连续内存中了,这里)跳转到loader.bin,将控制权交给loader.bin。在下一章中将分析loader完成的功能——加载OS内核+跳入保护模式。
loader.asm
org 0100h mov ax, 0B800h mov gs, ax mov ah, 0Fh ; 0000: 黑底 1111: 白字 mov al, 'L' mov [gs:((80 * 0 + 39) * 2)], ax ; 屏幕第 0 行, 第 39 列。 jmp $ ; Start
boot.asm
;%define_BOOT_DEBUG_; 做 Boot Sector 时一定将此行注释掉!将此行打开后用 nasm Boot.asm -o Boot.com 做成一个.COM文件易于调试
%ifdef_BOOT_DEBUG_
org 0100h; 调试状态, 做成 .COM 文件, 可调试
%else
org 07c00h; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00 处并开始执行
%endif
;================================================================================================
%ifdef_BOOT_DEBUG_
BaseOfStackequ0100h; 调试状态下堆栈基地址(栈底, 从这个位置向低地址生长)
%else
BaseOfStackequ07c00h; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)
%endif
BaseOfLoaderequ09000h; LOADER.BIN 被加载到的位置 ---- 段地址
OffsetOfLoaderequ0100h; LOADER.BIN 被加载到的位置 ---- 偏移地址
RootDirSectorsequ14; 根目录占用空间
SectorNoOfRootDirectoryequ19; Root Directory 的第一个扇区号
SectorNoOfFAT1equ1; FAT1 的第一个扇区号 = BPB_RsvdSecCnt
DeltaSectorNoequ17; DeltaSectorNo = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) - 2
; 文件的开始Sector号 = DirEntry中的开始Sector号 + 根目录占用Sector数目 + DeltaSectorNo
;================================================================================================
jmp short LABEL_START; Start to boot.
nop; 这个 nop 不可少 ???1为何
; 下面是 FAT12 磁盘的头
BS_OEMNameDB 'ForrestY'; OEM String, 必须 8 个字节
BPB_BytsPerSecDW 512; 每扇区字节数
BPB_SecPerClusDB 1; 每簇多少扇区
BPB_RsvdSecCntDW 1; Boot 记录占用多少扇区
BPB_NumFATsDB 2; 共有多少 FAT 表
BPB_RootEntCntDW 224; 根目录文件数最大值( 14扇区 * 512字节/扇区 / 32字节/根目录区条目 )
BPB_TotSec16DW 2880; 逻辑扇区总数 (2880扇区 * 512字节/扇区 = 1.4M 字节 = 一个软盘的容量)
BPB_Media DB 0xF0; 媒体描述符
BPB_FATSz16 DW 9; 每FAT扇区数
BPB_SecPerTrk DW 18; 每磁道扇区数
BPB_NumHeads DW 2; 磁头数(面数)
BPB_HiddSec DD 0; 隐藏扇区数
BPB_TotSec32 DD 0; 如果 wTotalSectorCount 是 0 由这个值记录扇区数
BS_DrvNum DB 0; 中断 13 的驱动器号
BS_Reserved1 DB 0; 未使用
BS_BootSig DB 29h; 扩展引导标记 (29h)
BS_VolID DD 0; 卷序列号
BS_VolLab DB 'OrangeS0.02'; 卷标, 必须 11 个字节
BS_FileSysType DB 'FAT12 '; 文件系统类型, 必须 8个字节
LABEL_START:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
; 清屏
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 0184fh
int 10h
; 上面清屏用到了BIOS中断 int 10h
; int 10h是BIOS对屏幕及显示器提供的服务程序,
; 功能号AH=6:屏幕初始化或上卷,
; 参数AL=0:屏幕为空白,
; 背景和前景颜色BH = Background Color and Foreground color.
; 矩形区域:左上角(行ch,列cl)=(0,0), 右下角(行dh,列dl)=(0x18,0x4f)=(24, 79)
mov dh, 0 ; "Booting "
call DispStr ; 显示字符串
; int 13h is a shorthand for BIOS interrupt call 13h, the 20th interrupt vector in an x86-based computer system. The BIOS typically sets up a real mode interrupt handler at this vector that provides sector-based hard disk and floppy disk read and write services using cylinder-head-sector(CHS) addressing.
; Drive Table:
; DL=00h, 1st floppy disk ("drive A:")
; DL=01h, 2st floppy disk ("drive B:")
; DL=80h, 1st hard disk
; DL=80h, 2st hard disk
; Function Table:
; AH=00h, Reset Disk Drives
; AH=01h, Get Status of Last Drive Operation
; AH=02h, Read Sectors From Drive
; AH=03h, Write Sectors To Drive
; Parameters ...
; An interrupt handler, also known as an interrupt service routine (ISR), is a callback subroutine in microcontroller firmware, operating system or device driver whose execution is triggered by the reception of an interrupt.
xor ah, ah; ┓
xor dl, dl; ┣ 软驱复位
int 13h; ┛
; 下面在 A 盘的根目录寻找 LOADER.BIN
mov word [wSectorNo], SectorNoOfRootDirectory;根目录分区的第一个扇区号SectorNoOfRootDirectory=19
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
cmp word [wRootDirSizeForLoop], 0 ; ┓;根目录分区占用的扇区数wRootDirSizeForLoop
jz LABEL_NO_LOADERBIN ; ┣ 判断根目录区是不是已经读完
dec word [wRootDirSizeForLoop] ; ┛ 如果读完表示没有找到 LOADER.BIN
; 从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
; call ReadSector后,[BaseOfLoader:OffsetOfLoader] (即[es:bx]),开始存放根目录分区的一个扇区的数据
mov ax, BaseOfLoader
mov es, ax ; es <- BaseOfLoader
mov bx, OffsetOfLoader ; bx <- OffsetOfLoader于是, es:bx = BaseOfLoader:OffsetOfLoader
mov ax, [wSectorNo] ; ax <- Root Directory 中的某 Sector 号
mov cl, 1
call ReadSector
; 比较[ds:si] (即"LOADER BIN")和 [es:di](即根目录区中每个条目中 文件的名称)
mov si, LoaderFileName; ds:si -> "LOADER BIN"
mov di, OffsetOfLoader; es:di -> BaseOfLoader:0100 = BaseOfLoader*10h+100
cld
mov dx, 10h ; 一个扇区大小/根目录区一个条目大小=512byte/32byte=10h 个 ==> 一个扇区有多少条目
LABEL_SEARCH_FOR_LOADERBIN:
cmp dx, 0 ; ┓循环次数控制,
jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR ; ┣如果已经读完了一个 Sector,
dec dx ; ┛就跳到下一个 Sector
; 下面(粗体斜体部分)实际上是在检查“一个扇区”中的“一个根目录区条目”中是否有loader.bin这个名称出现
; ∴在这里有两个循环,外层循环挨个读取根目录区的每个扇区,内层循环在指定扇区中挨个读取每个条目。
mov cx, 11
LABEL_CMP_FILENAME:
cmp cx, 0
jz LABEL_FILENAME_FOUND; 如果比较了 11 个字符都相等, 表示找到
dec cx
lodsb ; ds:si -> al
cmp al, byte [es:di]
jz LABEL_GO_ON
jmp LABEL_DIFFERENT; 只要发现不一样的字符就表明本 DirectoryEntry 不是
; 我们要找的 LOADER.BIN
LABEL_GO_ON:
inc di
jmp LABEL_CMP_FILENAME;继续循环
LABEL_DIFFERENT: ; 一个根目录区中的条目长20h (bytes),详见P105
and di, 0FFE0h ; else ┓di &= E0 为了让它指向本条目开头
add di, 20h ; ┃
mov si, LoaderFileName ; ┣ di += 20h 下一个目录条目
jmp LABEL_SEARCH_FOR_LOADERBIN ; ┛
LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
addword [wSectorNo], 1
jmpLABEL_SEARCH_IN_ROOT_DIR_BEGIN
LABEL_NO_LOADERBIN:
mov dh, 2 ; "No LOADER."
call DispStr ; 显示字符串
; 由于上面有这样一句:“jz LABEL_FILENAME_FOUND”,所以程序不会执行如下的预编译,而直接跳到LABEL_FILENAME_FOUND去 !
%ifdef _BOOT_DEBUG_
mov ax, 4c00h ; ┓
int 21h ; ┛没有找到 LOADER.BIN, 回到 DOS
%else
jmp $ ; 没有找到 LOADER.BIN, 死循环在这里
%endif
; 在找到文件的这一刻[es : di] 指向根目录区中相应条目的DIR_NAME中'N'字母后的那个字符,注意到一个“根目录区的条目”的结构如下:(总共长度为0x20h 即32个字节)
; 名称 偏移
; DIR_NAME 0x0
; DIR_ATTR 0xB
; 保留位 0xC
; DIR_WrtTime 0x16
; DIR_WrtDate 0x18
; DIR_FstClus 0x1A
; DIR_FileSize 0x1C
LABEL_FILENAME_FOUND: ; 找到 LOADER.BIN 后便来到这里继续
mov ax, RootDirSectors
and di, 0FFE0h ; di -> 当前条目的开始
add di, 01Ah ; di -> 首 Sector
; 此时 [es:di] 指向根目录区条目中LOADER.BIN对应的那个条目的开头(一个条目32字节长度)
mov cx, word [es:di]
push cx ; 保存此 Sector 在 FAT 中的序号
add cx, ax
add cx, DeltaSectorNo ; cl <- LOADER.BIN的起始扇区号(0-based)
; 此时的cx = word [es:di] + RootDirSectors + DeltaSectorNo
; word [es:di] ==> LOADER.BIN这个文件在FAT12格式数据区中对应的簇号/扇区号
; RootDirSectors ==> 14, 即根目录区占据的扇区数
; DeltaSectorNo ==> 19-2, 其中19是根目录区之前的扇区数
mov ax, BaseOfLoader
mov es, ax ; es <- BaseOfLoader
mov bx, OffsetOfLoader ; bx <- OffsetOfLoader
mov ax, cx ; ax <- Sector 号
LABEL_GOON_LOADING_FILE:
push ax ; `.
push bx ; |
mov ah, 0Eh ; | 每读一个扇区就在 "Booting " 后面
mov al, '.' ; | 打一个点, 形成这样的效果:
mov bl, 0Fh ; | Booting ......
int 10h; |
pop bx ; |
pop ax ; /
mov cl, 1
call ReadSector ; 从第 ax(相对整个磁盘起始的扇区号) 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
; 此时,ax为当前在FAT中的扇区号/簇号
pop ax ; 取出此 Sector 在 FAT 中的序号
call GetFATEntry
cmp ax, 0FFFh
; 此时,ax为下一个要读扇区在FAT中的扇区号/簇号
; 下面需要将“FAT中的扇区号/簇号“ax 转换为“在整个磁盘中的扇区号”ax
jz LABEL_FILE_LOADED
push ax; 保存 Sector 在 FAT 中的序号
mov dx, RootDirSectors
add ax, dx
add ax, DeltaSectorNo
add bx, [BPB_BytsPerSec]
jmp LABEL_GOON_LOADING_FILE
LABEL_FILE_LOADED:
mov dh, 1; "Ready."
call DispStr; 显示字符串
; *****************************************************************************************************
; 这一句正式跳转到已加载到内存中的 LOADER.BIN 的开始处,
; 开始执行 LOADER.BIN 的代码。
; Boot Sector 的使命到此结束
jmpBaseOfLoader:OffsetOfLoader
; *****************************************************************************************************
;============================================================================
;变量
;----------------------------------------------------------------------------
wRootDirSizeForLoopdwRootDirSectors; Root Directory 占用的扇区数, 在循环中会递减至零.
wSectorNodw0; 要读取的扇区号
bOdddb0; 奇数还是偶数
;============================================================================
;字符串
;----------------------------------------------------------------------------
LoaderFileNamedb"LOADER BIN", 0; LOADER.BIN 之文件名
; 为简化代码, 下面每个字符串的长度均为 MessageLength
MessageLengthequ9
BootMessage:db"Booting "; 9字节, 不够则用空格补齐. 序号 0
Message1db"Ready. "; 9字节, 不够则用空格补齐. 序号 1
Message2db"No LOADER"; 9字节, 不够则用空格补齐. 序号 2
;============================================================================
;----------------------------------------------------------------------------
; 函数名: DispStr
;----------------------------------------------------------------------------
; 作用:
;显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
DispStr:
movax, MessageLength
muldh
addax, BootMessage
movbp, ax; ┓
movax, ds; ┣ ES:BP = 串地址
moves, ax; ┛
movcx, MessageLength; CX = 串长度
movax, 01301h; AH = 13, AL = 01h
movbx, 0007h; 页号为0(BH = 0) 黑底白字(BL = 07h)
movdl, 0
int10h; int 10h
ret
;----------------------------------------------------------------------------
; 函数名: ReadSector
;----------------------------------------------------------------------------
; 作用:
;从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
ReadSector:
; -----------------------------------------------------------------------
; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
; -----------------------------------------------------------------------
; 设扇区号为 x
; ┌ 柱面号 = y >> 1
; x ┌ 商 y ┤
; -------------- => ┤ └ 磁头号 = y & 1
; 每磁道扇区数 │
; └ 余 z => 起始扇区号 = z + 1
pushbp
movbp, sp
subesp, 2; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]
movbyte [bp-2], cl
pushbx; 保存 bx
movbl, [BPB_SecPerTrk]; bl: 除数
divbl; y 在 al 中, z 在 ah 中
incah; z ++
movcl, ah; cl <- 起始扇区号
movdh, al; dh <- y
shral, 1; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
movch, al; ch <- 柱面号
anddh, 1; dh & 1 = 磁头号
popbx; 恢复 bx
; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^
movdl, [BS_DrvNum]; 驱动器号 (0 表示 A 盘)
.GoOnReading:
movah, 2; 读
moval, byte [bp-2]; 读 al 个扇区
int13h
jc.GoOnReading; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止
addesp, 2
popbp
ret
;----------------------------------------------------------------------------
; 函数名: GetFATEntry
;----------------------------------------------------------------------------
; 作用:
;找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
;需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
GetFATEntry:
pushes
pushbx
pushax
movax, BaseOfLoader; `.
subax, 0100h; | 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT
moves, ax; /
popax
movbyte [bOdd], 0
movbx, 3
mulbx; dx:ax = ax * 3
movbx, 2
divbx; dx:ax / 2 ==> ax <- 商, dx <- 余数
cmpdx, 0
jzLABEL_EVEN
movbyte [bOdd], 1
LABEL_EVEN:;偶数
; 现在 ax 中是 FATEntry 在 FAT 中的偏移量,下面来
; 计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
xordx, dx
movbx, [BPB_BytsPerSec]
divbx ; dx:ax / BPB_BytsPerSec
; ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号)
; dx <- 余数 (FATEntry 在扇区内的偏移)
pushdx
movbx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00
addax, SectorNoOfFAT1 ; 此句之后的 ax 就是 FATEntry 所在的扇区号
movcl, 2
callReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界
; 发生错误, 因为一个 FATEntry 可能跨越两个扇区
popdx
addbx, dx
movax, [es:bx]
cmpbyte [bOdd], 1
jnzLABEL_EVEN_2
shrax, 4
LABEL_EVEN_2:
andax, 0FFFh
LABEL_GET_FAT_ENRY_OK:
popbx
popes
ret
;----------------------------------------------------------------------------
times 510-($-$$)db0; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw 0xaa55; 结束标志
二、运行方法
方法一:Linux上装bochs虚拟机,然后在bochs虚拟机中运行(由于有时候bochs很难在Linux上安装成功,因此推荐用方法二)
1. 编译boot.asm和loader.asm
[hadoop@sam1 c]$ pwd
/home/hadoop/Desktop/OSImpl/一个操作系统的实现/chapter4/c
[hadoop@sam1 c]$ ls
a.img bochsrc boot.asm loader.asm Makefile
[hadoop@sam1 c]$ nasm boot.asm -o boot.bin
[hadoop@sam1 c]$ nasm loader.asm -o loader.bin
2. 创建a.img软盘镜像,并将boot.asm写入软盘第一个扇区作为“引导扇区”:
[hadoop@sam1 bochs-2.6]$ bximage
...
Do you want to create a floppy disk image or a hard disk image?
Please type hd or fd. [hd] fd
...
Please type 0.16, 0.18, 0.32, 0.36, 0.72, 1.2, 1.44, 1.68, 1.72, or 2.88.
[1.44]
...
What should I name the image?
[a.img]
...
[hadoop@sam1 bochs-2.6]$ dd if=/home/hadoop/Desktop/OSImpl/一个操作系统的实现/chapter4/c/boot.bin of=/home/hadoop/Desktop/OSImpl/bochs-2.6/a.img bs=512 count=1 conv=notrunc
注:必须要设置从软盘a.img启动,因此修改$bochs_home/.bochsrc为如下:
megs:32
romimage:file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage:file=/usr/share/vgabios/VGABIOS-lgpl-latest.bin
#floppya: 1_44=freedos.img, status=inserted
#floppyb: 1_44=pm.img,status=inserted
floppya: 1_44=a.img, status=inserted
boot: a
#disable the mouse
#enable key mapping, using US layout as default.
keyboard_mapping: enabled=1, map=gui/keymaps/x11-pc-us.map
mouse: enabled=0
log:bochsout.txt
3. 把loader.bin作为一个普通的文件写入软盘a.img(当然也可以是其他盘)
[hadoop@sam1 bochs-2.6]$ sudo mount -o loop /home/hadoop/Desktop/OSImpl/bochs-2.6/a.img /mnt/floppy/
[sudo] password for hadoop:
[hadoop@sam1 bochs-2.6]$ sudo cp /home/hadoop/Desktop/OSImpl/一个操作系统的实现/chapter4/c/loader.bin /mnt/floppy -v
`/home/hadoop/Desktop/OSImpl/一个操作系统的实现/chapter4/c/loader.bin' -> `/mnt/floppy/loader.bin'
[hadoop@sam1 bochs-2.6]$ sudo umount /mnt/floppy/
注:mount -o loop ——
-o: option
loop: loop device. loop device is a pseudo('fake') device (actually just a file) that acts as a block-based device.
(1)loop device 区别于loopback device. Sometimes, the loop device if erroneously referred to as 'loopback' device, but this term is reserved for a networking device in the Linux kernel (cf. loopback). The concept of the 'loop' device is distinct from 'loopback', although similar in name.
(2)为什么命名为loop呢:在使用之前,一个loop设备必须要和一个文件进行连接,这种结合方式给用户提供了一个代替块特殊文件的接口。如果一个文件包含一个完整的文件系统,那么这个文件就可以像一个磁盘设备一样被mount起来。
对于第一层文件系统,它直接安装(环绕)在我们的计算机物理设备上;而对于这种被mount起来的镜像文件(它也包含有文件系统),则建立在第一层文件系统上,就好像是在第一层文件系统上再环绕一层文件系统似的,所以称为loop.
4. 开始模拟
[hadoop@sam1 bochs-2.6]$ ./bochs
结果截图:
方法二:直接在VMware添加一块硬盘,将boot.bin和loader.bin写到这块盘中,然后从这块盘启动
1. 在VMWare中添加一块硬盘后,给他分区、格式化为FAT12***添加过程如图(注意:新添加的硬盘大小最大为32MB(FAT12格式限制)
我在添加新硬盘时,虚拟机是启动着的,需要
#reboot
#fdisk -l 看到新硬盘没有分区的提示:Disk /dev/sdb doesn't contain a valid partition table
#mkfs.vfat -I -F 12 /dev/sdb 不分区(-I)直接把整块硬盘格式化为FAT12格式
2. 将boot.bin和loader.bin写到这块盘中
#cd /home/hadoop/osimpl/OSImplCode/chapter4/c/
#dd if=boot.bin of=/dev/sdb bs=512 count=1
#cp loader.bin /dev/sdb
3. 然后从从新添加的硬盘/dev/sdb启动
#reboot
//重启时马上按F2,进入BIOS设置界面