1. 程式人生 > >5.1 Win32應用程式:EXE

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中,可以使用所有的東西。