5.3 动态链接库:DLL

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.19begin
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,否则:
  1. 初始化ModuleIsLib为True
  2. 从[EBP+8]位置取出DLL的模块实例句柄并填入全局变量HInstance。
  3. 初始化模块信息记录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 <>0then
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工或其他例程,可能导致不可测的后果};
endendbegin
//下面代码只会在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相关的代码。

转载于:https://www.cnblogs.com/YiShen/p/9887846.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值