ise生成msk文件的用处_动态链接编译可执行文件时.so/.lib文件的用处以及ELF与PE文件的区别...

前言

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

你可能感兴趣的:(ise生成msk文件的用处)