前言
Linux下编译动态链接文件会生成.so文件,而编译可执行文件时也要带上此.so文件一起;Windows下编译动态链接文件会生成.dll和.lib文件,编译可执行文件时需要带上.lib文件一起。本文主要介绍为什么可执行文件编译时需要带上.so/.lib文件,以及在这个角度下PE与ELF的一些区别。
本文以调用动态链接库中函数为例来说明。
为什么需要.so/.lib文件
举个例子,对于如下的两个文件:
Lib.c独自编译为动态链接库,Main.c编译为可执行文件,其中main()函数调用了Lib.c中的foo()。注意我们说main()调用了foo(),只是我们期望的结果,那程序怎么去寻找呢?显然我们独自编译Main.c是不可能找到foo()的,整个编译过程都没有Lib.c的存在,上哪找?事实上编译器也不会让Main.c编译通过。
所以编译时至少需要两个信息:函数定义在哪:在动态链接库中还是未定义;
动态链接库文件名是什么:可执行文件运行时可以加载对应的动态链接库。
正是因为需要这些信息,在编译生成使用到动态链接库的可执行文件的时候,才需要.so/.lib文件的参与:用来提供这些信息。下面我们来仔细看看编译时的一些细节问题,.so文件和.lib文件分别对应Linux下的ELF格式和Windows下的PE格式,我们分开讨论。
ELF
示例
Program.c:
Lib.c:
将Lib.c编译为共享对象,生成Lib.so:
编译Program.c,生成Program.o:
链接Program.o和Lib.so,生成可执行文件Program:
重定位
编译生成的Program.o并不知道foobar()的位置,链接时怎么发现foobar()的呢?在ELF下,共享对象(.so文件)的所有全局函数和全局变量都是默认导出的,保存在动态符号表.symtab段中。链接时通过查看Lib.so中的动态符号表,就能发现foobar(),即由共享对象定义。此时在可执行文件中创建GOT和PLT,并将foobar()加入其中,这里就不展开讲了。
对foobar()具体是如何重定位的?我们从汇编的层次来看。先看看重定位前的Program.o:
注意看e行,即调用foobar(),其二进制指令为e8 00 00 00 00,其中e8表示相对地址调用指令即call,其后的4个字节为目的地址相对于当前指令的下一条指令的偏移。显然这里的00 00 00 00是指foobar()的地址还未知,需要重定位。
看看重定位后的Program:
注意看400694,重定位后foobar()的地址已经确定,其相对偏移为c7 fe ff ff,我们看汇编代码给的注释是foobar@plt,即重定位到了PLT。这样运行时调用foobar()就跳转到PLT再跳转到GOT,最后跳转到foobar()的真实地址,这里不展开讲了。
装载动态链接库
可执行文件如何知道装载哪个共享对象?如果链接时同时给出了一些不相关的.so文件,那么可执行文件运行时会把这些.so文件全都装载吗?显然不会,可执行文件只会装载需要的共享对象,即用到了其函数或变量的共享对象,在这里就是Lib.so了。那么可执行文件是怎么记录的?我们来看看Program的.dynamic段:
.dynamic段中记录了Lib.so,这个Lib.so来源当然就是链接时提供的啦!
仔细看.dynamic段是不是发现Lib.so的前面还有个路径标记./?ELF可执行文件会记录.so文件路径?我们把.so文件换个位置再链接看看
看看.dynamic段:
果然可执行文件记录的是共享对象的路径,而不是文件名。也就是说可执行文件运行时会根据.dynamic段中记录的路径去装载共享对象(即.so文件),如果此共享对象不存在,则会报错。
PE
示例
Program.c:
Lib.c:
将Lib.c编译为DLL,生成Lib.dll和Lib.lib:
编译Program.c,生成Program.obj:
链接Program.obj和Lib.lib,生成可执行文件Program.exe:
.lib文件是什么
注意到将Lib.c编译为DLL生成了Lib.dll和Lib.lib,其中Lib.dll是动态链接库文件,那么Lib.lib又是什么呢?
这里Lib.lib称为导入库(Import Library),虽然后缀和Windows下的静态链接库相同,但实际上是两个不一样的东西。导入库(如Lib.lib)并不包含源文件(如Lib.c)的代码和数据,仅用来描述DLL(如Lib.dll)的导出符号,并包含了一部分“桩(Stub)代码”。来看看Lib.lib到底有什么:
就是记录了导出的函数foobar(),以及DLL的名字Lib.dll。
这里还有一点要注意,DLL默认不导出任何符号(ELF默认导出所有符号),需要导出的符号需要显式声明;如这里Lib.c中通过__declspec(dllexport)声明将foobar()导出。
重定位
DLL中导出的符号会放在导出表(Export Table)中,并且会记录在导入库(.lib文件)中。链接时查看Lib.lib就能发现foobar(),即由DLL定义。此时在可执行文件中创建IAT,并将foobar()加入其中,这里就不展开讲了。
我们也通过汇编代码来看看是如何重定位的。Program.obj:
和ELF类似,00000005行中E8表示相对地址调用指令即call,00 00 00 00则表示foobar()的地址还未知,需要重定位。
看看重定位后的Program.exe:
这里和ELF不同了。注意看00401005行,重定位后相对偏移为08 00 00 00,即跳转到00401012,而00401012并不对应IAT,而是jmp dword ptr ds:[0040C108h],此行才对应跳转到IAT。
为什么要用一个中间跳转,不直接跳到IAT?因为PE对直接调用指令和间接调用指令是区分开的,一个是call,而另一个是jmp dword ptr xxxx(这里我也不太确定,如有错误还请指出),所以不能像ELF那样只用修改偏移地址,而需要另外一条指令。
中间跳转的指令从哪来?我们知道链接器一般不产生指令,那这里00401012行是从哪来的呢?答案是来自导入库,就是那个Lib.lib,这个指令又被称为桩代码。
这样运行时调用foobar()就首先跳转到桩代码,再跳转到IAT,最后跳转到foobar()的真实地址,这里不展开讲了。
注意:如果将需要用到的外部导入函数提前用_declspec(dllimport)声明,则目标文件的指令直接是jmp dword ptr xxxx仅需重定位,而不是call xxxx需要桩代码(文末参考中有相关链接)。
装载动态链接库
ELF在链接时就使用的动态链接库,因此而在可执行文件中记录下动态链接库的名字和路径。那么PE在链接时只用到了导入库.lib文件,可执行文件又是怎么记录到动态链接库的呢?
答案其实在之前就已经给出了,看看Lib.lib的内容:
对于每一个导出符号,导入库不仅记录的符号的名字,还记录了所在的DLL,像这里就是Lib.dll,甚至,还记录了版本号和时间戳(这个有其他的用途,这里不解释)。
在可执行文件中,这些信息则被记录到了导入表(Import Table)中,看看Program.exe的导入表:
看到Lib.dll了吧?可执行文件在运行时就会根据导入表中记录的DLL名字去寻找。我们在这里没有看到像ELF那样记录DLL路径的标记,那么PE是不是不会记录DLL的路径?答案是肯定的,PE不会记录DLL的路径。在寻找DLL时操作系统会按照预选确定的规则去寻找DLL,在可执行文件中只会记录DLL的名字(你可以做做实验验证一下,文末参考中也有MSDN对于搜索路径的规定)。
ELF和PE的主要区别
这里从动态链接库的角度总结一下ELF和PE的主要区别:ELF的动态链接库默认导出全部符号,而PE则默认全部不导出,需要显式声明;
重定位时,ELF只需要修正偏移地址,而PE则可能需要额外插入桩代码(这是由于PE对内部函数和外部函数调用的指令不同造成的);
可执行文件中,ELF会记录动态链接库的路径,运行时从记录的路径装载动态链接库;PE只会记录动态链接库名字等信息,运行时从预先设定的路径寻找动态链接库。
可否不用.so/.lib文件
链接时不使用.so/.lib文件也是可行的,不过这时动态链接库的加载方式也变了,不再是装载时加载而是运行时加载。这种技术称为显式运行时链接(Explicit Run-time Linking),让程序自己在运行时控制加载指定模块,并且可以在不需要该模块时将其卸载。
看看ELF和PE的例子。
ELF
ELF使用API:打开动态库:dlopen();
查找符号:dlsym();
错误处理:dlerror();
关闭动态库:dlclose()。
Program.c:
Lib.c:
gcc -fPIC -shared Lib.c -o Lib.so
gcc -c Program.c
gcc Program.o -ldl -o Program
./Program
result = 1
/Program.c/
include
typedef void (*Foobar)(int);
int main() {
HINSTANCE hinstLib = LoadLibrary("Lib.dll");
Foobar foobar = (Foobar)GetProcAddress(hinstLib, "foobar");
foobar(1);
FreeLibrary(hinstLib);
return 0;
}
/Lib.c/
include
define DllExport __declspec(dllexport)
DllExport void foobar(int i) {
printf("result = %dn", i);
}
cl /LD Lib.c
cl /c Program.c
link Program.obj
Program.exe
result = 1