调用DLL早不是什么新话题了。从LoadLibrary和GetProcAddress配合使用的动态加载,到链接导入LIB的静态加载,方法很多。LoadLibrary和GetProcAddress的方法虽然麻烦但是很通用,毕竟调用系统API就可以了,DLL名和函数名和都是字符串形式。导入LIB方式的话,生成DLL的时候会带一个LIB给你,拿来用即可。
后一种方法很方便。这里主要也是讲后一种。
LIB链接的方式方便是方便,但是前提是要有LIB。对于EXE/DLL来说,LIB并不是必需的。那么就存在这种情况:手头只有一个DLL没有LIB,这要如何使用?
LoadLibrary/GetProcAddress固然可以,但是麻烦。要想不麻烦,就是导入的方式。从DEF做LIB是一个常规考虑的方式,但是这种方式有一个问题,对于导出名只有函数名的DLL,最好的例子就是Windows API那种,用DEF来做LIB就会造成链接的时候链不上:链接器去找__imp__Sleep@4,而LIB里的却是Sleep。DEF文件在做DLL的时候提供导出表的时候,可以指定别名。但是,在做导入LIB的时候,它不认那个别名了,于是DLL里导出什么符号就必须链接到什么符号否则就链不上。对于__stdcall的函数来说,简直无法使用。
于是我就想到一个方式,自己做一个这样的LIB来导入。
网上能找到现成的、最接近需求的工具是IMPLIB SDK( http://implib.sourceforge.net/ ),它是用FASM语法写的脚本,配合FASM编译器能直接生成导入用的LIB。我本来也一直在用它,但是到后来我发现这东西有个问题,DLL的文件名长了它就死了。好像只支持8.3的样子,这也太人品了吧……
除了这个,网上我也就再也没找到这个功能的东西了。囧
有自己做一个这样的工具的想法很久了。一直到了今天在真正算是出来。之前有尝试过一两次,不过是失败的……
最关键的参考资料是这个:Microsoft PE and COFF Specification
最初我是想直接做LIB的,不过遇到了点困难。经过Advance指点,就改为从COFF格式的OBJ下手了。本来想直接做LIB的原因是,这参考资料上提到LIB有一种特殊的import的格式,那么我直接拿来用就好了。但是后来手工修改LIB实验的时候,证实到头来还是需要自己构造导入表什么的。于是就放弃了,改为从OBJ下手了。
这个图是那个参考资料里原原本本的。可以看出,它的格式是首先一个文件头,然后所有Section的头,然后数据经过研究,COFF Header实际上是winnt.h里的IMAGE_FILE_HEADER,可以直接拿来用。然后Section Header实际上是IMAGE_SECTION_HEADER。直接从winnt.h里面引用就省去了按照那个参考资料里自己写结构体的麻烦。从图上还有两个地方没很好表示出来,一个是符号表,一个是字符串表。这两个东西合起来表示这个OBJ文件中的符号(包括内部和外部的,内部的就比如指向FirstThunk的(RVA值),链接的时候可以内部消化,外部的就比如__imp__这种给别人引用的)。其中符号的类型是IMAGE_SYMBOL,这个类型的数组就是符号表。符号的数目在IMAGE_FILE_HEADER里有,符号表的文件指针(所谓文件指针就是在文件的第几个字节位置,不是只PE加载器加载之后的,是原始文件)也在这个结构体里。
然后是Section的格式……所有Section的头在一起,数据在另外的一起。就是说,它是 头 头 头 数据 数据 数据 这个样子,而不是 头 数据 头 数据 头 数据。
Section的原始数据后面含有一个重定位表。重定位表的数据结构是IMAGE_RELOCATION,因为OBJ文件被链接之后,符号的位置才确定的。所以要先声明好哪些地方要重定位,比如这个语句
extern int add(int, int);
printf("%d", add(1,2));
在这个调用中,add的函数地址其实还不知道的,编译出来的OBJ里面,这个CALL语句的参数(四个字节)就先预留着,等到链接的时候链接器根据这个重定位表把数值写进去。
于是结构简明地写,用类似正则表达式的语法,就是这样:
IMAGE_FILE_HEADER → IMAGE_SECTION_HEADER+ → (Raw Data → IMAGE_RELOCATION+)+ → IMAGE_SYMBOL+ → String Table
String Table是用来存放符号表里提到的符号的。符号表里本身没有保存符号(字符串),而是保存了这个符号在字符串表的什么地方这样的信息。所以说这两个要配合起来用。重定位表里保存的关于什么符号要填入哪里的,是指向符号表的某个元素。
所以引用大致是,重定位表引用符号表的符号,符号表引用字符串表里的字符串。
知道了OBJ的格式,然后要弄清为什么可以通过链接LIB的方式来实现导入。
已经知道了EXE中导入表的相关知识的基础上,OBJ是通过一种特定的方式来实现在链接的时候构造导入表的:导入表保存在.idata节中,链接器链接的时候有一种规则,就是对于节的名字带有 $ 符号的,会根据 $ 后面的数字决定链接之后的数据的排列顺序。
用 link /dump /all xxx.lib 把VC生成的LIB文件的信息给DUMP出来,结合它提供的符号,可以大致猜出它以什么顺序组装导入表:
.idata$2 : IMAGE_IMPORT_DESCRIPTOR 结构体
.idata$3 : __NULL_IMPORT_DESCRIPTOR
.idata$4 : OriginalFirstThunk
.idata$5 : FirstThunk
.idata$6 : DLL的文件名,0结尾的字符串
这样就组成了导入表一开始是IMAGE_IMPORT_DESCRIPTOR结构体数组,然后一个__NULL_IMPORT_DESCRIPTOR,这样的结构。为了保证__NULL_IMPORT_DESCRIPTOR只有一个,应该是一个单独的OBJ里面放这个Section,最后把OBJ丢到LIB里。为了保证它能链接,要让它导出一个什么符号来其他的OBJ以EXTERNAL方式引用……DLL文件名应该也是同理。
我认为.idata$3应该用了类似__declspec(selectany)的方法,否则每个导入库都有这样一坨0的话,链接以后.idata$3就会有一堆的全0导入描述表而不是只有一个。虽然不知道到底是怎么回事,总之在定义的时候不需要特意加个COMDAT做成selectany。
至于$4和$5结尾的0,暂时的想法是在所有OBJ中都添加NULL_THUNK的引用,然后单独的文件里放一个这个NULL_THUNK。链接的时候可能就会被放在最后了?前提是链接器在查找符号的时候是广度优先而不是深度优先。广度优先的话,只要这些导入OBJ里没有奇怪的互相引用,那么就能保证NULL_THUNK在最后。最后实际做的时候,感觉它好像只是按照lib里object出现顺序来搞的,不确定。总之我是放在最后一个成员里。
按这样的方法,.idata$2就聚集了所有导入库的描述符,然后.idata$3结尾,然后第一个导入库的.idata$4,第二个的,第三个的,……每个导入库的.idata$4都要有结尾的0。.idata$5同理。
于是接下来是LookUp Table,也就是IMAGE_IMPORT_BY_NAME,这下我是根本找不到了……怀疑是那种0x0000 0xffff开头的LIB文件里的特殊OBJ会自己生成。但是放哪里,不知道了。我就先暂时放.idata$7去。
至于 _Sleep@4 : jmp [__imp__Sleep@4] 这种,可以自己生成一个.text段的,反正只有一句jmp,查一下很容易就知道。也可以不生成,反正声明了 __declspec(import) 以后就自动链接到 __imp__xxxx@nn 去了。
关于导入的参考资料,这里 http://www.microsoft.com/msj/0498/hood0498.aspx
对于64位的情况,目前通过winnt.h头文件观察到的,除了thunk的大小变了以外,就是表示cpu architecture的标识变了。其他好像没有变。
结果一直没有进展,到了1月24日,决定不模仿VC和GCC了,改为模仿IMPLIB SDK,发现了一点玄机。
首先,NULLTHUNK的名字最开头那个字符,其实是固定的,写作\177可能看不出来但是写作\x7f的话……有符号char中的最大值。猜测是为了排在最后。
然后,符号表里section的定义一定要在前,特别是COMDAT的section。否则生成lib的时候给你出fatal error LNK1143。
尝试了这么多次,搞不清,给implib sdk的作者发了个邮件,不知道对方能不能收到会不会回复……
1月25日的时候,抱着试试看的心理,把implib sdk生成的lib里的obj都手工拆解出来,然后再用lib工具生成lib文件,赫然发现,和我做的lib是一样的效果了……无法正常链接。这下好了,原因大概猜到了,问题出在lib生成那边,不是出在obj生成那边。因为implib sdk生成的lib文件里obj的名字都一样,于是我也弄成一样,好了可以链接了,淦……
于是结论,导入库中object的名字要保持全部一样,不然会不正常。这样的话就是最后生成lib的步骤也要自己写了,不好用link工具了。
还有就是,lib里的object文件,貌似文件开头都是对齐到2的整数倍字节的。如果不是,那么会加入0x0a作为pad,定义为IMAGE_ARCHIVE_PAD。
lib文件中的FirstLinkMember和SecondLinkMember,貌似First不能完全省掉。最后还是耐着性子把FirstLinkMember也填上了。这两个,前者要求offset是递增序列的,后者offset要递增、字符串也要递增。排序一下就是了。
然后到了今天下午终于做出来了。累不爱。
下载:
点击下载: 点我