自己动手写操作系统 第四章总结
本章主要内容是建立一个功能比較完好的引导扇区也称为 boot sector
首先回想一下一个操作系统从开机到执行的过程
1.引导
2.载入内核入内存
3.跳入保护模式
4.開始运行内核
可见在内核開始运行前还有非常多工作要做,假设所有交给boot sector,512字节非常可能不够用,所以我们须要将这个
功能分担出来,这个模块就是loader
如今,boot sector 的功能就是负责把loader载入到内存中,而且把控制权交给它,到此,boot sector就完毕任务了
而载入内核的功能就交给了loader
如今再来看看执行过程
1.boot sector引导载入loader程序,并将控制权交给loader
2.loader把内核载入到内存中,并负责进入保护模式(要实现分页等功能),然后将控制权交给内核
3.内核进行接下来的操作
在第四章中我们基本的任务是写一个boot sector,让他来载入loader
同一时候学习软盘的fat12格式
先大体对程序流程有一个认识
1.首先我们须要一个写一个DOS能够识别的引导盘,注意引导扇区须要有BPB等头信息才干被微软DOS识别,所以加上它
然后通过jmp short LABEL_START:让我们開始执行引导扇区,这里有一个问题须要注意,为什么我们的引导扇区是写在floppy上的,而我们的计算机是怎么能执行它呢?这程序肯定是在内存中的,那么它是什么时候被载入到内存中的呢?
解答,在计算机加电后BIOS回查看软盘的第一个扇区的最后两个字节是不是0xaa55,假设是的话,就觉得这个扇区是可引导的,然后将这512字节的内容载入到内存中的07c00h中,然后開始执行,所以我们的boot sector是不须要我们手动进行载入到内存中的,可是我们的loader.bin和kernel却不行,他们仅仅能以文件形式存在软盘的数据区然后我们须要在boot sector中载入loader.bin到内存中,然后将控制权交给它,接着通过loader.bin将kernel载入到内存中,然后将控制权交给内核
2.将loader载入到内存中,这里我们须要用到BIOS的int 13h中断来读取软盘,也就是将软盘数据区的内容载入到内存中来,这个内存是预先设定好的,并要记住这个内存位置,由于当我们将loader从软盘数据区载入到内存中后我们须要jmp到这个位置以便将控制权交给它
这里我们将读取软盘的工作交给一个叫readsector的函数,它接受两个參数,扇区逻辑号,和要读取的扇区数目,要注意的问题是我们里给出的是扇区的逻辑号,可是用int 13h中断须要确切的物理位置,所以在函数中我们要进行这个转换才行
须要注意的地方就是怎样将逻辑扇区号转换成可供int 13h调用的物理软盘位置,以及int 13h的调用方法,以及loader将要存储的内存位置
3.当然,要读取loader的内容到内存中,首先最重要的就是在软盘中找到loader的位置才行,否则从哪里将数据转移到内存中,这里须要注意的地方有下面几点
1.怎样着手寻找?首先须要了解软盘的结构,包含,boot sector , fat1 表,fat2表,根文件夹,数据区
2.那么怎样下手呢?首先,我们要知道全部数据区的文件的文件名称都是在根文件夹的条目中,这个32位的条目中存放了关于这个文件的各种信息,详见p128,当中如今最重要的就是文件名称(条目中的前11个字节,8字节的文件名称,3字节的扩展名),以及接下来连续读取的时候须要用到的32字节中偏移0x1Ah处存放的两字节的这个文件的事实上簇号(这个簇号与扇区类似,可是又不是一样的,而是有一一相应的映射关系),如今在寻找loader的步骤中我们须要的就是文件名称,我们要遍历软盘的根文件夹区中的每个条目中的前11字节,看看是不是和我们预先设定号的loader的文件名称全然一致(比方loader.bin)
假设一致,那么记住这个根文件夹中的条目,由于接下来我们要用到这个条目中的之前说的偏移0x1Ah处的文件第一个簇号(通过这个簇号我们能够找到第一个扇区,由于他们是一一相应的),在这个遍历中我们须要三个嵌套循环,第一个是根文件夹占用的扇区总数,这里是19,所以要遍历19次,它的作用是每次循环将一个扇区取出来存到一个临时存放扇区的地方,然后以下的循环对这个扇区进行遍历操作,这个循环中嵌套一个循环,这个第二级循环的作用是遍历一个扇区中的每个条目(512/32次),接下来这个循环又嵌套一个循环,这个第三级循环的作用是遍历这一个条目的前11个字符,推断是不是和我们要找的文件名称全然一致,若一致,则跳出循环进行下一个操作,否则将一直遍历(最多19 * (512 / 32) * 11次),若没找到则提示找不到loader
3.若我们在第二步中找到了loader,我们就要对这个根文件夹条目进行操作了(32位),我们首先确定这个文件夹的起始位置,然后找到这个文件夹的偏移1Ah处,这里有最重要的簇号,通过它我们就能找到全部的loader占用的扇区,并将他们载入到一个连续的内存区域,载入完毕后就jmp到这个内存地址,然后就完毕了控制权从BIOS到loader的转移
4.如今我们找到了loader的起始簇号,那么怎样找到下一个簇号呢?这时候就要靠fat表了,fat表中存放着非常多12位的fatentry,在根文件夹条目中的偏移26字节的位置事实上存放的就是fatentry的编号,这个编号是从2開始的,0, 1两个是不用的,同一时候数据区的第0, 1两个扇区是不用的,所以我们说他们是一一相应的,通过在根文件夹中的关键条目我们得到了第一个数据扇区的簇号,也就是它在fat表中的第一个fatentry的编号,这个编号的位置存放的值是下一个数据簇号(同一时候也是下一个fatentry的编号),这种话我们就能够一直找,知道这个fatentry的值是FFFh的时候说明文件结束了,我们找到了全部的数据,假设在找的同一时候我们还将这个找到的扇区读取到那个连续的内存区域,那么找完的同一时候我们也就完毕了loader载入到内存中的任务了,注意这期间有两个比較难的地方,第一是通过fatentry的编号找到这个fatentry的位置,我们须要有奇偶两种情况,还有就是通过簇号找到其相应的逻辑扇区号(由于我们要通过逻辑扇区号来进行readsector将扇区内容载入到数据区中)
5.到了这里,我们就基本完毕了载入loader到内存中的步骤,接下来要做的就是通过jmp baseofloader:offsetofloader来完毕控制权的转移了
注意!本书的p130下方有关fatentry的图是错误的!这里害我花了非常长时间来考虑getfatentry这个函数
以下贴出我的带有完整凝视的代码,注意这代码是照着书上敲的,顺序不太一样,可能也会有点问题,可是关键的是有凝视,
;boot.asm,载入loader,方法,遍历软盘,知道找到文件名称loader.bin
%define _BOOT_DEBUG_
%ifdef _BOOT_DEBUG_
org 0100h
%else
org 07c00h ;告诉编译器程序将要载入到这里
%endif
%ifdef _BOOT_DEBUG_
BaseOfStack equ 0100h ;栈底,栈向低地址生长
%else
BaseOfStack equ 07c00h ;
%endif
;事实上从这里開始都是磁盘的头
jmp short LABEL_START
nop ;这个nop为什么不可缺少!!!!!!!!!!!!!!
;原因看书上p126上的引导扇区的格式,能够看到開始处必须是一个跳转指令
;jmp LABEL_START
;nop
;接下来是fat12磁盘的头,作用是在boot sector的開始出,注意这里的位置是哪里
BS_OEMName DB 'ForrestY' ;OEM String,产商名字
BPB_BytesPerSec DW 512 ; 每扇区的字节数
BPB_SecPerClus DB 1 ;
BPB_RsvdSecCnt DW 1 ;Boot记录多少扇区
BPB_NumFATs DB 2 ;共同拥有多少FAT表
BPB_RootEntCnt DW 224 ;根文件夹文件数最大值
BPB_TotSec16 DW 2880 ;逻辑扇区总数
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
BS_DrvNum DB 0
BS_Reserved1 DB 0
BS_BootSig DB 29h
BS_VolID DD 0
BS_VolLab DB 'Tinix0.01 '
BS_FileSysType DB 'Fat12 '
LABEL_START:
mov ax,cs ;
mov ds,ax ; 初始化数据段
mov es,ax ;初始化es 为这个段的事实上,由于用到int 10h中断须要es:bp作为串的偏移地址
mov ss, ax
mov sp, BaseOfStack
;清屏 int 10h
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 0184fh
int 10h
;因为使用了堆栈,所以这里要初始化堆栈
; call DispStr
; jmp $
;从这里開始是在A盘根文件夹寻找loader.bin的程序
xor ah, ah
xor dl, dl
int 13h
mov word [wSectorNo], SectorNoOfRootDirectory ;当中wsectorno是一个变量,当中存放的是根文件夹的第一个扇区号,这是一个常量19,同一时候RootDirSectors是根文件夹占用空间,这里是14,这些值都是開始定义了的
LABEL_SEARCH_IN_ROOT_DIR_BEGIN: ;要找一个文件名称就在根文件夹下寻找即可了,从这里開始就像do{}while開始循环了
cmp word [wRootDirSizeForLoop], 0 ;这里wrootdirsizeforloop是一个变量,初始值为上面那个14
jz LABEL_NO_LOADERBIN ;假设为零,那么就是到了循环结束还没找到文件,跳转到没找到那里,结束
dec word [wRootDirSizeForLoop] ;假设运行到了这里说明还没循环结束,那么就让循环索引减一
mov ax, BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader ;这里BaseOfLoader:OffsetOfLoader是调用readsector函数的时候的缓存取,也就是中断int 13h的缓存区es:bx,每个扇区的内容,都将临时保存到这里
mov ax, [wSectorNo]
mov cl, 1 ;这两个是函数readsector的參数,ax代表逻辑扇区号,cl代表要读的扇区数为1
mov si, LoaderFileName ; ds:si当中数据段ds被初始化为本程序開始处所以ds:si就是"Loader bin"的字符串位置
mov di, OffsetOfLoader ; es和ds都被初始化为了一样的,也就是程序段的開始处, 如今准备比較offsetofloader中的值了,可是这里baseofloader:offsetofloader是一整个扇区,所以等一下还须要提取出当中的文件名称才行,注意查看书128也的根文件夹条目结构Dir_Name的位置,事实上不用提取,由于偏移量为零,比較前11个字节旧能够了
cld
mov dx, 10h ;每一个扇区512字节,每一个根文件夹32字节,所以一共同拥有512/32=16个文件夹入口,这个dx是为接下来的循环做准备,要遍历整个扇区的根文件夹
LABEL_SEARCH_FOR_LOADERBIN:
cmp dx, 0
jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR
dec dx ;假设没有读完一个扇区就继续读下一个文件夹
mov cx, 11 ;规定文件名称在文件夹入口偏移处为0的前11个字节里
LABEL_CMP_FILENAME:
cmp cx, 0
jz LABEL_FILENAME_FOUND ;假设循环能运行11次,说明找到了文件名称,假设文件名称不匹配是运行不了11次的
dec cx
lodsb ;作用,把ds:si的值写到al中,接下来和es:di中的字符比較,这两个ds es分别存了待比較的字符串,这个指令会导致si自增或自减,可是不会改变di的值,所以要在LABEL_GO_ON中手动更改
cmp al, byte [es:di]
jz LABEL_GO_ON ;假设相等,继续比較
jmp LABEL_DIFFERENT; 假设字符不匹配,则跳出循环
LABEL_GO_ON:
inc di
jmp LABEL_CMP_FILENAME ;因为lodsb仅仅改动了si的值,所以我们须要手动改动di的值
LABEL_DIFFERENT:
and di, 0FFE0h
add di, 20h;这两步是为了让,di指向下一文件夹条目
mov si, LoaderFileName; 让si知会字符串文件名称開始处
jmp LABEL_SEARCH_FOR_LOADERBIN;假设字符布匹配,那么跳回到下个文件夹入口開始运行
LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:;假设一个扇区读完了,那么旧跳到下一个扇区去,知道循环完所有的14个扇区
add word [wSectorNo], 1
jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN
LABEL_NO_LOADERBIN:
mov dh, 2 ;dh是DispStr的參数,相当于字符串数组,用数字来索引字符串
Call DispStr
%ifdef _BOOT_DEBUG_
mov ax, 4c00h
int 21h
%else
jmp $
%endif
LABEL_FILENAME_FOUND:
mov ax, RootDirSectors ;在接下来的某行里实用,在deltasector那一行
and di, 0FFE0h ;di原来指向es:di中的文件名称字符串的结尾处,这里两个语句先是让di,指向第一个字符处,add 01ah后指向偏移26字节的地方,也就是fistcul第一个簇,在数据区中
add di, 01Ah
mov cx, word [es:di] ;把两字节的长度赋值到cx中,也就是loader.bin在数据区的第一个扇区,注意这个并非逻辑扇区,不过簇
push cx
add cx, ax
add cx, DeltaSectorNo ;deltasector是17用来计算该扇区的位置,如今cx中存的是逻辑扇区号了
mov ax, BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader ;es:bx如今是之前那个baseofloader:offsetofloader的位置了,用完了,能够覆盖掉了
mov ax, cx ;ax如今是第一个数据区的逻辑扇区号
LABEL_GOON_LOADING_FILE: ;有了loader.bin的第一个逻辑扇区号,如今能够開始载入进内存中了
push ax ;暂存ax,bx由于要调用int 10h须要这两个寄存器
push bx
mov ah, 0Eh
mov al, '.'
mov bl, 0Fh
int 10h
pop bx
pop ax ;一用完旧恢复这两个寄存器的值,如今ax中的是loader.bin的第一个扇区号
mov cl, 1 ;參数ax,是逻辑扇区号,cl是要读几个扇区
call ReadSector ;调用readsector来读取该扇区的内容,缓冲区为es:bx,刚刚已经初始化过了
;如今開始读取fatentry
pop ax
call GetFATEntry
cmp ax, 0FFFh ;假设是0fffh旧说明这是最后一簇了
jz LABEL_FILE_LOADED ;读取完就跳出循环
push ax ;还没有读取完loader.bin,这里保存是给下一轮循环的getfatentry作为參数的,这里保存了下一个entry的数值
mov dx, RootDirSectors ;同一时候这个entry不仅是下一个entry的位置,同一时候也是下一个数据所在的簇号(相应一个扇区,算法为逻辑扇区=簇号+ RootDirSectors + SectorNoofRootDirectory - 2),在这里也就是ax + 14 + 17,
add ax, dx
add ax, DeltaSectorNo ;到这里旧得到了数据区的扇区,供下一次的readsector来使用
add bx, [BPB_BytesPerSec] ;bx+512指向下一个空白的地方来存放下一个数据扇区,初始值是offsetofloader
jmp LABEL_GOON_LOADING_FILE
LABEL_FILE_LOADED: ;假设读取完了loader.bin,那么就让程序跳转到该内存进行指令的运行,如今就開始将控制权交给loader.bin运行了!注意loader.bin就是一个可运行的,里面都是可运行的,所以直接跳转到我们之前用readsector载入到的内存地方就能够了(这个内存是es:bx, 且通过add, [BPB_BytesPerSec], 一直在增长)
mov dh, 1
call DispStr
jmp BaseOfLoader:OffsetOfLoader ;这个内存如今放的就是loader.binn的程序,如今相当于在运行loader.bin中的代码了
;这个函数用来读取8+4=12结构的fatentry,但眼下给出的是簇数,这个fatentry的内容既是当下一个fatentry的位置,也是相应数据取数据的簇号,也就是第几个12结构,等下要依据奇偶性得出这个12位的入口,可是如今不知道,这个12位的东西也不过存了下一个loader.bin的文件的下一个扇区这个的信息是包括同一文件的下一个扇区的簇数,这个簇数还要计算才干得到逻辑扇区
GetFATEntry:
push es
push bx
push ax ;es:bx原来的值baseofloader:offsetofloader,ax原来的值是待读取的fatentry也就是8+4=12的那个值,存在fat表中的信息
mov ax, BaseOfLoader
sub ax, 0100h
mov es, ax ;在baseofloader后面留出4Kb的空间,用来存放fat?什么fat?fat表还是什么东西
pop ax ;这是ax中的还是待读取的fatentry项,的簇数不是12位的
mov byte [bOdd], 0
mov bx, 3
mul bx ;这以后ax的值变了,
mov bx, 2
div bx ;ax的值变成了该fatentry在fat中的偏移量
cmp dx, 0 ;这里dx是dx:ax * 3 /2 的余数,用来推断ax是奇数还是偶数
jz LABEL_EVEN
mov byte [bOdd], 1 ;推断完后ax的值还是没变的,如今知道了奇偶性,也知道了是第几个fatentry,能够開始找到这个fatentry了
LABEL_EVEN: ;假设是偶数个
xor dx, dx
mov bx, [BPB_BytesPerSec] ;每扇区字节数512
div bx ;ax是所在扇区号,dx是在该扇区的偏移量
push dx
mov bx, 0 ;es:bx = baseofloader -100: 00
add ax, SectorNoOfFat1 ;ax就是fatentry所在的扇区号了,sectorNooffat1是fat1表所在的第一个扇区号,也就是1
mov cl, 2 ;一次读取两个扇区,由于fat可能会占用两个扇区
call ReadSector
pop dx ;dx中存放的是在该扇区的偏移量
add bx, dx ;ex:bx就是该entry所在的偏移量
mov ax, [es:bx] ;把entry所在的字读取出来
cmp byte [bOdd], 1 ;依据奇偶进行fatentry内容的筛选
jnz LABEL_EVEN_2 ;假设是奇数旧跳转
shr ax, 4 ;假设是偶数,仅仅取前12位,注意书上的图p130是有错误的,所以获得entry的方法事实上不是非常难~
LABEL_EVEN_2: ;也就是说是奇数
and ax, 0FFFFh ;仅仅取后12位
LABEL_GET_FAT_ENTRY_OK:
pop bx
pop es
ret
;刚刚用到的常量
BaseOfLoader equ 09000h ;用int 13h得到的缓存区,用来暂存扇区
OffsetOfLoader equ 0100h
DeltaSectorNo equ 17 ;用来计算簇号和扇区的关系的
RootDirSectors equ 14 ;根文件夹占用扇区个数
SectorNoOfRootDirectory equ 19 ;根文件夹開始的扇区数
SectorNoOfFat1 equ 1
;变量
wRootDirSizeForLoop dw RootDirSectors ;控制循环次数的变量
wSectorNo dw 0 ;要读的扇区号
bOdd db 0 ;奇数还是偶数
;字符串
LoaderFileName db "LOADER BIN", 0 ;字符串以0结尾,假设要用,可是为什么要以零结尾呢?
MessageLength equ 9
BootMessage: db "Booting "
Message1 db "Ready. "
Message2 db "No LOADER"
DispStr:
mov ax, MessageLength
mul dh ;字符串长度是固定的,索引乘以长度就是字符串的开头
add ax, BootMessage ;Booting,也就是字符串数组的開始位置
mov bp, ax
mov ax, ds ;ds被初始化为本程序的開始处
mov es, ax ;这三个语句确定了串的地址,如今es是程序開始, bp是该字符串開始到该位置的偏移量 es:bp,就是串的地址
;从这里開始为中断int 10准备參数
mov cx, MessageLength ;cx是串的长度
mov ax,01301h
mov bx,0007ch
mov dl,0
int 10h
ret
;要载入loader进入内存必需要读取软盘,由于loader.bin是直接复制到软盘数据区的,所以如今写一个读取软盘的函数,这里仅仅是读软盘数据,依据逻辑扇区读取,等下要写一个寻找loader.bin文件位置的函数
ReadSector:
push bp
mov bp, sp
sub esp, 2
mov byte [bp - 2], cl
push bx
mov bl, [BPB_SecPerTrk]
div bl
inc ah
mov cl, ah
mov dh, al
shr al, 1
mov ch, al
and dh, 1
pop bx
mov dl, [BS_DrvNum]
.GoOnReading:
mov ah, 2
mov al, byte [bp - 2]
int 13h
jc .GoOnReading
add esp, 2
pop bp
ret
times 510-($-$$) db 0
dw 0xaa55