链接过程:Windows下动态链接

来源:互联网 发布:mysql 添加分区出错 编辑:程序博客网 时间:2024/05/23 20:52

目录:

        • Windows和Linux下动态链接的原则不同
        • 映像基地址
        • RVA
        • DLL文件的符号导出声明
        • PE文件头下的DataDirectory
        • DLL文件的符号导出表
        • PE文件的符号导入表
        • DLL显式运行时加载链接demo

1. Windows和Linux下动态链接的原则不同

Linux系统以.so共享对象设计共享库,并在设计共享对象的过程,花费很多精力实现.so对象的代码段.text多进程共享,提升空间利用率(如PIC机制、SO-NAME机制、符号版本机制等);Windows系统设计.dll(DLL文件还可以是别的扩展名,如OCX控件.ocx,控制面板程序.cpl)则并不像Linux那么小家子气,并没有花费太多精力设计一些精巧的结构或机制来提升空间利用率,更侧重程序逻辑上的模块化,使得各模块之间能够自由松散地组合、重用和升级。所以Windows系统下可以看到各种各样的软件是通过升级DLL的形式来进行版本迭代,微软本身的系统更新也是将这些升级补丁积累到一定程度后发布一个软件更新包来实现系统升级。

Windows COFF/PE文件结构下的.exe可执行文件或.dll文件都需要被映射到虚拟内存空间中才能得以运行,相比于Linux采用的精打细算的代码地址无关机制,Windows下的文件采用的是一种叫做基地址重载的方式,即并没有采用代码地址无关,所有DLL涉及到的绝对地址的引用在实际装载时都需要重定位。所以要理解PE文件结构下的文件链接和装载过程就不得不谈两个概念:映像基地址(Image-Base-Address)和相对地址(RVA, Relative-Virtual-Address)。

2.映像基地址

在Windows PE文件结构下,当一个PE文件被装载时,其对应的虚拟空间的起始地址便是基地址,而任一PE文件在编译时便存在一个指定(或默认)的优先装载地址,如.exe文件一般的基地址为0x0040 0000,而.dll文件的基地址一般默认是0x1000 0000。系统在装载.exe文件时,因为可执行文件是第一个被加载的文件,显然没人和.exe抢默认的基地址空间,从而.exe文件是不需要基地址重定位的;而DLL文件装载时,则可能遇到默认指定的优先基地址空间被别人抢占了,故而这时就需要重新选择可用的空闲地址,这时整个文件将产生整体位移。

3.RVA

相对地址顾名思义就是在PE文件基地址确认后,一个地址相对于基地址的偏移量,比如一个PE DLL文件默认基地址为0x1000 0000,一个符号存储位置的RVA为0x1000,DLL文件编译时有以下赋值操作:

MOV  DWORD PTR [0x1000 1000],  0x20

该文件实际装载时,被基地址重载到0x4000 0000,则符号对应的赋值操作应该变成:

MOV  DWORD PTR [0x1000 1000 + 0x2000 0000 - 0x1000 0000],  0x20 //此处减法只是显示重定位的原理,实际这一步计算在链接阶段就已经完成了,此处填入的应该是计算结果

4.DLL文件的符号导出声明

ELF文件.so共享对象,在默认情况下,文件中所有的全局变量和全局函数都是导出的(除非加static修饰符限制方位范围)。
DLL文件在默认情况下,是不导出任何符号的,如果要导出需要手动指明,有两种方法:
1. “__declspec(dllexport)” 修饰符指明该符号导出,对应的,”__delspec(dllimport)”修饰符是指明该符号从外部导入(如果是C++ 文件,但是希望导出函数符号的修饰规则使用C的简洁修饰规则,那么需要再在函数符号前面添加external “C”。实际上,不推荐使用C++编写DLL,因为C++只规定了语言层面的规则,但是ABI二进制层面并没有定义,故而不同编译器甚至同一编译器的不同版本的具体实现都可能不同,故而很容易出现版本不兼容或升级困难等问题。如果一定要用C++编写,需要涉及到COM(Component Object Model)技术);
2. 编写.def链接脚本,批量声明导出符号。如下是某.def链接脚本声明该DLL的导出符号。

LIBRARY  MathEXPORTSAdd   @1Sub   @2Mul   @3Div    @4  NONAME

.def文件是在输入编译指令通过/DEF声明传递给link

cl math.c /LD /DEF /math.def

系统级软件开发时,一般推荐使用.def模块脚本批量定义导出符号。一方面,C/C++编译器可能会在编译后将函数修饰的面目全非,如_Add@16之类的,这时在.def文件中可以重命名,这时__declspec()这种方式无法做到的;另一方面是除了LIBRARY/EXPORTS关键字,还可以通过NAME/VERSION/SECTIONS/STACKSIZE/HEAPSIZE等关键字来定义输出文件名/DLL版本/各段的属性/默认堆栈大小/默认堆大小。显然.def模块脚本的操作空间更大,可以封装更多的细节。

EXPORTSAdd = _Add@16

5.PE文件头下的DataDirectory

这里先介绍以下DataDirectory这一关键的结构数组,因为其概念将引出后续的符号导入表和导出表。
首先PE文件中除去sectiontable/.data/.code等段信息外,还存在PE HEADER段,即文件头。这里需要深度解析下PE文件的各段组成和DataDirectory的内容。

//IMAGE_NT_HEADERStypedef struct{    DWORD                   Signature;//PE头的标识,类似魔数,50h 45h 00h 00h    IMAGE_FILE_HEADER       FileHeader;//20字节的数据结构,包含文件的物理层信息及文件属性    IMAGE_OPTIONAL_HEADER32 OptionalHeader;}IMAGE_NT_HEADERS;//IMAGE_FILE_HEADERtypedef struct{    WORD     Machine;//标识机器平台,如x86    WORD     NumberOfSection;//该PE文件的段数目    WORD     TimeDataStamp;//PE文件的时间戳    DWORD    PointerToSymbolTable;//指向符号表的指针    DWORD    NumberOfSymbols;//PE文件的符号总数    WORD     SizeOfOptionalHeader;//可选头optionalHeader的size    WORD     Characteristics;//主要用来标识当前文件是可执行文件还是DLL文件,其余各位也各有定义}IMAGE_FILE_HEADER;//IMAGE_OPTIONAL_HEADER32typedef struct{    WORD     Magic;    BYTE     MajorLinkerVersion;    BYTE     MinorLinkerVersion;    DWORD    SizeOfCode;    DWORD    SizeOfInitializedData;    DWORD    SizeOfUninitializedData;    DWORD    AddressOfEntryPoint;//PE文件的执行入口地址,如果是静态链接,一般是main(),如果是动态链接,则入口地址是动态链接器代码的地址,是一个RVA地址,HOOK钩子常修改这里    DWORD    BaseOfCode;//.code段的起始地址    DWORD    BaseOfData;//.data段的起始地址    DWORD    ImageBase;//PE文件的优先装载基地址,.exe默认是0x0040 0000,.dll默认是0x1000 0000    DWORD    SectionAlignment;//文件在内存中的各段的对齐粒度,如4096(0x1000h),则代表在内存中,每一段的其实地址都是4096的整数倍    DWORD    FileAlignment;//文件在磁盘的对齐格式,一般是磁盘块的大小,如512 B    ...    DWORD    SizeOfImage;    DWORD    SizeOfHeaders;//所有的头如DOS HEADER/DOS stub/PE HEADER + SectionTable的大小,即除去正经段的头部大小,一般用来定位第一个section的位置,如.data    DWORD    CheckSum;    ...    DWORD    NumberOfRvaAndSizes;//在IMAGE_NT_HEADERS结构的末尾是一个IMAGE_DATA_DIRECTORY结构数组。此域包含了这个数组的元素个数。自从最早的Windows NT发布以来这个域的值一直是16。    IMAGE_DATA_DIRECTORY  DataDirectory;//记录了16个重要的数据的结构数组,16个IMAGE_DATA_DIRECTORY结构,每个都对应一个重要的信息,如符号导入导出表、文件打开列表}IMAGE_OPTIONAL_HEADER32;//IMAGE_DATA_DIRECTORY对应一个重要的数据信息typedef struct{    DWORD   VirtualAddress;//对应数据结构的RVA地址    DWORD   iSize;//对应数据的大小}IMAGE_DATA_DIRECTORY;/* 16个重要数据结构列举如下 1. IMAGE_DIRECTORY_ENTRY_EXPORT //导出表http://blog.csdn.net/obuyiseng/article/details/502316772. IMAGE_DIRECTORY_ENTRY_IMPORT //导入表,是一个IMAGE_IMPORT_DESCRIPTOR的结构体数组,//每个IMAGE_IMPORT_DESCRIPTOR对应一个被导入的DLL,这个结构体定义在“Winnt.h"3. IMAGE_DIRECTORY_ENTRY_RESOURCE//资源目录http://blog.csdn.net/obuyiseng/article/details/502606714. IMAGE_DIRECTORY_ENTRY_EXCEPTION//异常目录5. IMAGE_DIRECTORY_ENTRY_SECURITY//安全目录6. IMAGE_DIRECTORY_ENTRY_BASERELOC//重定位基本信息http://blog.csdn.net/obuyiseng/article/details/502613057. IMAGE_DIRECTORY_ENTRY_DEBUG//调试目录8. IMAGE_DIRECTORY_ENTRY_COPYRIGHT//描述字串9. IMAGE_DIRECTORY_ENTRY_GLOBALPTR//机器值10. IMAGE_DIRECTORY_ENTRY_TLS //Thread Local Storage线程局部存储TLS目录11. IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG//载入配置目录12. IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT//绑定输入表13. IMAGE_DIRECTORY_ENTRY_IAT //导入地址数组Import Address Table,属于导入表的重要组成部分,//IAT中每个元素对应一个被导入的符号,在没有重定位或符号解析之前,IAT中的元素值表示相对应的导入符号的序号或//符号名,当重定位和符号解析完成后,IAT中元素值将被改写成符号的真正地址,和ELF文件结构下GOT相似14. IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT  //延迟导入,用于启动加速15. IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR//COM信息16. IMAGE_DIRECTORY_ENTRY_ENTRIES*///IMAGE_IMPORT_DESCRIPTORS对应一个被导入的DLL的导入符号信息typedef struct{    DWORD  OriginalFirstThunk;//指向INT    DWORD  TimeDataStamp;    DWORD  ForwarderChain;    DWORD  Name;//绑定的DLL文件名字    DWORD  FirstThunk; //指向IAT}IMAGE_IMPORT_DESCRIPTORS;// IMAGE_EXPORT_DIRECTORYM对应DLL文件的导出符号信息typedef struct _IMAGE_EXPORT_DIRECTORY {          DWORD Characteristics;          //保留 总是定义为0          DWORD TimeDateStamp;            //文件生成时间          WORD  MajorVersion;             //主版本号 一般不赋值          WORD  MinorVersion;             //次版本号 一般不赋值          DWORD Name;                     //模块的真实名称          DWORD Base;                     //索引基数 加上序数就是函数地址数组的索引值          DWORD NumberOfFunctions;        //地址表中导出函数符号的个数          DWORD NumberOfNames;            //名称表的个数          DWORD AddressOfFunctions;       //输出函数地址数组的RVA          DWORD AddressOfNames;           //输出函数名字数组的RVA          DWORD AddressOfNameOrdinals;    //输出函数序号数组的RVA  } IMAGE_EXPORT_DIRECTORYM;   
DataDirectory中项目 定义 IMAGE_DIRECTORY_ENTRY_EXPORT 指向导出表(一个IMAGE_EXPORT_DIRECTORY结构)。 IMAGE_DIRECTORY_ENTRY_IMPORT 指向导入表(一个IMAGE_IMPORT_DESCRIPTOR结构数组)。 IMAGE_DIRECTORY_ENTRY_RESOURCE 指向资源(一个IMAGE_RESOURCE_DIRECTORY结构。是PE文件结构下最为重要且难懂的地方。 IMAGE_DIRECTORY_ENTRY_EXCEPTION 指向异常处理表(一个IMAGE_RUNTIME_FUNCTION_ENTRY结构数组)。CPU特定的并且基于表的异常处理。用于除x86之外的其它CPU上。 IMAGE_DIRECTORY_ENTRY_SECURITY 指向一个WIN_CERTIFICATE结构的列表,它定义在WinTrust.H中。不会被映射到内存中。因此,VirtualAddress域是一个文件偏移,而不是一个RVA。 IMAGE_DIRECTORY_ENTRY_RESOURCE 指向资源(一个IMAGE_RESOURCE_DIRECTORY结构。是PE文件结构下最为重要且难懂的地方。 IMAGE_DIRECTORY_ENTRY_BASERELOC 指向基址重定位信息。 IMAGE_DIRECTORY_ENTRY_DEBUG 指向一个IMAGE_DEBUG_DIRECTORY结构数组,其中每个结构描述了映像的一些调试信息。早期的Borland链接器设置这个IMAGE_DATA_DIRECTORY结构的Size域为结构的数目,而不是字节大小。要得到IMAGE_DEBUG_DIRECTORY结构的数目,用IMAGE_DEBUG_DIRECTORY 的大小除以这个Size域。 IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 指向特定架构数据,它是一个IMAGE_ARCHITECTURE_HEADER结构数组。不用于x86或IA-64,但看来已用于DEC/Compaq Alpha。 IMAGE_DIRECTORY_ENTRY_GLOBALPTR 在某些架构体系上VirtualAddress域是一个RVA,被用来作为全局指针(gp)。不用于x86,而用于IA-64。Size域没有被使用。参见2000年11月的Under The Hood 专栏可得到关于IA-64 gp的更多信息。 IMAGE_DIRECTORY_ENTRY_TLS 指向线程局部存储初始化段。 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 指向一个IMAGE_LOAD_CONFIG_DIRECTORY结构。IMAGE_LOAD_CONFIG_ DIRECTORY中的信息是特定于Windows NT、Windows 2000和 Windows XP的(例如 GlobalFlag 值)。要把这个结构放到你的可执行文件中,你必须用名字__load_config_used 定义一个全局结构,类型是IMAGE_LOAD_CONFIG_ DIRECTORY。 对于非x86的其它体系,符号名是_load_config_used (只有一个下划线)。如果你确实要包含一个IMAGE_LOAD_CONFIG_DIRECTORY,那么在 C++ 中要得到正确的名字比较棘手。链接器看到的符号名必须是__load_config_used (两个下划线)。C++ 编译器会在全局符号前加一个下划线。另外,它还用类型信息修饰全局符号名。因此,要使一切正常,在 C++ 中就必须像下面这样使用: extern “C” IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {…} IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 指向一个 IMAGE_BOUND_IMPORT_DESCRIPTOR结构数组,对应于这个映像绑定的每个DLL。数组元素中的时间戳允许加载器快速判断绑定是否是新的。如果不是,加载器忽略绑定信息并且按正常方式解决导入API。 IMAGE_DIRECTORY_ENTRY_IAT 指向第一个导入地址表(IAT)的开始位置。对应于每个被导入DLL的IAT都连续地排列在内存中。Size域指出了所有IAT的总的大小。在写入导入函数的地址时加载器使用这个地址和Size域指定的大小临时地标记IAT为可读写。 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 指向延迟加载信息,它是一个CImgDelayDescr结构数组,定义在Visual C++的头文件DELAYIMP.H中。延迟加载的DLL直到对它们中的API进行第一次调用发生时才会被装入。Windows中并没有关于延迟加载DLL的知识,认识到这一点很重要。延迟加载的特征完全是由链接器和运行时库实现的。 IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 在最近更新的系统头文件中这个值已被改名为IMAGE_DIRECTORY_ENTRY_ COMHEADER。它指向可执行文件中.NET信息的最高级别信息,包括元数据。这个信息是一个IMAGE_COR20_HEADER结构。

6.DLL文件的符号导出表

和ELF文件结构下的.dynsym意义相同,对于DLL文件,其在被加载时,显然需要有个集中存放导出符号的段或数据结构来供链接器快速收集当前DLL的导出信息。COFF PE文件结构中,这些导出符号被放在文件的导出表中。导出表提供一个符号名和符号地址的映射关系,即可以通过符号名查找该符号对应的变量或函数的具体地址。

// IMAGE_EXPORT_DIRECTORYM对应DLL文件的导出符号信息typedef struct _IMAGE_EXPORT_DIRECTORY {          DWORD Characteristics;          //保留 总是定义为0          DWORD TimeDateStamp;            //文件生成时间          WORD  MajorVersion;             //主版本号 一般不赋值          WORD  MinorVersion;             //次版本号 一般不赋值          DWORD Name;                     //模块的真实名称          DWORD Base;                     //索引基数 加上序数就是函数地址数组的索引值          DWORD NumberOfFunctions;        //地址表中导出函数符号的个数          DWORD NumberOfNames;            //名称表的个数          DWORD AddressOfFunctions;       //导出地址表的首地址(RVA)          DWORD AddressOfNames;           //导出符号名表的首地址(RVA)         DWORD AddressOfNameOrdinals;    //名字序号对应表的首地址(RVA) } IMAGE_EXPORT_DIRECTORYM;   

可以看到对于DLL的导出表中有三个数组是最为核心的结构,分别是导出地址表(EAT,Export Address Table)、符号名表(Name Table)和名字序号对应表(Name-Ordinal Table)。导出地址表EAT对应的是DLL各导出函数的RVA地址,符号名表存储的则是DLL各导出函数名,序号表则是和符号名表一一对应,用以指明相应的函数名的RVA地址在EAT数组中的下标。

7.PE文件的符号导入表

一个DLL文件只有一个导出表(.exe可执行文件不存在导出符号),但是一个PE文件可能依赖多个文件(从多个DLL文件中导入符号),所以要为每个依赖文件单独弄一份导入符号集合,再将所有依赖文件的导入符号集合集中在一起便成了PE文件的导入表。在PE文件中,记录每个依赖文件的导入符号信息的是一个IMAGE_IMPORT_DESCRIPTOR结构体,_IMAGE_IMPORT_DIRECTOTY_ENTRY指向的便是一个该结构体的数组。

//IMAGE_IMPORT_DESCRIPTORS对应一个被导入的DLL的导入符号信息typedef struct{    DWORD  OriginalFirstThunk;//指向INT(Import Name Table),和IAT一致,但是涉及到DLL优化,暂且可以忽略    DWORD  TimeDataStamp;    DWORD  ForwarderChain;    DWORD  Name;//绑定的DLL文件名字    DWORD  FirstThunk; //指向IAT导入地址数组(IAT, Import Address Table)}IMAGE_IMPORT_DESCRIPTORS;

IAT中每个元素对应一个被导入的符号,在没有重定位或符号解析之前,IAT中的元素值表示相对应的导入符号的序号或符号名,当重定位和符号解析完成后,IAT中元素值将被改写成符号的真正地址,和ELF文件结构下GOT功能相似。

IAT(INT)的元素为IMAGE_THUNK_DATA32结构,而其指向为IMAGE_IMPORT_BY_NAME结构,这两个结构的定义如下。

typedef struct _IMAGE_THUNK_DATA32 {    union {        DWORD ForwarderString;      // PBYTE         DWORD Function;             // PDWORD        DWORD Ordinal;        DWORD AddressOfData;        //RVA 指向_IMAGE_IMPORT_BY_NAME     } u1; //联合体,对于32位PE来说,来说如果最高位为1,那么低31位则直接就是导入符号的序号值;如果没有,那么这个IMAGE_THUNK_DATA32启用的是AddressOfData,其指向一个_IMAGE_IMPORT_BY_NAME结构体} IMAGE_THUNK_DATA32;typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;typedef struct _IMAGE_IMPORT_BY_NAME {    WORD    Hint;       //可能为0,编译器决定,如果不为0,是函数在导出表中的序号    BYTE    Name[1];    //函数名称,以0结尾,由于不知道到底多长,所以干脆只给出第一个字符,找到0结束} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

对于32位PE来说,来说如果最高位为1,那么低31位则直接就是导入符号的序号值;如果没有,那么这个IMAGE_THUNK_DATA32启用的是AddressOfData,其指向一个_IMAGE_IMPORT_BY_NAME结构体。使用_IMAGE_IMPORT_BY_NAME结构体时,先根据Hint值去往相应DLL导出表中查找是否对应的符号名是所需的符号,如果不是,则需要按照符号名去符号名数组中二分查找(符号名数组在收集导出符号做了预处理,按照字典序进行排列,所以可以进行二分查找)。

Fig.1 COFF PE文件结构下的符号导入导出表对应关系

Q: 在导出表中可以看到函数名序号数组的存在其实蛮多余的,明明可以通过函数符号名表和EAT地址表一一对应来解决问题,却非要通过一个序号表中转下,这是为什么?
A: 有很多说法,但核心还是考虑DLL兼容性。在Windows系统还是16bits的年代,显然保留导出函数的函数名数组是一件极为奢侈的事情,故而出于节省空间的考虑,将DLL导出函数符号分配唯一的序号用以代表,从而在完成重定位的任务下,也可尽可能地节省内存占用。如下面.def模块脚本的内容,为各导出函数手动绑定序号。

LIBRARY  Math    EXPORTS    Add   @1    Sub   @2    Mul   @3    Div   @4  NONAME

使用序号虽好,可一旦发生函数变更增减,则需要再次手动更新一遍函数序号,但这也会影响到使用老版本DLL的程序的中函数调用,因为这种强绑定关系导致采用序号机制的DLL升级较为繁琐。后来随着计算机内存的增加,自然保留导出函数名成为主流选择。而为了兼容性考虑,序号机制这一历史便依旧被传承下来了。

7.DLL显式运行时加载链接demo

#include <windows.h>#include <stdio.h>typedef double (*Func) (double, double);int main(int argc, char **argv){    Func function;    double result;    //load DLL using Library()    HINSTANCE hinstLib = LoadLibrary("Math.dll");    if (hinstLib == NULL) {        printf("ERROR: unable to load DLL\n");        return -1;    }    //DLL装载成功,则直接通过GetProcAddress()获取目标函数符号的地址    function = (Func) GetProcAddress(hinstLib, "Add");    if(function == NULL) {        printf("ERROR: Unable to find target function\n");        FreeLibrary(hinstLib);        return 1;    }    result = function(1.0, 2.0);    printf("Result = %f\n", result);    FreeLibrary(hinstLib);    return 0;}