1.原始的链接方法

在讲静态链接与动态链接前,先看看原始的链接方法。比如编译如下代码:

假设printf的代码存在 printf.c,scanf的代码存在scanf.c,翻译之后变为可重定位模块.o

link0

这种链接方式有几个问题:

  • 每次链接都要显式的列出所有需要的可重定位模块.o,过程繁琐。
  • 不同程序若需要使用相同的模块,则需将该模块分别链接至不同的程序,浪费磁盘空间。

为了解决第一个问题,就有了静态库的概念:

2.静态链接

静态库的核心思想是:将不同的可重定位模块打包成一个文件,在链接的时候会自动从这个文件中抽取出用到的模块。这样就无需手动列出需要用的模块,方便了链接的过程。

比如用静态链接的方式来编译上面的那段test.c(自动链接libc.a等静态库)

link2

生成的可执行文件中包含了完整的底层实现printf,scanf的代码,所以生成的文件要比源文件大的多。

可以看到,静态库的出现解决了第一个问题,但是仍然存在相当大的空间浪费,为了解决这个问题,动态库就出现了:

3.动态链接

动态链接的核心思想是:代码共享和延迟绑定。 代码共享依靠虚拟存储器实现,延迟绑定的核心在于两张表:PLT(Procedure Linkage Table)和GOT(Global Offset Table)。基于虚拟存储器的代码共享使得在内存中只存在某个模块的唯一一份代码,通过虚拟存储器的内存映射机制将物理地址空间中的代码映射到不同进程的虚拟地址空间中(如下图)。

link3

那进程又该如何确定什么时候将哪些模块映射到虚拟地址空间中呢?PLT表和GOT表配合解决了这个问题。PLT在代码区(.text),GOT在数据区(.data),以上面的test.c动态链接生成的可执行文件为例,用objdump-D 命令生成动态链接后生成可执行文件的反汇编代码:

plt0

在调用scanf时,其实并不是直接调用scanf的代码,而是跳转到了848360的地址,这个地址中存的即是scanf函数在PLT表中代码:

plt1

scanf的这个PLT项中存了3句代码,第一句是个间接跳转,跳转的实际地址为0x804a018(存着GOT中scanf函数的实际地址)中存的4字节内容:

plt3

这四个字节的内容为0x08048366(小顶端存储。右边的反汇编代码没用,因为这是数据区,objdump也把它当成指令翻译了- -!)。刚好这个指令是PLT表的下一条指令,即push指令,接下来又是一个jmp,这两句代码的作用是找到scanf的真实地址并修改GOT表中的scanf项,将其改成scanf的真实地址。

理一理思路,第一次来到scanf的PLT中并没有直接跳转到scanf的代码部分,因为这时候scanf的代码还没有被映射到进程的虚拟地址空间,所以要调用PLT后面的代码来将scanf的代码加载进来,然后将GOT中scanf项改成scanf的真实地址,当第二次调用scanf时,通过执行 jmp *0x804a018就能直接跳转到scanf的真实地址。第一次跳转到PLT中的情况如下图:

plt4

第二次跳转到PLT中的情况如下图:

plt5

第一次跳转到PLT中一定不能找到函数的真实地址,这种推迟找到真实地址的机制就叫做延迟绑定。能为什么不一次就找到,非得等到第二次呢?这种延迟绑定机制使得可执行文件中不需存储其他模块的东西,这就使得动态链接的文件不仅比静态链接的小的多,而且一个模块只需在内存中存储一次,大大提高了内存的利用率。

*

+
跳转到评论