`

第7章 动态链接

 
阅读更多

一、为什么要动态链接


 

       相比静态链接,动态链接的优势
       1. 节省磁盘和内存空间:静态链接中,磁盘和内存重要保存多份Lib.o的副本;而动态链接则只有一份。

       2. 方便程序开发和发布:静态链接中,如果一个程序的任何模块需要更新,整个程序需要重新链接,发布给用户;而动态链接则只需要发布变化了的模块。

       3. 内存中共享一个目标文件(Lib.o),可以增加CPU缓存的命中率

       4. 动态链接的程序在运行时可以动态地选择加载各种程序模块,被用来制作程序的插件。

 

       相比静态链接,动态链接的劣势

       1. DLL Hell:当程序依赖的某个模块更新后,由于新的模块与旧的模块之间接口不兼容,导致原有的程序无法运行;

       2. 运行性能损失:动态链接的程序每次被装载时都要重新进行链接,尽管采用了延迟绑定(Lazy Binding),依然会损失5%左右性能。

 

二、简单的动态链接例子

        动态链接编译Program1和Program2两个程序,我用一个图就总结书上这节的内容(我太厉害了!)

Lib.c

#include <stdio.h>
void foobar(int i){
    printf("printing from Lib.so %d\n",i);
}

Lib.h

#ifndef LIB_H
#define LIB_H

void foobar(int i);

#endif

 

Program1.c

#include "Lib.h"
int main(){
    foobar(1);
    return 0;
}

Program2.c

#include "Lib.h"
int main(){
    foobar(2);
    return 0;
}

 

看图:

 

 

三、地址无关代码

       1. 装载时重定位:

           1.1. 为什么要“装载时重定位”:可执行文件基本可以确定自己在进程虚拟地址空间的实际位置,因为可执行文件往往是第一个被加载的文件,他可以选择一个固定空闲的地址,比如Linux下一般都是0x08040000,Windows下一般都是0x0040000;与此不同,共享对象.so在编译时就不能假设自己在进程虚拟空间中的其实位置。如果操作系统本想把共享对象装载到进程虚拟空间的0x100这个地址,结果发现这个地址已经被别的程序占用了,则将装载地址调整到一个空闲的位置。总之,因为共享对象.so被装载到的位置(VMS中某地址)不能固定,所以要装载时重定位

 

gcc -shared 选项指定输出对象使用装载时重定位

 

       2. 地址无关代码PIC(Position-independent Code):

           2.1. 为什么要“地址无关代码”

           采用装载时重定位的方式存在一个问题就是相同的共享对象.so代码(不包括数据部分,数据部分每个进程都有自己的副本)不能在多个进程间共享(每个都将代码放到了它自己的进程虚拟地址空间中了),这就失去了动态链接库dll(或共享对象so)节省内存的优势。

           要解决这个问题,我们的目的很简单,希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。ELF中是这样实现的:在数据段中建立一个指向那些需要被使用的地址列表GOT(Global Offset Table全局偏移表),通过GOT相应的位置进行间接引用。

 

gcc -fPIC 选项指定输出对象使用地址无关方式

 

           2.2.怎样做到地址无关代码

           具体来看,如何将.so中代码段做成与绝对地址无关的——有4种情况。

static int a;
extern int b;
extern void ext();

void bar(){
    a=1; //情况二、内部数据访问
    b=2; //情况四、外部数据访问
}

void foo(){
    bar(); //情况一、内部函数调用
    ext(); //情况三、外部函数调用
}

 

情况一、模块内部调用和跳转:

情况二、模块内部数据访问:

              这两种情况不需要用到GOT,只需指定“内部数据”相对于下一条指令的偏移,因为模块在编译时就可以确定模块内部变量相对于当前指令的偏移。(Sam: 下面“情况一、情况二图”中.text和.data两个section相对位置应该不会变化,只是在装载时会根据VMS空闲情况整体调整他们两一起被装载的位置)

 

情况三、模块间数据访问:

情况四、模块间调用和跳转:

              在编译时就可以确定GOT相对于当前指令的偏移(这与情况一、二使用的方法一样),然后我们根据变量地址在GOT中的偏移就可以得到变量的地址,当然变量对应于GOT中哪个偏移是由编译器决定的。链接器在装载模块时会查找每个变量所在的地址,然后填充GOT中的每个项。由于GOT本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以有独立的副本,互不影响。

 



情况一、情况二、PIC(模块内)

 



 情况三、情况四、PIC(模块间)

 

 

四、延迟绑定 PLT

 

       1.1. 为什么要“延迟绑定PLT”:如果不用PLT,程序开始执行时候,动态链接器都要进行一次链接工作,动态链接器会寻找并加载所需要的共享对象,然后进行符号查找地址重定位等工作,势必减慢程序的启动速度。延迟绑定则在没用用到函数时不进行绑定,当函数第一次被用到时才进行绑定(符号查找、重定位等)。

 

区别.got   .rel.dyn  .got.plt   .rel.plt  :

        acess位置无关的数据redirect到.got中某项 <--> 延迟加载对应的重定位段  .rel.dyn
        acess位置无关的函数redirect到.got.plt中某项 <--> 延迟加载对应的重定位段  .rel.plt

 

       1.2.  延迟绑定PLT具体实现

.PLT0:
push *(.got.plt+4)      #本模块的ID
jump *(.got.plt+8)      #_dl_runtime_resolve()函数的地址
 
.PLT1:              #==> 凡调用bar(),将跳转到.PLT1。
                    #(1) 如果已经重定位,则bar()的绝对地址已经被填充到got.plt的表项bar@got.plt中,就可以直接跳转到bar()
                    #(2) 如果尚未重定位,则got.plt的表项bar@got.plt中还是0,直接跳到下一条语句执行,压入bar()在重定位表”.rel.plt”中的下标,
                    #    压入本模块ID,
                    #    跳转到_dl_runtime_resolve()函数,去绑定函数bar()
jump *(bar@got.plt)
push offset_of_rel      #offset_of_rel是bar()在重定位表”.rel.plt”中的下标
jump .PLT0


延迟绑定的符号解析过程大概如下(以上面示例中的bar()为例):
    1.代码中调用bar()的地方在编译时已经被替换成一条调用bar@plt的转移指令,即是调用的”.plt”中的bar所在的项,即”.PLT1″。
    2.当首次解析的时候,虽然在”.got.plt”中有bar函数的项,但是在没有解析之前,该项存储的内容为0,而非bar函数的绝对地址,因此指令”jump *(bar@got.plt)”实际上没有任何的动作,只是直接跳转到下一条push指令(因为jump的偏移为0)。
    3.指令”push offset_of_rel”是将bar()函数的重定位项的索引压栈,而该重定位项的类型为R_386_JMP_SLOT,offset_of_rel是重定位表”.rel.plt”中的下标。
    4.接着指令”jump .PLT0″跳转到.PLT0,将”.got.plt”的第二表项压栈,然后再跳转到”.got.plt”第三个表项指定的地址。这里要简述一下”.got.plt”的结构,”.got.plt”和”.got”几乎是一样的,除了”.got.plt”前三项的值有特殊意义:(1)第一项保存”.dynamic”段的地址,这个段描述了本模块动态链接相关的信息(可暂忽略);(2)保存本模块的ID(用于在动态链接时查找本模块的信息);(3)保存的是_dl_runtime_resolve()函数的地址,用于启动动态链接器,加载模块,解析符号并重定位。。
    5.此时,可以看到在堆栈上,已经存有bar()函数在重定位项的索引和当前要重定位的模块ID,那么_dl_runtime_resolve()函数就可以启动动态链接器,当动态链接器得到控制后,它恢复堆栈,获取在步骤3中入栈的重定位项索引和步骤4中入栈的本模块信息,通过这2个信息,动态链接器符号解析和计算绝对地址,将bar()的“真实”地址存储于bar()在全局偏移表”.got.plt”的表项(原来的值为0)。
    6.动态链接器更新了”.got.plt”中的信息后,将执行的控制权传递给bar()函数。那么当再次调用bar()函数时,PC依然会跳转到”.PLT1″,但是此时”bar@got.plt”的值已经不再是0了,因为会正确地跳转到bar()函数的绝对地址

 

五、动态链接相关结构

 

.interp段:保存一个字符串(可执行文件所需动态链接器的路径)

.dynamic段:动态链接相关信息(i.e. 依赖哪些共享对象;动态链接符号表的位置;动态链接重定位表的位置等)

.dynsym段(动态符号表):类似.symtab段(符号表),与它不同的是.dynsym段只保存了动态链接相关的符号,对于那些模块内部的符号,比如模块私有变量则不保存。即,.dynsym是.symtab的一个子集。

 

动态链接重定位表

              .rel.dyn实际上是对数据引用的修正,它所修正的位置位于.got以及数据段;

              .rel.plt  是对函数引用的修正,它所修正的位置位于.got.plt

 

 

六、动态链接步骤和实现

       1. 动态链接器自举

       2. 装载共享对象

       3. 重定位和初始化

 

Linux下的动态链接器是: /lib/ld-linux.so.2 ,它是 /lib/ld-x.y.z.so的软链接。

 

七、显式运行时链接

 

 

参考资料:

1. 编译,链接相关的问题。-fPIC ,地址无关代码,等等  http://www.verydemo.com/demo_c116_i3904.html

2. Dynamic Linker   http://ytliu.github.com/blog/2012/12/15/dynamic-linker/

 

相关参考文章和文章用到的图片 见附件

 

-------------------------------------------------------------------------------------------------------------------------------------

如何理解“地址无关代码”:

Sam:本质就是不要绝对跳转,要跳转也是相对PC的相对跳转

    首先,需要理解加载域与运行域的概念。加载域是代码存放的地址,运行域是代码运行时的地址。为什么会产生这2个概念?这2个概念的实质意义又是什么呢?
     在一些场合,一些代码并不在储存这部分代码的地址上执行地址,比如说,放在norflash中的代码可能最终是放在RAM中运行,那么中norflash中的地址就是加载域,而在RAM中的地址就是运行域。
     在汇编代码中我们常常会看到一些跳转指令,比如说b、bl等,这些指令后面是一个相对地址而不是绝对地址,比如说b main,这个指令应该怎么理解呢?main这里究竟是一个什么东西呢?这时候就需要涉及到链接地址的概念了,链接地址实际上就是链接器对代码中的变量名、函数名等东西进行一个地址的编排,赋予这些抽象的东西一个地址,然后在程序中访问这些变量名、函数名就是在访问一些地址。一般所说的链接地址都是指链接这些代码的起始地址,代码必须放在这个地址开始的地方才可以正常运行,否则的话当代码去访问、执行某个变量名、函数名对应地址上的代码时就会找不到,接着程序无疑就是跑飞。但是上面说的那个b main的情形有点特殊,b、bl等跳转指令并不是一个绝对跳转指令,而是一个相对跳转指令,什么意思呢?就是说,这个main标签最后得到的只并不是main被链接器编排后的绝对地址,而是main的绝对地址减去当前的这个指令的绝对地址所得到的值,也就是说b、bl访问到的是一个相对地址,不是绝对地址,因此,包括这个语句和main在内的代码段无论是否放在它的运行域这段代码都能正常运行。这就是所谓的位置无关代码。
     由上面的论述可以得知,如果你的这段代码需要实现位置无关,那么你就不能使用绝对寻址指令,否则的话就是位置有关了。

 

 

  • 大小: 43.9 KB
  • 大小: 75.1 KB
  • 大小: 38.6 KB
  • 大小: 51.2 KB
分享到:
评论

相关推荐

    第7章 链接2018.pdf

    第7章 链接2018.pdf

    QNX 完整架构文档 中文

    第7章动态链接 ............ 第8章资源管理 ............ 第9章文件系统 ............ 第10章PPS ............... 第11章字符I/O. .......... 第12章网络架构 ........... 第13章词汇表 .............

    第七章-链接1

    链接编译驱动程序静态链接符号解析:确定链接输入目标模块中代码节和数据节的大小重定位:确定每个符号的内存地址可重定位目标文件格式符号和符号表符号解析C++和Jav

    FluentUDF中文教程UDF第7章编译与链接[参照].pdf

    FluentUDF中文教程UDF第7章编译与链接[参照].pdf

    第7章 链接.xmind

    本人看《深入理解计算机系统》的整理文档,以思维导图的形式讲述链接的背景、原理、过程、方法和优缺点,分享出来,如有错误,欢迎指正交流!

    第7章 PIC、GOT和PLT和延迟绑定(动态链接补充)

    NULL 博文链接:https://chuanwang66.iteye.com/blog/1839210

    Linux基础课件第七章多模块软件的编译和链接.ppt

    Linux基础课件第七章多模块软件的编译和链接.ppt

    MFC程序设计轻松入门第七章

    第7章 项目ExMouseCapture,鼠标捕获; 项目ExCursor,改变客户区光标为I形光标; 项目ExMK,鼠标光标位置坐标在状态栏的显示和客户区点击鼠标左键,弹出鼠标光标位置信息对话框; 项目ExChar,只有在当前窗口下输入...

    链接器和加载器.PDF(链接器和加载器 Beta 2)

    第7章 重定位 7.1 硬件和软件重定位 7.2 链接时重定位和加载时重定位 7.3 符号和段重定位 7.4 基本的重定位技术 7.5 可重链接和重定位的输出格式 7.6 其他重定位格式 7.7 特殊情况的重定位 练习 项目 第8章 加载和...

    Delphi 6集成开发环境 pdf

    第7 章 图形与图像 第8 章 多媒体编程技术 第 9 章 OpenGL 开发三维图形 第10 章 多线程应用程序 第11 章 动态链接库 第 12 章 Delphi 数据库的基本概念 第 13 章 简单数据库应用程序的创建 第14 章 数据交换 第 15 ...

    数据结构(C语言版) 第八章 排序 知识梳理 + 习题详解1

    七、第七章作业答案本系列博客为《数据结构》(C语言版)的学习笔记(上课笔记),仅用于学习交流和自我复习数据结构合集链接: 《数据结构》C语言版(严蔚敏版) 全书

    visual c++完全自学手册(随书源码)

    第7章 Windows的基本输入 第8章 ActiveX控件 第9章 菜单和框架窗口 第10章 工具栏和状态栏 第11章 文档和视图的分离 第12章 文档的读和写 第13章 切分窗口和多视图 第14章 打印功能 第15章 上下文相关帮助 第16章 ...

    VC 基础 学习 课件

    第1章 Visual C++6.ppt第2章 Visual C++6.ppt第3章 Windows编程与MFC基础.ppt第4章 面向对象程序设计与C.ppt第5章 基本输入——键盘.ppt第6章 菜单、工具栏和状态栏.ppt第7章 使用Windows标准控件.ppt第8章 MFC的...

    JessonYue#Computer-basics#第七章3

    第七章 链接链接是将各种代码和数据片段手机并组合成为一个单一文件的过程,这个文件可以被加载(复制)到内存中执行。静态链接链接可以在编译时由静态编译器完成,为了构

    Visual C++编程与项目开发 李英编著华东理工大学出版社 源代码

    全书共包括十七章:第一章软件开发环境与软件项目开发过程,第二章C++语言基础,第三章Windows应用程序编程与MFC,第四章文档/视图结构及其编程,第五章程序界面设计,第六章对话框与控件,第七章绘图,第八章文件操作,第九...

    Visual C++编程与项目开发 实验指导书

    全书共包括十七章:第一章软件开发环境与软件项目开发过程,第二章C++语言基础,第三章Windows应用程序编程与MFC,第四章文档/视图结构及其编程,第五章程序界面设计,第六章对话框与控件,第七章绘图,第八章文件操作,第九...

Global site tag (gtag.js) - Google Analytics