链接器如何确定PE/ELF可执行文件section的进程地址空间映射位置以完成符号解析
PE/ELF 节到进程虚拟地址空间的映射原理
链接器为section分配虚拟地址的核心逻辑
链接器计算地址的全程,完全不需要和操作系统运行时交互,所有地址决策都是基于和系统加载器提前约定好的全局排布规则完成的:
- 对ELF体系(Linux等类Unix系统):链接器(如GNU ld、lld)会按照内置链接脚本的规则,按固定顺序排列所有节,依次满足内存页对齐要求后从预设的默认加载基地址开始累加计算地址。常见排布顺序为:带可执行权限的
.text节(代码)→ 带只读权限的.rodata节(字符串常量、只读全局变量)→ 带可读写权限的.data节(已初始化全局变量)→ 零初始化的.bss节。每个节的长度会向上取整到内存页大小的整数倍,留出对齐填充,链接器顺着这个顺序就能算出每个节的起始虚拟地址,所有引用节内符号的地址项,都会直接在链接完成时写入计算好的绝对地址(非PIE场景下)。 - 对PE体系(Windows):链接器会使用配置好的默认ImageBase作为起始计算点,比如用户态EXE默认基址为
0x00400000,用户态DLL默认基址为0x10000000,同样按照页对齐规则把.text、.rdata(对应ELF的rodata)、.data等节依次往后排列。节的相对偏移会被记录为RVA(相对虚拟地址),链接器解析符号时直接将RVA加上默认基址,得到最终写入文件的虚拟地址。
举个实际计算的例子:如果.text节长度为4096字节(刚好1个4K内存页),按4K向上对齐后,后续紧跟的.rodata节起始RVA就是0x1000,对应默认基址下的虚拟地址就是0x00401000,那存放在rodata节偏移8字节位置的字符串常量,地址就会被直接算为0x00401008——这个计算过程全程在链接阶段完成,和运行时OS把这个文件加载到哪个内存位置没有任何关系。
操作系统是否为各section划定固定映射地址范围?
操作系统不会给单个section划定专属的固定地址区间,但会和链接器约定进程虚拟地址空间的全局大分区边界:
- 以Windows为例,用户态虚拟地址空间会划分为几个固定权限的大区域:低地址64KB为禁止访问的空保护区,从默认ImageBase起始的连续区域预留为PE文件自身的映射范围,中间的独立区域分别预留为堆、线程栈、DLL加载映射区。这些大分区的边界是固定的,但具体每个section落在分区内的哪个页,完全由链接器排好的节顺序决定,OS不会强制要求
.text必须映射到某一个固定虚拟页,只要节的内存权限(可读/可写/可执行)和节表定义匹配、落在合法的可映射地址范围内即可。 - 如果加载时发现链接器预设的基地址已经被其他模块占用(比如DLL地址冲突),加载器会触发整模块重定位逻辑:把整个可执行文件的所有section整体搬迁到空闲的虚拟地址区间,把文件内所有硬编码的地址项统一加上一个修正偏移量,不破坏节和节之间的相对位置关系。
无固定单节地址时,链接器如何保证符号地址解析正确?
这里的核心误区是:链接器从始至终计算的都是地址的相对关系,而非运行时的绝对地址值,因此完全不需要OS提供固定的节地址范围:
- 链接器计算的所有符号地址,都是基于“预设基地址 + 节的先后顺序 + 对齐填充”的自洽计算,整个模块内部所有节的相对偏移是从链接阶段就固定死的。就算运行时加载器把整个模块搬迁到了任意合法的基地址,节和节之间的相对距离没有变,只要给所有硬编码的绝对地址统一加上搬迁带来的偏移量,之前解析好的所有符号地址就都是正确的。
- 如果是PIE/位置无关代码的编译场景,链接器甚至根本不会在文件里硬编码绝对地址,所有对符号的引用都会被计算为相对当前指令的偏移量。不管整个模块最终被映射到虚拟地址空间的哪个位置,只要相对偏移不变,所有符号引用就都能命中正确的内存位置,连全量重定位的开销都能省掉。
内容的提问来源于stack exchange,提问作者Akshat Joshi
相关产品推荐
相关产品推荐

