前言:
一、动态链接
1、静态链接存在的问题?
静态链接主要存在空间浪费和更新困难两个主要问题:
- 空间浪费
- 冗余存储:每个可执行文件都独立包含所用库的代码副本,导致磁盘空间浪费(尤其多个程序使用相同库时)。
- 内存冗余:运行多个静态链接程序时,相同库代码被重复加载到内存,占用更多物理内存。
- 更新困难
- 升级困难:库升级需重新编译并分发所有依赖它的程序,维护成本高。
- 版本碎片化:不同程序可能绑定不同库版本,导致安全补丁难以统一应用,增加兼容性风险。
2、动态链接的核心思想
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在运行时才将它们连接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都连接成一个单独的可执行文件,其主要目标是:
- 减少磁盘和内存占用:多个程序共享同一份库代码,避免静态链接导致的冗余存储。
- 灵活更新:库的更新只需替换共享库文件(如 .so、.dll),无需重新编译依赖它的程序。
- 运行时加载:程序可以在运行时动态加载所需的库(如插件系统),增强灵活性。
动态链接及运行时的链接及多个文件的装载,必须要有操作系统的支持,因为动态链接的情况下,进程的虚拟地址空间的分布会比静态链接情况下更为复杂。
当程序被装载的时候,系统的动态连接器会将程序所需要的所有动态链接库装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位操作。相较静态连接器动态链接是把链接过程从本来的程序装载前被推迟到了程序装载的时候。
3、示例
下面是一个简单的示例,如下:
// utils.h
#pragma once
__declspec(dllexport) int add(int a, int b);
// utils.cpp
#include "utils.h"
int add(int a, int b)
{
return a + b;
}
// DynamicLinkDemo.cpp
#include <iostream>
#include "utils.h"
int main()
{
int a = 10;
int b = 20;
std::cout << " a + b = " << add(a, b) << std::endl;
}
使用
dumpbin工具,用cl编译器把utils.h与utils.cpp编译成地址无关utils.dll
cl /DUTILS_EXPORTS /LD /DYNAMICBASE utils.cpp /Fe:utils.dll
把
DynamicLinkDemo.cpp编译成把DynamicLinkDemo.exe,并动态链接utils.dll
cl DynamicLinkDemo.cpp /Fe:DynamicLinkDemo.exe /link utils.lib
4、装载时重定位
装载时重定位是指程序在从磁盘加载到内存时,对其中的绝对地址引用进行修改的过程,用于解决程序编译链接时的预期地址与实际加载到内存的物理地址不匹配的问题,使得程序能够在实际分配的内存地址上正确运行。装载时重定位具备下面的特点,如下:
- 重定位时机固定: 仅在程序被装载到内存的阶段执行,完成后程序进入运行阶段,不再进行任何地址修正。这与运行时重定位(动态链接库常用,运行中修正地址)形成鲜明对比。
- 直接修改内存镜像: 操作系统会根据重定位表(可执行文件中记录需修正地址的信息),直接修改内存中已加载的程序指令和数据。例如,若原代码中call 0x00401234(默认基地址下的函数地址),实际加载偏移量为0x00400000,则修正为call 0x00801234。
- 内存镜像不可共享: 由于地址修正会直接修改内存中的代码,同一份程序的内存镜像无法被多个进程共享(每个进程加载地址不同,修正后的代码也不同),每个进程需加载独立副本,内存利用率较低。
装载时重定位是早期操作系统中适配程序地址的关键技术,通过装载阶段一次性修正地址,解决了静态链接程序的地址适配问题,同时保证运行效率。但其内存镜像不可共享的局限性,使其逐渐被动态链接中的地址无关代码(PIC)+ 运行时重定位取代,目前主要用于嵌入式、小型静态程序等场景。
5、地址无关代码
5.1、概念
地址无关代码 是一种在编译和链接阶段生成的、其二进制代码可以被加载到内存任意位置而无需重定位的技术。简单来说,就是一段程序,无论操作系统把它放在内存的哪个地址上,它都能正确运行,不需要修改自身的代码。
5.2、核心作用
- 支持代码共享与动态加载:使编译后的代码(尤其是共享库,如 Linux 的 .so 或 Windows 的 .dll)能被加载到内存任意地址,且多个进程可共享同一份代码副本(仅数据段独立),大幅节省内存资源。
- 适配动态内存分配: 现代操作系统(如启用 ASLR 地址随机化)会随机分配程序 / 库的加载地址,PIC 确保代码在任意地址都能正常运行,无需重新编译或修改。
- 简化动态链接:在动态链接场景中,程序可在运行时加载外部库,PIC 保证库代码能适配程序的内存空间,避免地址冲突。
5.3、解决的核心问题
- “编译时地址假设” 与 “运行时实际地址” 不匹配的矛盾:编译器编译代码时,无法预知程序最终会被加载到内存的哪个地址(尤其是共享库,加载地址不固定)。若代码中硬编码绝对地址(如 call 0x00401234),当实际加载地址不同时,这些地址会失效,导致程序崩溃。PIC 通过相对寻址(如 call rip+0x100,基于当前指令地址计算偏移)或间接引用(如通过 GOT/PLT 表),消除对绝对地址的依赖。
- 共享库的多进程内存复用问题: 若共享库代码依赖绝对地址,每个进程加载时都需修改代码中的地址(即 “重定位”),导致每个进程必须保存一份独立的代码副本,浪费内存。PIC 使代码段完全只读且地址无关,多个进程可共享同一份代码段(仅数据段因进程私有而独立),显著提升内存利用率。
- 地址空间布局随机化(ASLR)的兼容性问题: ASLR 是操作系统的安全机制,通过随机化程序 / 库的加载地址抵御缓冲区溢出等攻击。若代码依赖固定地址,ASLR 会导致地址失效。PIC 与 ASLR 天然兼容,无论加载地址如何随机化,代码都能通过相对或间接方式正确访问符号。
5.4、如何产生地址无关代码?
现代机器产生地址无关代码并不麻烦,首先,分析下模块中各种类型的地址引用方式。这里把共享对象模块中的地址引用按照是否跨模块分成两类:模块内部引用和模块外部引用;按照不同的引用方式又可以分为指令引用和数据访问,这样就可以分成下面四种情况:
- 类型一:模块内部调用或跳转
这一种类型是最简单的,因为被调用的函数与调用者处在同一个模块,它们之间的相对位置是固定的。对于现代的系统来讲,模块内部的函数跳转与调用都可以是相对地址调用,所以对这种指令是不需要重定位的。
- 类型二:模块内部数据访问
很明显,指令中不能直接包含数据的绝对地址,那么唯一的办法就是相对寻址。通常,一个模块前面一般是若干页的代码,后面紧跟着若干页的数据,这些页之间的相对位置是固定的,也就是说,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移就可以访问模块内部数据了。
- 类型三:模块间数据访问
模块间的数据访问比模块内部稍微复杂,因为模块间的数据访问目标地址要等到装载时才决定。要使得代码地址无关,基本的思想就是把跟地址相关的部分放到数据段,很明显,这些其他模块的全局变量的地址是跟模块装载地址有关的。
在 Windows 平台的 PE(Portable Executable)格式中,IAT(Import Address Table,导入地址表) 和 INT(Import Name Table,导入名称表) 是实现动态链接的核心结构,二者配合工作,让程序能在运行时正确调用外部 DLL(动态链接库)中的函数或访问外部变量。
在 Linux 平台的 ELF(Executable and Linkable Format)格式文件中,GOT(Global Offset Table,全局偏移表) 是实现动态链接和地址无关代码(PIC)的核心结构,主要作用是通过间接寻址解决程序运行时的地址引用问题,确保代码可以在内存任意位置加载执行。
- 类型四:模块间调用、跳转
在Windows平台,对于模块间的调用与跳转,可以采用IAT与INT表来解决。
1191

被折叠的 条评论
为什么被折叠?



