上次講解到了OS內核的開始以及在進入保護模式之前需要了解一些概念。首先給出這部分內容的完整代碼,然後分別來介紹。
BOTPAK EQU 0x00280000
DSKCAC EQU 0x00100000
DSKCAC0 EQU 0x00008000
CYLS EQU 0x0ff0
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2
SCRNX EQU 0x0ff4
SCRNY EQU 0x0ff6
VRAM EQU 0x0ff8
org 0xc200 ; 這裏的org並不是真的可以決定在內存中的加載位置(由ipl決定),而是爲了讓編譯器可以算出正確的標籤所代表的內存位置
mov al, 0x13 ; 320×200×8位色
mov ah, 0x00
int 0x10 ; 0x10號中斷,ah=0時爲VGA顯卡圖形模式
mov byte[VMODE], 8
mov word[SCRNX], 320
mov word[SCRNY], 200
mov dword[VRAM], 0x000a0000
mov ah, 0x02
int 0x16 ; ah=0x02時獲取鍵盤led燈狀態,保存在al中
mov [LEDS], al
; PIC關閉一切中斷
; 根據AT兼容機的規格,如果要初始化PIC
; 必須在CLI之前進行,否則有時會掛起
; 隨後進行PIC的初始化
MOV AL,0xff
OUT 0x21,AL
NOP ; 如果連續執行OUT指令,有些機種會無法正常運行
OUT 0xa1,AL
CLI ; 禁止CPU級別的中斷
; 爲了讓CPU能夠訪問1MB以上的內存空間,設定A20GATE
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout
; 切換到保護模式
[INSTRSET "i486p"] ; 要想使用486指令
LGDT [GDTR0] ; 設定臨時GDT
MOV EAX,CR0
AND EAX,0x7fffffff ; 設bit31爲0(爲了禁止頒)
OR EAX,0x00000001 ; 設bit0爲1(爲了切換到保護模式)
MOV CR0,EAX
JMP pipelineflush
pipelineflush:
MOV AX,1*8 ; 可讀寫的段32bit
MOV DS,AX
MOV ES,AX
MOV FS,AX
MOV GS,AX
MOV SS,AX
; bootpack的傳送
MOV ESI,bootpack ; 傳送源
MOV EDI,BOTPAK ; 傳送目的地
MOV ECX,512*1024/4
CALL memcpy
; 磁盤數據最終轉送到它本來的位置去
; 首先從啓動扇區開始
MOV ESI,0x7c00 ; 傳送源
MOV EDI,DSKCAC ; 傳送目的地
MOV ECX,512/4
CALL memcpy
; 所有剩下的
MOV ESI,DSKCAC0+512 ; 傳送源
MOV EDI,DSKCAC+512 ; 傳送目的地
MOV ECX,0
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 ; 從柱面數變換爲字節數除以4
SUB ECX,512/4 ; 減去IPL
CALL memcpy
; 必須由asmhead來完成的工作,至此全部完畢
; 以後交由bootpack完成
; bootpack的啓動
MOV EBX,BOTPAK
MOV ECX,[EBX+16]
ADD ECX,3 ; ECX += 3;
SHR ECX,2 ; ECX /= 4;
JZ skip ; 沒有要轉送的東西時
MOV ESI,[EBX+20] ; 轉送源
ADD ESI,EBX
MOV EDI,[EBX+12] ; 轉送目的地
CALL memcpy
skip:
MOV ESP,[EBX+12] ; 棧初始值
JMP DWORD 2*8:0x0000001b
waitkbdout:
IN AL,0x64
AND AL,0x02
IN AL,0x60 ; 空讀(爲了清空數據接收緩衝區中的垃圾數據)
JNZ waitkbdout ; AND的結果如果不是0,就跳到waitkbdout
RET
memcpy:
MOV EAX,[ESI]
ADD ESI,4
MOV [EDI],EAX
ADD EDI,4
SUB ECX,1
JNZ memcpy ; 減法運算的結果如果不是0,就轉到memcpy
RET
ALIGNB 16
GDT0:
RESB 8 ; NULL selector
DW 0xffff,0x0000,0x9200,0x00cf ; 可以讀寫的段(segment)32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 可以執行的段(segment)32bit(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:
31-36行做的事情在於,如果在切換模式的(指從實模式切換至保護模式)過程中發生了中斷,CPU是不能停下來去處理中斷的(因爲處理的方式都沒有切換過來),所以,在這之前,需要關閉中斷。兩句out指令用於屏蔽主PIC和從PIC的中斷髮送,CLI用於停止CPU級別的中斷。NOP指令使得CPU空轉一個時鐘週期,這是爲了防止兩句OUT連用存在的隱患(例如沒有優化到位的競爭冒險)。
接下來會調用waitkbdout函數,所以先來介紹110-115行的內容,這是在清除緩衝器數據,把鍵盤緩衝區中的數據轉移出來,並且清空,循環讀取直至控制器數據爲0,跳出函數體。
40-46行,初始化鍵盤數據,啓動鍵盤控制電路。向0x60輸出0xdf這條指令是爲了開啓1M以上的內存空間,在x86(以及x86以後)的架構中,計算機剛啓動時都必須進入實模式,而爲了使用這個模式,就不得不先把1MB以外的內存屏蔽掉,使得此時的狀態能夠模擬出8086時的工作狀態,換句話說就是爲了向下兼容,在軟件級別上使得其與8086工作模式相同。而這條out指令將會使得A20GATE信號箱轉爲1狀態(也就是禁止其抑制狀態),也就開啓了大內存的支持。
50行準備進入保護模式,之後的彙編指令將會被翻譯成32位機器碼。而由於保護模式和實模式尋址方式不同,要想使得段寄存器有效,就必須使用GDT,52行這裏暫時制定一個GDT,實際的GDT將會在以後去完成。設置了CR0之後,便正式進入保護模式。57行這個突兀的jmp指令的目的在於,由於保護模式的機器語言會採用管道機制,也就是會預先解釋下面的指令,但是,由於剛剛轉換成保護模式,下面的一條語句還是按照以前的方式來解釋的,直接去執行會出錯,所以專門加一條jmp指令是爲了容錯,讓計算機重新解釋一遍後面的語句,防止奇怪的bug。而58到64行就是用來初始化這些32位寄存器的。
由於接下來會用到內存傳送函數,因此先來解釋117-124行。這個函數的目的是以4字節爲單位進行內存數據的複製,將以ESI數據爲首地址的內存數據傳送到以EDI數據爲首地址的內存中,傳送數據的大小是ECX中的值。
66-71行就是藉助了內存複製函數,把將來將要寫的操作系統內核的部分加載到內存(在合川先生的教程中把它明明爲bootpack,也就是用於啓動的程序包,但是由於我們將要寫的操作系統比較小,還並不至於用一個專門的bootloader來啓動,所以把bootpack和OS寫在一起,所以這裏的bootpack就可以理解成操作系統內核程序本身,在後面的程序中將不再使用bootpack這個名字,而是使用OSMain這個名字,特此說明)。到90行爲止,都是在加載數據。之前給啓動區空出來的內存位置,我們把啓動區也加載進去,而之前ipl只加載了10個扇區,這裏內存夠用了,就把剩下的加載完畢。
97-105行來解析系統文件頭,這裏要具體說明爲什麼如此編寫有點複雜。簡單來說,我們在完成這些設定以後,就想用C語言來作爲主要語言進行操作系統的開發,但C語言編譯後不像彙編語言,你可以直接指定每一段函數的加載位置,C語言編譯後函數的加載位置是由編譯器臨時決定的,再加之不同編譯器以及編譯參數對編譯內容進行的優化也不盡相同,所以,我們無法指定C語言文件編譯後各函數之間的執行順序。而這裏就是在對文件頭進行分析,找出合適的執行切入點,我們纔可以把我們用C語言的入口函數加載到需要的地方。
106-108行就跳轉到剛纔已經加載合適的程序部分,也就是說,接下來就該執行我們將有C語言編寫的入口函數了(我個人將其命名爲OSMain)。關於當前只是進行大概解釋的部分,會在後續的文章中詳細介紹。
當前的程序已經成功的把裸機通過軟驅啓動運行了ipl加載了程序並且切換至了32位保護模式,取得了4G內存的尋址空間,最後還提供了接口使得我們可以將程序的主邏輯轉交至C語言,增加開發效率。下次將會開始使用C語言進行主要開發語言,將會介紹OSMain函數的寫法。