5.3.1丢失的DlIMain()
使用过C语言或者汇编语言的DLL开发者都清楚地知道:DLL应该从一个名为D11Main()的例程开始编写,而编译器也会使D11Main()成为DLL载入时的入口。以Delphi方式描述的例程声明如下:
function D1lMain(hinstDLL:DWORD;fdwReason:DWORD;pVReserved:Pointer):BOOL;stdcall;
操作系统在四种情况下,以不同的fdwReason参数值调用_D11Main()入口。具体如下:
- DLL_PROCESS_ATTACH:进程试图装入DLL;
- DLL_PROCESS_DETACH:进程试图卸载DLL;
- DLL_THREAD_ATTACH:进程开启了一个线程;
- DLL_THREAD_DETACH:进程中的一个线程结束。
■DLL_PROCESS_ATTACH与DLL_PROCESS_DETACH
同一个DLL在一个进程中加载时,必将发生、也只发生一次DLL_PROCESS_ATTACH调用。这与在进程的哪一个线程中装入是无关的。DLL卸载时的处理与此类同。进程中止(TerminateProcess)时,DLL不会得到任何消息。
■DLL_THREAD_ATTACH与DLL_THREAD_DETACH
操作系统不会为进程的主线程发送以DLL_THREAD_ATTACH或DLL_THREAD_DETACH为参数的D11Main()入口调用。
如果装载DLL时,进程已经拥有了几个线程,那么操作系统也不会发送与这些已有线程相关的DLL_THREAD_ATTACH 调用。但是在DLL卸载时,可能会得到与它们相关的DLL_THREAD_DETACH调用。
当发生DLL_THREAD_DETACH调用时,相关的线程仍然存在。因此,动态链接库甚至可以向线程发送消息。但是不应当使用PostMessage,因为线程可能在此消息被处理之前就已经结束了。
在Delphi中,编译器隐含了有关DllMain()的一切。能够知道的,仅仅是编译器生成了一段入口代码,并调用系统内部例程_InitLib()。这段入口代码是:
Project2.dpr.19:begin 00B81FD455 push ebp 00B81FD5 8BEC mov ebp,esp 00B81FD7 83C4C4add esp,-$3c 00B81FDAB85C1FB800 mov eax,$00b81f5c//传入单元初始化表地址 00B81FDF E8F435FFFP call eInitLib //调用模块初始化例程
使用这样的入口代码,有三个目的:
在栈顶开辟大小为$3c个字节的空间;在eax寄存器中填入单元初始化表的地址;使用类似于D11Main()的接口方式调用_InitLib()。
所以,在_InitLib()中可以看到这样的一段注释:
//Icode from SysInit.pas procedure _InitLib; asm (->EAX Inittable} {[EBP+8]Hinst } {[EBP+12]Reason} {[EBP+16]Resvd} //...
因此,Delphi中的_InitLibC)也就扮演着与D11Main()类似的角色:DLL的入口点(Entry Point)。
5.3.2IniLib0例程
在_InitLib()中,Delphi仅完成四个操作:
- 如果不是DLL_PROCESS_ATTACH,则直接转入4,否则:
- 初始化ModuleIsLib为True
- 从[EBP+8]位置取出DLL的模块实例句柄并填入全局变量HInstance。
- 初始化模块信息记录Module,并调用例程procedure InitializeModule()。
- 对于所有入口的情况,均:
4.传入D11Proc和TlsProc参数,并调用例程_StartLib()。
由此可见,_InitLib()的实质是在进程装载时,完成模块在当前进程中的初始化;并负责将每次(线程和进程装载时)的入口参数传给_StartLib()。
5.3.3_StartLlib0例程
为了维护堆栈的完整性,_InitLib()采用了非常不规范的方法调用内部例程_StartLib()。它具体的参数传入情况为:
procedure _StartLib; asm {->EAX Init Table} {EDX Module} {ECX InitTLS } {[ESP+4]D11PrOC } {[EBP+8]HInst } {[EBP+12]Reason } {[EBP+16]Resvd}
由EAX和EDX两个参数传入模块的单元初始化表(InitTable)和模块信息记录(Module),随后它们将被填入初始化上下文记录(InitContext)。此外-StartLib()完成如下工作:
- 备份后续代码中要修改寄存器的值到InitContext的DLLSaveXXX域;
- 将当前初始化上下文的备份的指针保存到InitContext.OuterContext域:
- 初始化InitContext.InitCount;
- 初始化InitContext.DLLInitState=Reason+1(标识.EXE模块时占用了0值);
- 异常管理,线程局部存储管理;调用D11Proc,使用户代码得到处理入口的机会;
- 如果不是DLL_PROCESS_ATTACH入口,则跳转到_Halto()做结束化理;
- 单元初始化。
5.3.4.DLL的结束化过程
内部例程_Halto()同时扮演了.EXE与.DLL结束化过程的角色。
每一次操作系统向.DLL的入口传入参数并调用时,最终都会执行到_HaltO()。也就是说,.DLL中会多次调用_HaltO(),其准确的数量为:
2+DLL载入后创建的线程数*2+DLL载入前创建的线程数*1
_HaltO()总是在StartLib()调用结束时被执行到,因此,_Halto()的具体行为取决于StartLib()中对系统参数的设置和调整。而_StartLib()例程的内部逻辑相对复杂,以不同的Reason 入口参数调用_StartLib,主要功能的执行情况如表5-3。
例程_HaltO()中与DLL相关的代码有:
procedure _Halt0; var P:procedure; begin //... if ErrorAddr<> nil then begin MakeErrorMessage; WriteErrorMessage; ErrorAddr:=ni1;//这行代码是为.DLL准备的,对.EXE没有意义 end; while True do begin if(InitContext.DLLInitState=2)and(ExitCode=0)then InitContext.InitCount:=0; FinalizeUnits; //下面这一段代码,可以参考.EXE结束化处理中的相关描述 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; //如果是DLL,D11Initstate=1..4 if InitContext.D1lInitState<>0 then ExitD11; end; end;
如果是以DLL_PROCESS_ATTACH为入口,则必然是通过在库项目文件中的“end。“行进入_HaltO()的。此时,系统已经调用过InitUnits(),且InitCount存放着初始化过的单元数。如果初始化过程正常(没有修改ExitCode),则需要将InitCount置0,以避免调用FinalizeUnits时做不必要的单元结束化处理。事实上,在_HaltO()中的这段代码:
if(InitContext.DLLInitstate=2)and(ExitCode=0)then InitContext.InitCount:=0; FinalizeUnits;
与下面代码是等义的:
case InitContext.DLLInitState of 0://.EXE,单元结束化 FinalizeUnits; 3,4://.DLL,且Reason =DLL_THREAD_ATTACH 或DLL_THREAD_DETACH (由于在_StartLib()中设置InitCount=0,所以是否调用Finalizeunits()无意义); 2://.DLL,Reason=DLL_PROCESS_ATTACH if ExitCode <>0)then FinalizeUnits else (进程载入时,不应该执行单元结来化); 1://.DLL,Reason =DLL_PROCESS_DETACH FinalizeUnits; end;
对于任何参数的.DLL入口调用,_HaltO()都会调用内部例程ExitD11()来退出。过程EXitD11()主要用于恢复DLL入口时的堆栈和寄存器现场。此外,由于D11Main()实际上是一个返回BOOL值的函数,操作系统需要识别它的返回值并作进一步处理。因此,ExitD11)也根据全局变量ExitCode的值置EAX。
5.3.5DlIProc与DlIMain()的不同
Delphi中没有D1lMain()。部分资料声称,Delphi中的过程变量D11Proc用以替代D11Main)。
但这两者之间存在很多不同。
首先,D11Main()被用来接管DLL的入口,所以在D11Main()之前应该是没有代码的。
然而在D11Proc被执行之前,已经有_InitLib()和_StartLib()被执行过了。因此,D11Proc()面对的将不再是最初DLL的加载现场——栈以及一些全局变量已经被处理过了。其次D11Proc()的入口中不再有hInstance参数,如果要使用它,可以访问全局变量HInstance。
最后,也是最重要的一点:基于Delphi的机制,在D11Proc中不可能得到处理DLL_PROCESS_ATTACH的机会。因为Delphi的用户代码是从单元初始化开始的,而单元初始化例程却是在 DLL_PROCESS_ATTACH处理完之后,才会被_StartLib()调用的。这意味着没有任何用户代码,可以赶在内核试图以DLL_PROCESS_ATTACH为参数调用D11Proc之前,将D11Proc初始化为有效值。
作为弥补,Delphi提供了一种技巧让开发者得到处理DLL_PROCESS_ATTACH的机会。例如下面这个项目:
1ibrary Project1; uses SysUtils; begin //call ernitLib sleep(10); end.//call eHalt0
begin行相当于D1lMain()入口,在这里,编译器加入了调用_InitLib()的代码。因此,当_InitLib()以及StartLib()被调用完成后,程序流程会回到“sleep(10);”处,最后再通过编译在“end.”行中的“cal1@Halto”退出。
由于在_StartLib()中有这样的代码:
//... @@haveExe: MOV EAX,[EBP+12]//取Reason值 DEC EAX//减1 JNE Halt0//如果Reason-1>0,则跳转到Halto(),结束化
可见,只有以Reason参数为DLL_PROCESS_ATTACH时,流程才会回到项目文件中的“end.”行,其他情况下,都会直接跳转到_Halto()。因此,这样编写D11Proc()是完全可行的:
library SameDLLmain; uses Windows,SysUtils,Dialogs; procedure Same_D1lMain(Reason:Integer); begin case Reason of DLL_PROCESS_ATTACH, DLL,_THREAD_ATTACH, DLL_THREAD_DETACH: ShowMessage(format("HInst:%.8x,Reason:%.8x',[HInstance,Reason])); DLL_PROCESS_DETACH: (小心调用AP工或其他例程,可能导致不可测的后果}; end; end; begin //下面代码只会在DLL装载时被执行到一次,此时系统的状况为: //1.HInstance为当前模块的实例句柄 //2.内部模块表已经准备好 //3.线程局部存储已经初始化 //4.单元已经完成初始化 if not assigned(D11Proc)then//单元初始化中,可能已经给D11Proc赋过值 D11Proc:=QSame_D11Main; Same_D1lMain(DLL_PROCESS_ATTACH); end.
5.3.6动态链接库的内核最小化
如果试图以类似于C语言或者汇编语言的方式来写一个DLL,可以将Delphi中与动态链接库相关的代码大量简化,甚至可以用手工的方式来接管Delphi的入口代码。下例可以实现一个极小化的DLL:
1ibrary Minimump11; function MessageBox(hwnd:Longword;1prext,1pCaption:PChari uType:Longword):Integer;stdcal1;external user32 name "MessageBoxA'; const MB_ICONINFORMATION =$00000040; sArr;array [0..3]of pchar =(‘PROCESS_DETACH',‘PROCESS_ATTACH',"THREAD_ATTACH, THREAD_DETACH); procedure Same_D1lMain (Reason:Integer); MessageBox(0,sArr[Reason],"MinimumD11',MB_ICONINFORMATION); end; procedure BxitD11(BAX:LongBool); asm LEAVE RET 12 end; begin //在这里,可以开始写类似于D11Main()的处理代码,但入口的情况如下 {->EAX Inittable [EBP+8]Hinst } [EBP+12]Reason } [EBP+16]Resvd } //这里示范如何实现与Delphi的D11Proc类似的调用 asm mov eax,[EBP+12] mov ecx,[EBP+16] ca11 Same_D1lMain end; //这里是类似于D11Main()的返回方式 ExitD11(True); end.
编译上面的项目,仅需要在最简化内核中加入如下代码:
interface type TInitContext=Integer;//未用 procedure _InitLib; implementation procedure _InitLib; asm end:
需要注意的是,改变TInitContext的类型定义,会影响入口代码中在栈上分配空间的大小,但是不会影响程序的正常运行。因为在最小化内核中,并不需要使用InitContext.
在随书光盘中,提供一个为编写DLL而改写的最小化内核及其示例代码MiniDl.dpr。使用这个内核,可以用Delphi标准的方式开发动态链接库,而不必像上面的例子那样去重写与D11MainO相关的代码。