2012年3月23日 星期五

Simple OS for Flppy Disk

開機磁區如圖:

所以我們可以用NASM編譯出一個boot.img放到磁區中,記得0x01fe~0x0200要填入0xaa55標記為開機磁區,bios先把0x0000~0x0200的資料填入0x7c00~0x7dfe,檢查0x7dfe是否為0xaa55,若是,則到0x7c00執行開機程序

依此,我們可以寫一段範例如下

;%define _BOOT_DEBUG_ ; 設為DEBUG的話輸出成COM檔
%ifdef _BOOT_DEBUG_
 org  0100h    
%else
 org  07c00h   
%endif
;================================================================================================
%ifdef _BOOT_DEBUG_
BaseOfStack  equ 0100h ; DEBUG下stack位置為0x100
%else
BaseOfStack  equ 07c00h ; Boot下stack位置為0x7c00
%endif

BaseOfLoader  equ 09000h ; 設定LOADER.BIN被載到的段位地址
OffsetOfLoader  equ 0100h ; 設定LOADER.BIN被載到的偏移地址

RootDirSectors  equ 14 ; 根目錄佔用磁區數 RootDirSectors = ((BPB_RootEntCnt * 32) + (BPB_BytesPerSec - 1)) / BPB_BytsPerSec
SectorNoOfRootDirectory equ 19 ; Root Directory的第一個磁區號碼FirstRootDirSecNum = BPB_RsvdSecCnt + (BPB_NumFATs * BPB_FATSz16) => (有2個FAT表1個佔9所以從19開始)
SectorNoOfFAT1  equ 1 ; FAT1 的第一个磁區號碼 = BPB_RsvdSecCnt
DeltaSectorNo  equ 17 ; DeltaSectorNo = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) - 2
; 文件的開始Sector號碼 = DirEntry中的開始Sector号 + 根目錄佔用磁區數 + DeltaSectorNo
;================================================================================================
;磁片1.44MB=2(磁頭)x80(磁道)x18(磁區)x512 bytes(磁區的大小)=2880x512 bytes = 1440 KB
 jmp short LABEL_START  ; Start to boot.
 nop    ; 必加nop
 ; 下面是 FAT12宣告
 BS_OEMName DB ' NEO OS ' ; OEM String, 佔8個字節
 BPB_BytsPerSec DW 512  ; 每磁區字節數
 BPB_SecPerClus DB 1  ; 每簇多少磁區
 BPB_RsvdSecCnt DW 1  ; Boot記錄佔用多少扇區
 BPB_NumFATs DB 2   ; 共有幾張FAT表
 BPB_RootEntCnt DW 224  ; 根目錄文件最大值
 BPB_TotSec16 DW 2880  ; 邏輯磁區總數(2x80x18)=(磁頭數x磁道數x磁區數)=磁區號碼
 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 'neo boot1.0'; 標籤,必須11個字節
 BS_FileSysType DB 'FAT12   ' ; 文件系統類型必須8個字節   
;============================================================================
;變量
;----------------------------------------------------------------------------
wRootDirSizeForLoop dw RootDirSectors ; Root Directory 要讀的磁區數,在循環中會減少至0.
wSectorNo  dw 0  ; 要讀的磁區號碼
bOdd   db 0  ; 奇數或偶數

;============================================================================
;字符串
;----------------------------------------------------------------------------
LoaderFileName  db "LOADER  BIN",0 ; LOADER.BIN 之文件名
; 为简化代码, 下面每个字符串的长度均为 MessageLength
BootMessage:  db "Find->",0; 13游標移到開頭 10游標往下移一行
FinishLoaderMessage  db 13,10,"Loading...",0; 
NoLoaderMessage  db 13,10,"No LOADER",0; 
;============================================================================
LABEL_START: 
 mov ax, cs
 mov ds, ax
 mov es, ax
 mov ss, ax
 mov sp, BaseOfStack  

 ; 清除畫面
 mov ax, 0002h
 int 10h
 mov ax, 0600h  ; AH = 06,  AL = 00h
 mov bx, 0700h  ; 黑底白字(BL = 07h)
 mov cx, 0   ; 左上角: (0, 0)
 mov dx, 0184fh  ; 右下角: (80, 50)
 int 10h   ; int 10h 

 mov si,BootMessage    ; "Find->..."
 call Print 
 mov si,LoaderFileName   ; "LOADER.BIN"
 call Print 
 
 xor ah, ah ; 
 xor dl, dl ; 軟碟復位
 int 13h ; ┛
 
; 捜尋LOADER.BIN
 mov word [wSectorNo], SectorNoOfRootDirectory     ;wSectorNo填入19表示根目錄從第19磁區開始
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
 cmp word [wRootDirSizeForLoop], 0 ;wRootDirSizeForLoop為剩下多少根目錄磁區未讀
 jz LABEL_NO_LOADERBIN  ; 判斷根目錄磁區是否完全讀完
 dec word [wRootDirSizeForLoop] ; wRootDirSizeForLoop-1
 mov ax, BaseOfLoader            ;09000h
 mov es, ax   ; es <- BaseOfLoader
 mov bx, OffsetOfLoader ; bx <- OffsetOfLoader 於是, es:bx = BaseOfLoader:OffsetOfLoader(9000h:0100h)
 mov ax, [wSectorNo] ; 把ax指向某個根目錄區磁區,ReadSector會用到
 mov cl, 1   ;cl=1一次讀一個磁區
 call ReadSector  ;把磁區號瑪為[wSectorNo]的磁區讀到es:bx

 mov si, LoaderFileName ; ds:si -> "LOADER  BIN"
 mov di, OffsetOfLoader ; es:di -> BaseOfLoader:0100 = BaseOfLoader*10h+100
 cld                     ; clears the direction flag
 mov dx, 10h                                     ;根目錄文件最大值224根目錄佔14個磁區所以每個磁區16個檔案名
LABEL_SEARCH_FOR_LOADERBIN:
 cmp dx, 0          
 jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR ; dx=0表示讀完這磁區換下個磁區
 dec dx           
 mov cx, 11          ;"LOADER  BIN"共11個字節
LABEL_CMP_FILENAME:
 cmp cx, 0
 jz LABEL_FILENAME_FOUND ; 如果11個字符都相等表示找到
dec cx
 lodsb      ; ds:si -> al 即"LOADER  BIN"字串地址放入al
 cmp al, byte [es:di]  ;[es:di]為ReadSector的檔案名
 jz LABEL_GO_ON
 jmp LABEL_DIFFERENT   ;不一樣就跳出 
LABEL_GO_ON:
 inc di
 jmp LABEL_CMP_FILENAME ; 

LABEL_DIFFERENT:
;根目錄的格式.共32bytes= 20h
;名稱        |偏移(bytes)| 長度(bytes)|        描述           |舉例(loader.bin)|
;DIR_Name    |     0     |     0xb    |檔案名8byte,擴展名3byte| "LOADER  BIN"  |
;DIR_Attr    |    0xb    |     1      |     檔案屬性          |       0x20     |
;保留位      |    0xc    |    10      |       保留位          |       0        |
;DIR_WrtTime |    0x16   |     2      |     最後一次寫入時間  |     0x7a5a     |
;DIR_WrtDate |    0x18   |     2      | 最後一次寫入日期      |     0x3188     |
;DIR_FstClus |    0x1a   |     2      | 此目錄項的開始簇編號  |       0x0002   |
;DIR_FileSize|    0x1c   |     4      |       檔案大小        |     0x000000f  |
 and di, 0FFE0h      ; di -> 當前目錄項的開始
 add di, 20h       ; 每個檔案資訊佔32bytes=20h
 mov si, LoaderFileName    ;
 jmp LABEL_SEARCH_FOR_LOADERBIN;   

LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
 add word [wSectorNo], 1    ;word [wSectorNo]號碼+1指向root下一個磁區
 jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN

LABEL_NO_LOADERBIN:
 mov si,NoLoaderMessage  ; "No LOADER."
 call Print  ; 显示字符串
%ifdef _BOOT_DEBUG_
 mov ax, 4c00h  ; 
 int 21h    ;DEBUG下没有找到 LOADER.BIN, 回到 DOS
%else
 jmp $    ;root下没有找到 LOADER.BIN, 死循環
%endif

LABEL_FILENAME_FOUND:   ; 找到 LOADER.BIN 
 mov ax, RootDirSectors  ;根目錄佔用磁區數
 and di, 0FFE0h  ; di -> 當前目錄項的開始
 add di, 01Ah  ; di -> 此目錄項的開始簇編號
 mov cx, word [es:di] ;word [es:di]指向LOADER.BIN的開始簇編號
 push cx   ; 保存此Sector在FAT中的序號
 ;文件的開始Sector號碼 = DirEntry中的開始Sector號 + 根目錄佔用磁區數 + DeltaSectorNo
 add cx, ax              ; cx = DirEntry中的開始Sector号 + 根目錄佔用磁區數
 add cx, DeltaSectorNo ; cx = ... + DeltaSectorNo = 文件的開始Sector號碼
 mov ax, BaseOfLoader 
 mov es, ax    ;es <- BaseOfLoader=9000h
 mov bx, OffsetOfLoader ; bx <- OffsetOfLoader = 0100h
 mov ax, cx   ; ax <- 文件的開始Sector號碼

LABEL_GOON_LOADING_FILE:
 push ax       ;  |
 push bx       ;  |
 mov ah, 0Eh   ;  | 
 mov al, '.'   ;  | 每讀一個磁區打一個點
 mov bl, 0Fh   ;  | Booting ......
 int 10h       ;  |
 pop bx        ;  |
 pop ax        ;  |

 mov cl, 1
 call ReadSector   ;把ax=磁區號瑪的磁區讀到es:bx     AX是文件的開始Sector號碼
 pop ax   ; 取出此 Sector 在 FAT 中的序號     AX是DirEntry中的開始Sector號
 call GetFATEntry
 cmp ax, 0FFFh
 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 si,FinishLoaderMessage    ; "Ready."
 call Print   ; 印出字串

; *****************************************************************************************************
 jmp BaseOfLoader:OffsetOfLoader ; 跳轉到LOADER.BIN的開始處
; *****************************************************************************************************
;----------------------------------------------------------------------------
; 函數名: Print
;----------------------------------------------------------------------------
; 作用:
;  將DS:SI地址字串印出
Print:

lodsb            ;從SI地址處加載一個字節到al
or al,al          ;檢查要顯示的串是否結束,如果結束,si給加載0×0進入al
jz pe            ;判斷是否相等即zf=0,則結束,跳至pe結束輸出
mov ah,0x0e           ;10號中斷初始化,功能0EH  在Teletype模式下顯示字符
mov bx,0x007        ;顯示方式
int 0x10          ;打開中斷
jmp Print          ;中斷響應,跳至print輸出
pe:
ret
;----------------------------------------------------------------------------
; 函數名: ReadSector
;----------------------------------------------------------------------------
; 作用:
;      從第 ax 個 Sector 開始, 將 cl 個 Sector 讀入 es:bx 中
ReadSector:
 ; -----------------------------------------------------------------------
 ; 2880個磁區號碼如何轉成磁盤位置(磁區號碼=(磁道號,起始磁區號,磁頭號))
 ; -----------------------------------------------------------------------
 ; 設磁區號碼為x
 ;                            ┌ 磁道號 = y >> 1 其實是(y/磁頭數)剛好磁頭數=2
 ;       x           ┌ 商  y ┤
 ; -------------- => ┤       └ 磁頭號 = y & 1
 ; 每磁道磁區數量    │
 ;                   └ 餘數 z => 起始磁區號 = z + 1
 push bp
 mov bp, sp
 sub esp, 2   ; 劈出兩個字節的stack區放要讀磁區數: byte [bp-2]

 mov byte [bp-2], cl ;byte [bp-2] 放 1 表一次一個磁區
 push bx  
 mov bl, [BPB_SecPerTrk] ; bl為除數即每磁道磁區數量
 div bl   ; ax/bl後y在al中, z在ah中
 inc ah   ; z++ = 起始磁區號
 mov cl, ah   ; cl <- 起始磁區號
 mov dh, al   ; dh <- y
 shr al, 1   ; y >> 1 
 mov ch, al   ; ch <- 磁道號
 and dh, 1   ; dh & 1 = 磁頭號
 pop bx   
 mov dl, [BS_DrvNum]  ; 驅動裝置號 (0 表示 軟碟)
.GoOnReading:
 mov ah, 2   
 mov al, byte [bp-2]  ; byte [bp-2]=1放入al表一次讀一個磁區
 int 13h
 jc .GoOnReading  ; 如果讀取錯誤CF->1跳回GoOnReading

 add esp, 2    ;esp加回2,stack區還原
 pop bp

 ret

;----------------------------------------------------------------------------
; 函数名: GetFATEntry
;----------------------------------------------------------------------------
; 作用:
; 找到序號为 ax 的 Sector 在 FAT 中的項目, 结果放在 ax 中
; 需要注意的是, 中間需要讀 FAT 的磁區到 es:bx 處, 所以函數一開始保存了 es 和 bx
GetFATEntry:
 push es
 push bx
 push ax
 mov ax, BaseOfLoader  ;9000h
 sub ax, 0100h ;  在 BaseOfLoader 後面留出 4K 空間用來存放 FAT
 mov es, ax  ;  es<-8F00h
 pop ax
 mov byte [bOdd], 0
 mov bx, 3  
 mul bx   ; dx:ax = ax * 3
 mov bx, 2
 div bx   ; dx:ax / 2  ==>  ax <- 商, dx <- 餘數
 cmp dx, 0
 jz LABEL_EVEN
 mov byte [bOdd], 1
LABEL_EVEN:;偶數
 ; 現在 ax 中是 FATEntry 在 FAT 中的偏移量,下面來
 ; 計算 FATEntry 在哪個磁區中(FAT佔用不止一個磁區)
 xor dx, dx   
 mov bx, [BPB_BytsPerSec]
 div bx ; dx:ax / BPB_BytsPerSec
     ;  ax <- 商 (FATEntry 所在的磁區相對於 FAT 的磁區號)
     ;  dx <- 餘數 (FATEntry 在磁區内的偏移)。
 push dx
 mov bx, 0 ; bx <- 0 於是, es:bx = (BaseOfLoader - 100):00
 add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是 FATEntry 所在的磁區號
 mov cl, 2
 call ReadSector ; 讀取 FATEntry 所在的磁區, 一次讀兩個, 避免在邊界出錯, 因為 FATEntry 可能跨越兩個磁區
 pop dx
 add bx, dx
 mov ax, [es:bx]
 cmp byte [bOdd], 1
 jnz LABEL_EVEN_2
 shr ax, 4
LABEL_EVEN_2:
 and ax, 0FFFh
LABEL_GET_FAT_ENRY_OK:

 pop bx
 pop es
 ret
;----------------------------------------------------------------------------
times  510-($-$$) db 0 ; 填滿填到510
dw  0xaa55    ; 開機標記
times  1474560-($-$$) db 0
找到LOADER.BIN時會載入到0x9000:0x0100 此時再跳轉到0x9000:0x0100 LOADER.BIN範例如下
org 0x100
mov ax,cs ;設置數據段
mov ds,ax
mov es,ax
jmp start ;跳到開始處執行
kelstr db 13,10,'Now Its in kernel',13,10,0 ;歡迎字符串定義
chbuf db '',0,0 ;臨時緩衝區定義,用來裝載輸入字符
;--------------------------------------------------------------
;將SI位置的字元印出,BL為顏色值(背景+前景)h
print:
lodsb            ;從SI地址處加載一個字節到al
cmp al,10
jz print10
cmp al,13
jz print13
or al,al          ;檢查要顯示的串是否結束,如果結束,si給加載0×0進入al
jz pe            ;判斷是否相等即zf=0,則結束,跳至pe結束輸出
mov ah,0x09      ;10號中斷初始化,功能0EH  在Teletype模式下顯示字符
mov bh,0         ;顯示方式
mov cx,1
int 0x10          ;打開中斷
mov ah,03
int 0x10
inc dl
mov ah,02
mov bh,0
int 0x10
jmp print          ;中斷響應,跳至print輸出
print10:
mov ah,03
int 0x10
mov ah,02
mov bh,0
mov dl,0
int 0x10
jmp print  
print13:
mov ah,03
int 0x10
mov ah,02
mov bh,0
inc dh
int 0x10
jmp print  
pe:
ret
;--------------------------------------------------------------------
echo: ;回顯功能塊
mov bl,0xb ;顏色
mov ah,0 ;准備接受鍵盤字符,存入到al中
int 0x16 ;開16號鍵盤中斷
cmp al,13
jz press13
or al,al          ;檢查要顯示的串是否結束,如果結束,si給加載0×0進入al
mov ah,0x09      ;10號中斷初始化,功能0EH  在Teletype模式下顯示字符
mov bh,0         ;顯示方式
mov cx,1
int 0x10          ;打開中斷
mov ah,03
int 0x10
inc dl
mov ah,02
mov bh,0
int 0x10
jmp echo          ;無窮迴圈
press13:
mov ah,03
int 0x10
mov ah,02
mov bh,0
inc dh
mov dl,0
int 0x10
jmp echo 
;--------------------------------------------------------------------
start:
mov si,kelstr
mov bl,0xf
call print
call echo

沒有留言:

張貼留言