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