天天看點

仿照着寫個bootloader (三)

    到第三篇了,對應<x86>第8章的内容。這次分兩個代碼:一個是加載器,複位後首先得到執行的程式,從磁盤讀取使用者程式,并為使用者程式的執行初始化環境,最後跳轉到使用者程式;另一個是使用者程式,無所事事的運作着,僅僅是證明加載器沒出錯。

    兩個程式最重要部分是使用者程式的程式頭,程式頭就像是一份協定,使用者程式告訴加載器關于整個程式的屬性,加載器獲得這些屬性後,在指定記憶體位置加載。

    首先,加載器在指定扇區讀到使用者程式頭,頭部最開始部分是長度,告訴加載器要讀多少扇區哪些内容是有效的(uboot還在頭部加上魔術字,确定存儲的使用者程式是否有效(其他一些可執行程式的頭部也是魔術字,這不簡單起見,去了魔術字的驗證)。

    其次,是程式入口點和代碼段位址,以及後面的重定位表。除了程式入口點是相對與代碼段開始處的偏移,不需要重定位,其他段都需要重定位。為什麼需要重定位?因為程式被加載的位置不确定。假設,程式編寫時,從0x0000開始,資料段在整個程式開始位置,此時,ds段被寫死指向0x0000(這種說法可能不确切,其實形如 mov ax,0x0000 mov ds,ax),以後的資料通路都是從0x0000開始,如mov ax,[0];當程式被加載到0x10000段時,此時資料段其實已經被加載到0x10000,如果ds的段值沒有進行相應的修改,執行mov ax,[0]時,将獲得錯誤的資料。是以,對所有段經行重定位很重要,當然,也可以寫出與位置無關的代碼,不過過程挺麻煩的。重定位的過程大緻是:1)加載器在記憶體中尋找一片空閑的位址,計算出這片位址的基址(想法是美好的,但是現實不是這樣,加載器從LoadPhyBase指定的位置加載);2)找到空閑記憶體後,加載器周遊并修改使用者程式段表(段表存放了編譯時,使用者程式各個段的基址,當加載器加載後,需要将各個段的基址+LoadPhyBase,重新得到基址);3)當使用者程式得到執行後,不要急着運作自己的代碼邏輯,而應該先擷取使用者頭中各個段的重定位資訊,并修改相應的段寄存器,最後再開始執行。

    最後,所有的準備工作完成後,需要遠跳轉跳轉到使用者程式中執行。為嘛是遠跳轉?加載器所在的段這裡是0x0000,而使用者程式所在的段難以保證也在0x0000,intel 手冊上說,對于不同段之間的跳轉,需要借助遠跳轉。跳轉的目标位址在程式頭中指定,處理器将低16位元組加載到ip,高16位元組加載到cs。是以程式頭的結構被設計為

CodeEntry     dw start

CodeSeg     dd section.CodeSeg.start

以滿足這個要求。(這個是調試出來的,開始時,我寫成段位址在低位,入口偏移在高位,跳轉到未知世界)

貼代碼

加載器部分:

DiskDataReg   equ 0x01f0
DiskErrReg    equ 0x01f1
DiskSectCntReg  equ 0x01f2
DiskLoLBAAddr equ 0x01f3
DiskMeLBAAddr equ 0x01f4
DiskHiLBAAddr equ 0x01f5
DiskModReg    equ 0x01f6
DiskCmdStatReg  equ 0x01f7

DiskReadCmd   equ 0x20
DiskWriteCmd  equ 0x30

Arg1Off   equ 0x06 ;第一個參數相對bp偏移
Arg2Off   equ 0x08 ;第二個參數相對bp偏移
Arg3Off   equ 0x0A ;第三個參數相對bp偏移
Arg4Off   equ 0x0C ;第四個參數相對bp偏移

StackBase equ 0x0000
StackEnd  equ 0x2000
DataBase  equ 0x0300
DataEnd   equ 0x1FFF

AppEntryOff   equ 0x04
AppCodeSegOff equ 0x06
AppSegNumsOff equ 0x0A
AppRelocTab   equ 0x0C
;==========================
ProgLocSect equ 0x04

section bootloader align=16 vstart=0x7c00
jmp start

Relocation:
  ;前面取出沒重定位前儲存在使用者程式中的位址,加上重定位的基址,得到最終的位址
  ;實模式下,位址20位。ax取低16位,dx取高16位,其中低4位有效
  ;20位右移4位,得到段位址。ax先右移4位,空出高位4位;
  ;dx低4位左移12位,即15-12位為段位址高位,再與ax的低12位相或,得到16為段位址
  add ax,word [cs:LoadPhyBase]
  adc dx,word [cs:LoadPhyBase+2]
  shr ax,0x04
  shl dx,0x0c
  or ax,dx
  ret

SetSectAddr:
  ;設定邏輯扇區的位址
  push bp
  mov bp,sp
  push bx
  ;發扇區總數
  mov dx,DiskSectCntReg
  mov al,byte [bp+Arg4Off]
  out dx,al
  ;發扇區位址
  mov dx,DiskLoLBAAddr
  mov al,byte [bp+Arg3Off]
  out dx,al
  
  mov dx,DiskMeLBAAddr
  mov al,byte [bp+Arg3Off+1]
  out dx,al
  
  mov dx,DiskHiLBAAddr
  mov al,byte [bp+Arg2Off]
  out dx,al
  
  mov dx,DiskModReg
  mov al,byte [bp+Arg2Off+1]
  out dx,al
  
  pop bx
  mov sp,bp
  pop bp
  
  ret

WaitDiskReady:
  ;等待磁盤就緒
  mov dx,DiskCmdStatReg
.waits:
  in al,dx
  and al,0x88
  cmp al,0x08
  jnz .waits
  
  ;判斷是否有錯誤
  mov dx,DiskErrReg
.getErr:
  in al,dx
  cmp al,0x00
  ;al不為0出錯
  jnz .resume
  ;沒有出錯 傳回0
  xor ax,ax
  ret
  
.resume:
  ;出錯 傳回
  mov ax,0x01
  ret
  
ReadFromDisk:

  call SetSectAddr
  
  ;發讀指令
  mov dx,DiskCmdStatReg
  mov al,DiskReadCmd
  out dx,al
  
  call WaitDiskReady
  
  xor bx,bx ;準備拷貝,bx做索引
  ;讀取儲存在扇區上的資料的有效長度
  mov dx,DiskDataReg
  in ax,dx
  mov [bx],ax ;程式的長度也要儲存起來,要不然使用者頭部都不完整了
  add bx,2
  
  shr ax,0x01
  ;比較要讀取資料的長度和有效資料的長度,取最短的
  ;磁盤上儲存的資料,長度按位元組計數,但是DiskDataReg
  ;端口是16位端口,每次讀取1字,是以循環次數需要除2
  mov cx,ax
  mov dx,DiskDataReg
.readw:
  in ax,dx
  mov [bx],ax
  add bx,2
  loop .readw
  
  ret
  
start:
xor ax,ax
xor dx,dx
;計算用于加載程式的記憶體段位址
mov ax,word [cs:LoadPhyBase]
mov dx,word[cs:LoadPhyBase+2]
mov bx,0x10
div bx
;dx:ax/bx->商存放在ax中
;商即為段位址
;為後面使用者程式設定段寄存器(es/ds),指向LoadPhyBase所指的記憶體起址
;如果沒有設定ds,使用者程式運作時,程式不知道到何處去取出段頭資訊,程式大小
mov ds,ax
mov es,ax
push word 0x0001
push word ProgLocSect
push word 0xe000
push word 0x0200
;從4号扇區先讀一個扇區,扇區裡前部是程式頭,感覺跟pe檔案頭有點像
call ReadFromDisk
;恢複堆棧
add sp,0x08
;從0x1000:0000開始讀
xor bx,bx
mov ax,word [bx]
mov dx,word [bx+0x02]
;從使用者程式的總長度決定還有多少扇區要讀取
mov bx,0x200
div bx
;ax:程式占了完整的幾個扇區,dx:還多餘幾個位元組
;多餘的位元組占用一個扇區,故,共占用ax+1個扇區
;前面已經讀取了一個扇區,剩下ax個扇區還要讀取

cmp ax,0x0000
jz ReadSectComplete
;循環讀取次數
mov cx,ax
xor di,di
mov di,ProgLocSect
;段位址每次增加0x200,存儲扇區内容
mov ax,ds
RemainSector:
  add ax,0x20
  mov ds,ax
  ;繼續讀剩下的扇區
  push word 0x0001
  push word ProgLocSect
  push word 0xe000
  push word 0x0200
  call ReadFromDisk
  add sp,0x08
loop RemainSector
;現在程式全在從LoadPhyBase開始的記憶體中,由ds指向
ReadSectComplete:
;使用者程式重定位
RelocationCodeSeg:
  mov ax,word [AppCodeSegOff]
  mov dx,word [AppCodeSegOff+2]
  call Relocation
  ;修正使用者頭中代碼段基址,先清空以前的内容
  mov word [AppCodeSegOff],0x0000
  mov word [AppCodeSegOff+2],0x0000
  mov word [AppCodeSegOff],ax
RelocationTabElem:
  ;修正使用者頭中重定位表中各項基址,先清空以前的内容
  mov cx,word [AppSegNumsOff]
  ;重定位表偏移
  xor bx,bx
  mov bx,AppRelocTab
  RelocRound:
  mov ax,word [bx] ;表項低位
  mov dx,word [bx+0x02] ;表項高位
  call Relocation
  mov word [bx],0x0000
  mov word [bx+0x02],0x0000
  mov word [bx],ax
  add bx,0x04
  loop RelocRound
  ;重定位結束,跳轉到使用者程式
  ;代碼在[AppEntryOff]開始的位置
  ;開始時沒加far,結果是近跳轉,過不去
  ;遠跳轉,從記憶體中取出雙字,修改cs:ip的值
  jmp far [AppEntryOff]
LoadPhyBase dd 0x10000
times 510-($-$$) db 0
db 0x55,0xaa      
section ProgHead align=16 vstart=0
ProgSize  dd ProgEnd
CodeEntry   dw start
CodeSeg   dd section.CodeSeg.start
ProgSegNums dw (ProgSegEntryTabEnd-ProgSegEntryTab)/4
ProgSegEntryTab:
CodeSegEntry dd section.CodeSeg.start
DataSegEntry dd section.DataSeg.start
StackSegEntry dd section.StackSeg.start
ProgSegEntryTabEnd:

section CodeSeg align=16 vstart=0x0000
nop
nop
nop
;執行到這,要設定資料段堆棧段等資訊,
;加載器在加載時,設定ds/es指向LoadPhyBase
;既然這樣,加載器跳轉過來後ds:[n]能正确通路
;自己定義的使用者頭
start:
;現在section.CodeSeg.start,section.DataSeg.start中的内容是段基位址
;開始時,我先加載了ds,再用[ds:StackSegEntry],出錯了
;因為ds改變後不能在用
;mov ax,[StackSegEntry]
;mov ss,ax
;語句加載ss了 因為ds指向其他段,不再是LoadPhyBase,是以應該在ds改變前先加載ss段

mov ax,[StackSegEntry]
mov ss,ax
mov sp,stack_end
mov ax,[DataSegEntry]
mov ds,ax
lab1:
xor ax,ax
inc ax
inc ax
jmp lab1

section DataSeg align=16 vstart=0x0000
times 64 db 0xcc

section StackSeg align=16 vstart=0x0000
  resb 256
;彙編中的标号都表示偏移位置,偏移堆棧基址256,堆棧向下生長,即堆棧保留了256位元組。
;好吧,我承認stack_end這段我完全抄書的
stack_end: 

section tail align=16
ProgEnd:      

繼續閱讀