读书-程序员的自我修养-链接、封装与库(17:第七章:动态链接(1)动态链接和地址无关代码

    技术2023-08-09  62

    读书-程序员的自我修养-链接、封装与库(17:第七章:动态链接(1)动态链接和地址无关代码)

    一、 动态链接二、 地址无关代码2. 固定装载地址的困扰2.1 静态共享库2.1.1 静态共享库和静态库 2.2 解决-共享对象在任意地址加载 3. 装载时重定位3.1 静态链接中重定位的基本思想3.1.1 例子说明 3.2 几个重定位的概念3.3 装载时重定位不能解决共享对象中的问题3.3.1 原因 3.4 -shared 支持装载时重定位 4. 地址无关代码4.1 -fPIC 作用4.2 装载时重定位不能解决共享对象中的问题4.3 地址无关代码4.3.1 共享对象模块中的四种引用方式4.3.2 GOT 表4.3.3 地址无关代码小结 4. 共享模块的全局变量问题5. 数据段地址无关

    一、 动态链接

    参考: 静态链接库和动态链接库 https://blog.csdn.net/lqy971966/article/details/105207532

    二、 地址无关代码

    2. 固定装载地址的困扰

    2.1 静态共享库

    共享对象在被装载时,如何确定它在进程虚拟地址空间中的位置? 早期是通过静态共享库(Static Shared Library)解决的。

    2.1.1 静态共享库和静态库

    静态共享库和静态库有明显的区别。 静态共享库的做法就是:将程序的各个模块统一交给操作系统来管理, 操作系统在某个特定的地址划分出一些地址块,为那些已知的模块预留足够的空间。

    静态共享库的缺点: 地址冲突,升级问题多

    2.2 解决-共享对象在任意地址加载

    为了解决这个模块装载地址固定的问题,我们设想是否可以让共享对象在任意地址加载?就是: 共享对象在编译时不能假设自己在进程虚拟地址空间中的位置。

    3. 装载时重定位

    3.1 静态链接中重定位的基本思想

    为了能够使共享对象在任意地址装载,我们首先系想到的方法就是静态链接中的重定位。基本思想: 在链接时,对所有的绝对地址的引用不做重定位,而把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中所有的绝对地址应用进行重定位。

    3.1.1 例子说明

    假设函数 helloHani()相对于代码段的起始地址是0x100,当模块被装载到0x10000时,我们假设代码段位于模块的最开始,那么 helloHani 的地址就是:0x10100。 此时,系统遍历模块中的重定位表,把所有对helloHani的地址引用(其他模块调用它)都重定位至 0x10100。

    3.2 几个重定位的概念

    静态链接的重定位叫做:链接时重定位 现在的叫做:装载时重定位 windows中,这种装载时重定位又叫做:基址重置

    3.3 装载时重定位不能解决共享对象中的问题

    程序模块在编译时目标地址不确定而需要在装载时将模块重定位。 但是装载时重定位的方法不适合用来解决共享对象中的问题。

    3.3.1 原因

    动态链接模块被装载映射到虚拟空间后,指令部分是在多个进程 之间共享的,由于装载时重定位的方法需要修改指令,所以没有办法 做到同一份指令被多个进程共享,因为指令被重定位后对于每个进程 来讲是不同的。

    当然,动态链接库中的可修改数据部分对于不同的进程来说有多个副本, 所以它们可以采用装载时重定位的方法来解决。

    3.4 -shared 支持装载时重定位

    linux 支持这种装载时重定位的方法:叫做 -shared 该选项指定生成动态连接库

    4. 地址无关代码

    4.1 -fPIC 作用

    PIC: Postion-independent Code 地址无关代码 基本思想: 把跟地址相关的部分放到数据段里面

    4.2 装载时重定位不能解决共享对象中的问题

    装载时重定位是解决动态模块中有绝对地址引用的办法之一, 但是它有一个很大的缺点是:指令部分无法再多个进程之间共享, 这样就失去了动态链接库节省内存的一大优势。

    4.3 地址无关代码

    我们希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以再每个进程中拥有一个副本。 这个方案就是:地址无关代码的技术。

    4.3.1 共享对象模块中的四种引用方式

    第一种:模块内部的函数调用、跳转等第二种:模块内部的数据访问,比如模块中定义的全局变量,静态变量第三种:模块外部的函数调用、跳转等第四种:模块外部的数据访问,比如其他模块中定义的全局变量

    代码例子 pic.c

    static int a; extern int b; extern void ext(); void bac() { a - 1; //第二种 b - 1; //第四种 } void foo() { bac(); //第一种 ext(); //第三种 }

    4.3.2 GOT 表

    GOT: Global Offset Table 全局偏移表

    代码地址无关的思想是:把指令段里跟地址相关的部分数据放到数据段里面。

    这里,其他模块的全局变量的地址是跟模块装载地址有关的。 ELF做法是在数据段里面建立一个指向这些变量的指针数组,也被称为Got全局偏移表。

    优点: GOT放在数据段,所以它可以再模块装载时被修改,并且每个进程都可以有独立的副本,相互不受影响。

    基本机制图:

    4.3.3 地址无关代码小结

    各种地质引用方式 指令跳转、调用 数据访问 模块内部 相对跳转和调用 相对地址访问 模块外部 渐渐跳转和调用(GOT) 间接访问(GOT)

    4. 共享模块的全局变量问题

    前面四种情况没有包含:定义在模块内部的全局变量的情况,怎么解决?

    大多数可以和内部静态变量一样处理,但是有一个特殊情况:

    extern int global; int foo() { global = 1; }

    这里,当编译到 global的时候,无法判断是跨模块还是模块间调用 解决: 这里ELF编译时,默认把定义在模块内部的全局变量当做定义在其他模块的全局变量。

    5. 数据段地址无关

    static int a; static int* p = &a;

    p的地址是一个绝对地址,a的地址会随着共享对象的装载地址改变而变。

    方法一: 可以采用装载时重定位的方法来解决。 通过重定位表来解决它。

    Processed: 0.012, SQL: 9