----- 老鰓 --------
Delphi作為WINDOWS平台下的RAD工具,其開發的高效性吸引了眾多軟體公司用來開發應用程式層軟體,除了一般RAD所具備的高效性外,Delphi的開放性(開放原始碼、方便深入底層)確實讓眾多的開發人員愛不釋手,Delphi作為優秀的WINDOWS平台下的32位編譯器,其用途是非常之廣的,國外已經有不少組織利用Delphi來開發進階編譯器、驅動程式,甚至作業系統,筆者有心做了一番嘗試,感受到Delphi的另一片廣闊天地。
本文將帶領讀者來領略一下Delphi在開發X86的作業系統核心方面的應用,旨在起個拋磚引玉的作用,啟迪廣大開發人員對優秀的編譯器進行更深入的研究。
首先介紹一下本文中簡單OS雛形的Image檔案(磁碟片映像檔案)的大致架構(有興趣的讀者可以自行重新安排和擴充):
[OS大致運行架構]:
啟動代碼負責進行必要的保護模式初始化,載入OS核心代碼,進行段寄存器初始化化直接跳到OS核心代碼處執行。
啟動代碼用彙編編寫、Nasm編譯,OS核心代碼用Delphi實現。
第一步: 產生啟動代碼
啟動代碼主要功能是構建GDT(含程式碼片段和資料區段,平坦記憶體設定),讀取核心代碼到0x8000處,然後切換到保護模式,跳轉到核心,內容如下:
[BITS 16]
[ORG 0x7C00]
jmp BootBegin
; GDT資料
gdtBegin:
; 空描述符
dd 0
dd 0
codeSel equ $ - gdtBegin ;程式碼片段選擇子
; 程式碼片段描述符
dw 0xffff
dw 0
dw 0x9A00
dw 0x00CF
dataSel equ $ - gdtBegin ;資料區段選擇子
; 資料區段描述符
dw 0xffff
dw 0x0000
dw 0x9200
dw 0x00CF
gdtend:
gdtInfo:
dw gdtend - gdtBegin - 1 ; GDT 大小
dd gdtBegin ; GDT 地址
BootBegin:
Mov ax,cs
Mov ds,ax
; 從第二扇區開始讀核心到記憶體中 0000:0x8000 (es:bx)處
;(讀17個扇區,讀者可以自行擴充)
readKernel:
mov ax , 0x0000
mov es , ax
mov bx , 0x8000
mov ah , 2
mov dl , 0
mov ch , 0
mov cl , 2
mov al , 17
int 13h
jc readKernel
; 關中斷
cli
; 載入 GDT
lgdt [gdtInfo]
;進入保護模式
mov eax , cr0
or eax , 1
mov cr0 , eax
; 跳入32位的程式碼片段中
jmp codeSel: code32Begin
[BITS 32]
code32Begin:
;設定 DS,ES,SS,FS,GS
mov ax , dataSel
mov ds , ax
mov es , ax
mov ss , ax
mov fs , ax
mov gs , ax
mov esp , 0x30000 ;堆棧初始設定
; 跳入核心
jmp codeSel:0x8000
;---------------------------------------------------------------------------
times 510-($-$$) db 0
;啟動盤標誌
dw 0xAA55
利用Nasm編譯成COM檔案,寫入Image檔案第一扇區(0-0x01ff).完成啟動代碼設定(註:Image原始檔案產生最簡單的方式是用Ultraedit產生一個0x167fff大小的二進位檔案。啟動代碼寫入也同樣可以用Ultraedit16進位編輯功能實現)。
第二步: 開發核心代碼
下面著重介紹如何通過DELPHI開發OS核心代碼。
首先瞭解一下DELPHI產生的PE程式碼片段結構,對不顯式引用任何系統單元的工程,DELPHI會預設在程式碼片段前部加入System.Pas和SysInit.Pas這兩個Delphi核心運行期庫RTL, RTL之後是我們自己的單元,最後才是Program中begin…end之間的代碼,為了簡化從PE檔案中載入核心代碼的煩瑣操作,我們可以只用到程式碼片段,並在SysInit單元之後(即OS核心代碼之前)和Program代碼結束處加上標記,這兩個標記之間的代碼就是我們想要OS代碼,讀者可以自行變換思路,也可以使用多段,只要能方便地從PE中載入出核心即可。
為了簡化開發,本文涉及的利用DELPHI編寫作業系統代碼有幾個要點:
1.不依賴於RTL,不調用任何DELPHI的RTL函數。
2.在代碼中定義資料(使用嵌入彙編),只用程式碼片段,對內部定義的資料訪問使用相對
定址,保證代碼可以載入到記憶體任何位置執行。
3.因為是OS核心,記憶體規劃好可以隨意使用,不需要申請。
OK,現在開啟Delphi,建立一個空工程(不引用任何單元),然後添加一個空的單元Kernel.pas(即OS核心單元),然後在該單元中添加一個過程KernelBegin,作為核心的入口過程,並在單元開始處和工程結束處打上核心起始和結束標誌,先實現簡單的核心操作,調用showTest過程在螢幕上顯示兩個字元’OS’然後進入死迴圈,代碼內容如下:
program OsKernel;
uses
Kernel in 'Kernel.pas';
begin
//顯式調用的代碼才會被DELPHI編譯進EXE,所以有用的代碼要調用一下
KernelBeginFlag;
//核心結束標記
asm
db 'KernelEnd'
end;
end.
unit Kernel;
interface
//核心起始標記過程
procedure KernelBeginFlag;
//核心入口
procedure KernelBegin; stdcall;
//顯示字元:在螢幕第11行第1列顯示’OS’
procedure showTest; stdcall;
implementation
//核心起始標記過程,
//擴充單元數目時要注意調整單元引用次序確保該過程編譯在核心代碼的頭部(可以通過DELPHI反組譯碼察看)
procedure KernelBeginFlag;
begin
asm
db 'KernelBegin'
end;
KernelBegin;
end;
procedure KernelBegin; stdcall;
begin
//開始核心操作
showTest;
end;
procedure showTest; stdcall;
var p: PChar;
begin
p := PChar($b8000 + (80 * 10 + 0) * 2); //計算行列對應的顯存地址
p[0] := 'O';
p[1] := #$0c; //顯示內容 黑底紅字
p[2] := 'S';
p[3] := #$0c;
while true do;
end;
end.
編譯產生OsKernel.exe,現在需要一個工具把核心代碼抓取到IMAGE檔案第二扇區開始處,下面的程式實現此功能(同樣用DELPHI編寫):
program WriteOSToImg;
uses
dialogs,classes,SysUtils;
var f1,f2:TFilestream;
b:char;
p:pointer;
begin
f1 := nil;
f2 := nil;
try
try
f1 := TFileStream.Create('OsKernel.exe',fmOpenRead);
f2 := TFileStream.Create('MyOS.IMG',fmOpenwrite);
f2.Position := $200;
while true do
begin
f1.Read(b,1);
if b <> 'K' then continue;
f1.Read(b,1);
if b <> 'e' then continue;
f1.Read(b,1);
if b <> 'r' then continue;
f1.Read(b,1);
if b <> 'n' then continue;
f1.Read(b,1);
if b <> 'e' then continue;
f1.Read(b,1);
if b <> 'l' then continue;
f1.Read(b,1);
if b <> 'B' then continue;
f1.Read(b,1);
if b <> 'e' then continue;
f1.Read(b,1);
if b <> 'g' then continue;
f1.Read(b,1);
if b <> 'i' then continue;
f1.Read(b,1);
if b <> 'n' then continue;
break;
end;
//複製核心:簡單起見,暫時只複製10K
getmem(p,1024*10);
try
f1.Read(p^,1024*10);
f2.Write(p^,1024*10);
finally
freemem(p,1024*10);
end;
showmessage('寫核心完畢!');
finally
f1.Free;
f2.Free;
end;
except
showmessage('寫核心出錯!');
end;
end.
現在確保磁碟片映像檔案MyOS.IMG、核心PE檔案OsKernel.exe、寫核心程式WriteOSToImg.exe在同一目錄下,然後運行WriteOSToImg.exe,完成核心的寫入,可以利用VirtualPC(因為VirtualPC運行結果更接近實際機器)運行我們的磁碟片映像檔案MyOS.IMG了,運行結果如下:
OK,雖然只是很簡單的兩個字元,但意義重大,我們成功地實現了OS跳轉到Delphi開發的核心,這意味著我們可以利用Delphi進行高效便捷的OS開發了,可以方便地在DELPHI下進行OS各無特權操作模組的獨立測試,有進行特權/直接寫記憶體等核心操作的模組需要在BOCHS下進行調試,因為篇幅所限制,在此就不對BOCHS調試方法做介紹,有興趣的朋友可以在互連網查閱相關資料。
為了啟發讀者的擴充思路,下面講解一下如何存取碼段中定義的資料,保證代碼可以在記憶體任意位置運行,如我們在Kernel.pas中增加了一個在特定行列開始顯示一個字串過程 DispStr(字串以/0結束),代碼如下:
procedure DispStr(RowId,ColId: Integer; p:PChar); stdcall;
var h:PChar;
i: Integer;
begin
h := PChar($b8000 + (80 * RowId + ColId) * 2);
i := 0;
while true do
begin
if p[i] = #0 then break;
h[i*2] := p[i];
h[i*2+1] := #$0c;
inc(i);
end;
end;
現在要把KernelBegin過程中定義的一個字串顯示出來,代碼如下:
procedure KernelBegin; stdcall;
label Dispdat1,kBegin;
var p1: PChar;
begin
//取欲顯示字串首地址
asm
push esi
call @BB
@BB:
//運行期間擷取該處實際運行地址到esi 保證代碼可以在記憶體任何位置運行
pop esi
//計算出Dispdat1在實際運行時的地址
mov ebx,offset Dispdat1
sub ebx,offset @BB
add ebx,esi
mov p1,ebx
pop esi
jmp kBegin
Dispdat1: db 'My OS 2006 is Loading..............',0 //待顯示的字串
kBegin:
end;
DispStr(11,22,p1);
while true do;
end;
注意到call指令的妙用,利用CALL指令原理(將下一條指令地址壓棧,然後跳轉目的地址),讓call指令直接調用下條指令,在下條指令處即可從堆棧中取出當前地址,這樣就可以利用位移地址差值擷取出周圍各處的運行期地址了,這樣編譯好的模組就實現了無需重定位無首地址限制正常啟動並執行目的。
特別說明一點,對於特權指令,可以利用Delphi的嵌入彙編技術直接使用,均能編譯通過,各種X86資料結構利用DELPHI的結構體定義取代彙編的直接位元組級定義,大大方便了核心的組織。
到此為止,讀者應該對利用DELPHI進行X86的OS開發有個基本的感性認識了,有興趣的讀者可以自行從KernelBegin開始進行OS核心的擴充,如設定開啟分頁、設定開啟中斷、簡單的進程調度控制等,OS技術是軟體技術的基礎,能用象DELPHI這樣的便捷開發工具進行OS技術的親身實踐,相信對廣大軟體愛好者來說是很有吸引力的,對廣大長期在WINDOWS平台下工作的開發人員來說是便捷的,拋開煩瑣的LINUX環境下的OS開發,天地就開闊多了,對OS技術有興趣的朋友們,可以馬上動手親身體驗一下,還等什麼呢!
......老鰓...採菊東籬下.悠然見南山...2007.02......