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
    评论
内容简介 《链器和加载器》讲述构建程序的关键工具——链器和加载器,内容包括链和加载、体系结构、目标文件、存储分配、符号管理、、重定位、加载和覆盖、共享动态和加载、动态的共享,以及着眼于成熟的现代链器所做的一些变化;并介绍一个持续的实践项目,即使用Perl语言开发一个可用的小链器。 《链器和加载器》适合高校计算机相关专业的学生、实习程序员、语言设计者和开发人员阅读参考。 编辑推荐 《链器和加载器》:不管你的编程语言是什么,不管你的平台是什么,你很可能总是会涉及链器和加载器的功能。但是你知道如何最大限度地利用它们吗?只有现在,随着《链器和加载器》的出版,总算有一本深入完整地彻底揭示编译时和运行时过程的权威著作了。 《链器和加载器》首先通过实例深入浅出地阐述了在不同的编译器和操作系统中链和加载过程的差异。在这个基础上,作者提出了清晰实用的忠告,来帮助你创建更快、更清晰的代码。你将会学习如何规避和Windows DLL相关的陷阱,充分利用UNIX ELF模式等。如果你对程序设计抱有非常认真的态度,那么你可以通过这本书充分地理解这个领域内最难懂的主题之一。《链器和加载器》对于编译器和操作系统课程同样也是一本理想的补充读物。 《链器和加载器》特性 ◆覆盖了Windows,UNIX,Linux,BeOS和其它操作系统的动态过程。 ◆解释了Java链模式,以及它是如何应用在网络小应用程序和可扩展Java代码中的。 ◆帮助你编写更优雅、更高效的代码,以及构建能够被更加高效地编译、加裁和运行的应用程序。 ◆包含了一个用Perl构建链器的练习项目,项目文件可以从网络下载得到。 媒体推荐 “我很享受阅读这本对实现链器和加载器的众多技术和挑战进行有效概述的书。虽然书中的多数例子都集中在今天被广泛使用的三种计算机体系结构上,但这本书也包含了很多描述过去的一些有趣和古怪的计算机体系结构的注解。通过这些真实的战例,我断定作者本人真正经历了这些事情并存活了下来给我们讲述这个故事。” ——Guy Steele 作者简介 作者:(美国)莱文(John R.Levine) 译者:李勇 莱文(John R.Levine),是很多书籍的作者或合作者,包括Lex & Yacc(O'Reilly),Programming for Graphics Files in C and C++(Wiley),以及7-heIntemetforDummies(IDG)。他还是Journal of C Language Translation的荣誉退休发行人、comp.compilers新闻组的长期仲裁人员,以及某个最早的商用Fortran 77编译器的创建考。他在耶鲁大学获得了计算机科学的博士学位。 目录 第1章 链和加载 1.1 链器和加载器做什么? 1.2 地址绑定:从历史的角度 1.3 链与加载 1.4 编译器驱动 1.5 链:一个真实的例子 练习 第2章 体系结构的问题 2.1 应用程序二进制口 2.2 内存地址 2.3 地址构成 2.4 指令格式 2.5 过程调用和寻址能力 2.6 数据和指令引用 2.7 分页和虚拟内存 2.8 Intel 386分段 2.9 嵌入式体系结构 练习 第3章 目标文件 3.1 目标文件中都有什么? 3.2 空目标文件格式:MS-DOS的COM文件 3.3 代码区段:UNIX的a.out文件 3.4 重定位:MS-DOS的EXE文件 3.5 符号和重定位 3.6 可重定位的a.out格式 3.7 UNIX的ELF格式 3.8 IBM 360目标格式 3.9 微软可移植、可执行体格式 3.10 Intel/Microsoft的OMF文件格式 3.11 不同目标格式的比较 练习 项目 第4章 存储空间分配 4.1 段和地址 4.2 简单的存储布局 4.3 多种段类型 4.4 段与页面的对齐 4.5 公共块和其他特殊段 4.6 链器控制脚本 4.7 实际中的存储分配 练习 项目 第5章 符号管理 5.1 绑定和名字解析 5.2 符号表格式 5.3 名称修改 5.4 弱外部符号和其他类型符号 5.5 维护调试信息 练习 项目 第6章 6.1 的目的 6.2 的格式 6.3 建立文件 6.4 搜索文件 6.5 性能问题 6.6 弱外部符号 练习 项目 第7章 重定位 7.1 硬件和软件重定位 7.2 链时重定位和加载时重定位 7.3 符号和段重定位 7.4 基本的重定位技术 7.5 可重链和重定位的输出格式 7.6 其他重定位格式 7.7 特殊情况的重定位 练习 项目 第8章 加载和覆盖 8.1 基本加载 8.2 带重定位的基本加载 8.3 位置无关代码 8.4 自举加载 8.5 树状结构的覆盖 练习 项目 第9章 共享 9.1 绑定时间 9.2 实际的共享 9.3 地址空间管理 9.4 共享的结构 9.5 创建共享 9.6 使用共享 9.7 使用共享运行 9.8 malloc hack和其他共享问题 练习 项目 第10章 动态和加载 10.1 ELF动态 10.2 ELF文件内容 10.3 加载一个动态程序 10.4 使用PLT的惰性过程链 10.5 动态的其他特性 10.6 运行时的动态 10.7 微软动态 10.8 OSF/1伪静态共享 10.9 让共享快一些 10.10 几种动态方法的比较 练习 项目 第11章 高级技术 11.1 C++的技术 11.2 增量链和重新链 11.3 链时的垃圾收集 11.4 链时优化 11.5 链时代码生成 11.6 Java链模型 练习 项目 参考文献 序言 几乎从有计算机以来,链器和加栽器就是软件开发工具包中的一部分,因为它们允许使用模块(而不是一个单独的大文件)来构建程序的关键工具。 早在1947年,程序员们就开始使用原始的加载器:将程序的例程存储在多个不同的磁带上,并将它们合并、重定位为一个程序。在20世纪60年代早期,这些加栽器就已经发展得相当完善了。由于那时内存很贵且容量有限,计算机的速度很慢(以今天的标准),为了创建复杂的内存覆盖策略(以将大容量的程序加载到小容量内存中),以及重新编辑先前链过的文件(以节省重新创建程序的时间),这些链器都包含了很多复杂的特性。 20世纪七八十年代,链技术几乎没有什么进展。链器趋向于更加简单,虚拟内存技术将应用程序和覆盖机制中的大多数存储管理工作都转移给了操作系统,越来越快的计算机和越来越大的磁盘也使得重新链一个程序或替换个别模块比仅仅链改变过的地方更加容易了。从20世纪90年代起,链器又开始变得复杂起来,增加了诸多现代特性,包括对动态共享的支持和对C++独特要求的支持。同时,像IA64那样具有宽指令字和编译时访存调度特性的先进处理器架构,也需要将一些新的特性加入到链器中,以确保在被链的程序中可以满足代码的这些复杂需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值