5.1 Win32應用程式:EXE
Nico Bendlin的MiniDExe很好地演示了不使用任何Delphi例程來實現一個Win32應用程式的方法。對於一個可執行程式.EXE來說,只須滿足如下條件,就可以在被Windows系統中執行:
是一個以.EXE方式生成的格式正確的PE(Portable Executable)檔案有一個正確的入口地址並記錄在PE格式檔案的頭部
編譯器會按這樣的規則生成檔案模組,並將一段入口程式碼的地址記錄在PE格式檔案的頭部。這段入口程式碼固定地呼叫System.pas中的例程_InitExe()。
因此,可以進一步簡化Nico Bendlin的MiniDExe:
//系統初始化單元
unit SysInit;
interface
var
TlsIndex:Integer=-1;
TlsLast:Byte;
const
ptrToNi1:Pointer=nil;
implementation
end.
//系統核心單元
unit System;
interface
procedure _InitExe;
procedure HandleFinally;
procedure _Halt0;
const
Kerne132=‘kernel32.d11;
User32='user32.dll';
type
TGUID=record
D1:Longword;
D2:Word;
D3:Word;
D4: array[0..7]of Byte;
end;
implementation
procedure ExitProcess (uExitCode:Longword);stdcal1;
external kerne132 name ExitProcess';
procedure _InitExe;
asm
end;
procedure HandleFinally;
asm
end;
procedure _Halt0;
begin
ExitProcess(0);
end;
end.
//示例程式。目標檔案為3584Bytes
program MiniDExe;
function ShowMessageBox(hwnd:Longword;1pText, 1pCaption:PChar;uType:Longword):Integer;stdcal1;external user32 name 'MessageBoxA';
const
MB_ICONINFORMATION=$00000040;
begin
ShowMessageBox (0, Written in pure Delphi!', Hello World!', MB_ICONINFORMATION);
end.
通過匯入外部例程,上面的程式碼可以呼叫全部Win32API以及其他的動態連結庫資源。可能存在的問題包括:
- 由於沒有處理在入口程式碼中傳入的單元初始化表,因此單元檔案的初始化和結束化節是無效的;
- 由於沒有內部例程的支援,因此一些相容型別的賦值不能進行。例如長字串與寬字串的賦值;
- 由於沒有處理_HandleFinally(),因此try-finally-end 語法是無效的(try-except-end是由SysUtils.pas來處理的)。
5.1.2初始化例程InitExe()
//code from SysInit, pas
procedure _InitExe(Initrable: Pointer);
begin
T1sIndex:=0;
HInstance:=GetModuleHandle(nil);
Module. Instance:=HInstance;
Module. CodeInstance:=0;
Module.DataInstance:=0;
InitializeModule;
_StartExe(InitTable,QModule);
end;
位於Syslnitpas中的初始化例程_InitExe()其實只完成如下極少的功能:
- 初始化系統全域性變數T1sIndex和HInstance;
- 初始化模組資訊記錄Module;
- 呼叫InitializeModule()來初始化內部模組表;呼叫_StartExe()。
其中模組資訊記錄Module是非常重要的一個系統內部變數,其型別定義如下:
PLibModule=~TLibModule;
TLibModule=record
Next:PLibModule;
Instance:Longword;//模組的例項控制代碼
CodeInstance:Longword;//模組的程式碼例項控制代碼
DataInstance:Longword;//模組的資料例項控制代碼
ResInstance:Longword;//模組的資源例項控制代碼
Reserved:Integer;
end;
5.1.3內部模組表管理例程
Delphi提供了一組用於管理模組資訊記錄和內部模組表的例程:
procedure RegisterModule(LibModule:PLibModule);
procedure UnregisterModule(LibModule:PLibModule);
function FindHInstance(Address:Pointer):Longword;
function FindClassHInstance(ClassType:TClass):Longword;
function FindResourceHInstance(Instance:Longword):Longword;
function LoadResourceModule(ModuleName:PChar;Checkowner:Boolean=True):Longword;
procedure EnumModules(Func:TEnumModuleFunc;Data:Pointer);overload;
procedure EnumResourceModules(Func:TEnumModuleFunc;Data:Pointer);overload;
procedure EnumModules(Func:TEnumModuleFuncLW;Data:Pointer);overload;
procedure EnumResourceModules(Func:TEnumModuleFuncLW;Data:Pointer);overload;
procedure AddModuleUnloadProc(Proc:TModuleUnloadProc);overload;
procedure RemoveModuleUnloadProc(Proc:TModuleUnloadProc);overload;
procedure AddModuleUnloadProc(Proc:TModuleUnloadProcLW):overload;
procedure RemoveModuleUnloadproc (Proc: TModuleUnloadProcLW); overload;
//define in SysInit. pas
procedure InitializeModule;
procedure UninitializeModule;
需要注意的是,這個內部模組表與作業系統的模組表並不一致。Delphi的內部模組表只記錄當前模組載入的包,而作業系統的模組表記錄當前程序載入的所有模組。
初始情況下,.EXE的內部模組表只有一個記錄,即當前模組((.EXE)的資訊記錄。
SysInit單元的例程InitializeModule()只是簡單地呼叫System單元中的RegisterModule()。例程RegisterModule()則負責把模組放在系統模組列表的頭部。
5.1.4.EXE啟動例程_StartExe()
_InitExe()將由啟動程式碼傳入的單元初始化表,以及定義在SysInit.pas中的當前模組資訊指標傳給_StartExe(),從而真正地啟動.EXE的初始化過程。
procedure _StartExe(InitTable:PackageInfo;Module:PLibModule);
begin
RaiseExceptionProc := GRaiseException;//初始化異常引發程式的指標
RTLUnwindProc := QRTLUnwind;//初始化異常展開程式的指標
InitContext.InitTable := InitTable;
Initcontext.Initcount := 0;//在InitUnits()中將使用Initcount對初始化過
//的單元進行計數
Initcontext.Module := Module;//.EXE的模組資訊
MainInstance := Module.Instance;//模組控制代碼
IsLibrary := False;
InitUnits;
end;
在_StartExe()中,主要處理初始化上下文(InitContext)。這個系統內部變數用於記錄初始化和結束化中的一些重要資訊。結構如下:
type
PInitContext=TInitContext;
TInitContext=record
Outercontext:PInitContext;{當前上下文的備份}
InitTable:PackageInfo;{單元初始化資訊表}
InitCount:Integer;{InitTable的長度}
Module:PLibModule;{當前模組資訊指標}
DLLSaveEBP:Pointer;{saved regs for DLLs}
DLLSaveEBX:Pointer;{saved regs for DLLs}
DLLSaveESI:Pointer;{ saved regs for DLLs}
DLLSaveEDI:Pointer;{saved regs for DLLsExitProcessTLS:procedure;}
ExitProcessTLS:procedure;{程序TLS退出例程}
DLLInitState:Byte;{0=package,1=DLL shutdown,2=DLLstartup}
end platform;
該記錄中各個域的使用情況如表5-1。
5.1.5應用程式的結束化控制
在Delphi中,有四種情況可以導致一個應用程式(程序)的結束:
- 程式程式碼執行完成,執行.DPR的結束語句“END.”正常退出;
- 應用程式內部呼叫例程procedure halt();
- 應用程式內部呼叫作業系統API;
- procedure ExitProcess();
- 應用程式內部或者其他應用程式呼叫作業系統API:function TerminateProcess()。
最後兩種方法都是用作業系統API來使程序中止的,這種情況下,Delphi不做任何的處理。前兩種方法最終都將呼叫內部例程_HaltO()。在這個例程中,與.EXE相關的程式碼有:
procedure Halt0;
var
P:procedure;
begin
//檢查並執行退出過程
if InitContext.DLLInitState=0 then //.exe module's DLLInitState=0
while ExitProc <>nil do
begin
eP := ExitProc;
ExitProc := nil;
P;
end;
//檢查並顯示系統錯誤資訊,WriteErrorMessage()例程將自動識別是否是控制檯輸出
if ErrorAddr <>nil then
begin
MakeErrorMessage;
WriteErrorMessage;
ErrorAddr := nil;
end;
//檢查模組的初始化上下文
while True do
begin
//單元結束化
FinalizeUnits;
//執行與procedure UninitializeModule()例程相同的操作
if(Initcontext.DLLInitState <=1)or (ExitCode <>0)then
begin
if InitContext.Module<>nil then
with InitContext do
begin
//從內部模組表中解除安裝,但不併表明模組從記憶體中解除安裝
UnregisterModule(Module);
//從記憶體中解除安裝模組載入的資源模組
if(Module.ResInstance <>Module.Instance)and(Module.ResInstance <>0)then
FreeLibrary(Module.ResInstance);
end;
end;
//如果當前模組是,EXE,則試圖執行ExitProcessProc(),然後退出程序
if InitContext.OuterContext=nil then
begin
if Assigned(ExitProcessProc)then
ExitProcessProc;
ExitProcess(ExitCode);
end;
nitContext := InitContext.OuterContext^
end;
end;
在退出過程ExitProc的檢查時,沒有使用IF來檢測“ExitProc<>Ni1”,而是採用了while迴圈,這使得可以在ExitProc中再次給ExitProc賦值,從而形成ExitProc的連結串列。這意味著類似下面的程式碼能被正常地執行:
var
oldExitProc:Pointer;
procedure NewExitProc;
begin
showmessage('test');
BxitProc:=01dExitProc;//在退出過程中重設ExitProc
end;
procedure ReplaceExitProc;
begin
OldExitProc:=ExitProc;
ExitProc:=QNewExitProc;
end;
應當使用SysUlils,pas中的例程AddExitProc()來安全地操作ExitProc。
在System,pas單元的內部,還隱藏著一個模組退出過程列表。這個列表中的退出過程,是通過在procedure UnregisterModule()中呼叫例程NotifyModuleUnload()來執行的。可以使用例程AddModuleUnloadProc()和RemoveModuleUnloadProc()來維護模組的退出過程列表。
值得注意的是HaltoO中還有一個ExitProcessProc的檢查過程。在Delphi看來:ExitProc是表明當前可執行模組(.EXE)的退出過程;ModuleunloadProc是任意型別模組(.EXE、.DLL、.BPL)的退出過程;而ExitProcessProc則是當前程序的退出過程。對於.EXE來說,當前模組與當前程序有非常緊密的關係,但ExitProcessProc與ExitProc卻完全無關。
因此,只有.EXE會使用到ExitProc和ExitProcessProc過程。而ModuleUnloadProc是當前程序中所有模組都可以使用的。如果動態連結庫和包不是使用“帶執行期庫”方式編譯的,那麼,ExitProc和ExitProcessProc對於這兩種模組來說是無意義的。
只有條件“InitContext.OuterContext=ni1”為真,才表明當前模組是.EXE模組。這種情況下,Halto()才執行作業系統API:ExitProcess()退出程序。
HaltO()例程的結束化處理中也包括單元結束化。不過需要注意的是:系統是在模組結束化之前呼叫例程FinalizeUnits()。這意味著在ModuleUnloadProc和ExitProcessProc中無法使用某些型別的全域性變數(例如物件),但是在ExitProc中,可以使用所有的東西。