链接过程: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;
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导出表中查找是否对应的符号名是所需的符号,如果不是,则需要按照符号名去符号名数组中二分查找(符号名数组在收集导出符号做了预处理,按照字典序进行排列,所以可以进行二分查找)。
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;}
- 链接过程:Windows下动态链接
- Windows下动态链接
- Windows下的动态链接
- 编译器链接过程 静态链接 动态链接
- windows下动态链接库(dll)
- Windows下动态链接库的使用
- Windows下查看动态链接库接口
- windows下动态链接库(讲解)
- 动态链接执行过程
- windows动态链接库
- windows 动态链接库
- windows动态链接库
- windows 动态链接库
- Linux下动态链接
- linux下动态链接库的加载及解析过程
- Linux下编写动态链接库的简单过程
- Linux下库函数动态链接过程分析(转)
- Linux下编写动态链接库的简单过程
- 背包问题及其空间优化
- 怎么写好文档
- 33. Search in Rotated Sorted Array
- 【C++面向对象程序设计】20170527银行系统
- 综合问题文章01
- 链接过程:Windows下动态链接
- 第七届蓝桥杯——第1题(年龄问题)
- CodeForces 876B
- 可达性分析算法
- 有名管道通信
- 数据结构实验之栈与队列二:一般算术表达式转换成后缀式
- python2 与Python3
- oracle学习之sql1999语法
- 批处理文件