----- 老鳃 --------
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技术有兴趣的朋友们,可以马上动手亲身体验一下,还等什么呢!
请发表评论