在C/C++程序中,你可以打印出函数或者全局变量的内存地址,实际上这些地址在编译时就已经确定了,确切的说是在链接时确定的(不考虑地址空间布局随机化ASLR)。
可是链接器是怎么在程序运行起来之前就确定这些符号的运行时地址呢?
这种困惑源于一个常见的误解:认为链接器确定的是物理内存地址。实际上,链接器处理的是虚拟内存地址(Virtual Address),这是现代操作系统提供的一种强大抽象。
虚拟地址空间
现代操作系统为每个进程提供了一个独享的虚拟地址空间(Virtual Address Space, VAS)。
这是一个从0到最大地址(例如在32位系统中是0xFFFFFFFF)的、私有的、连续的、独立的内存空间:

从进程的视角看,它"独占"了整个内存资源,所有代码和数据都从它"认为"的固定地址开始存放。
这种设计带来了一个关键优势:每个进程都拥有自己完整且一致的地址空间视图:

操作系统保证,在进程的生命周期内,其虚拟地址空间的布局规则是稳定且一致的。
这意味着链接器可以依赖这个稳定的虚拟地址空间布局来进行地址分配。
ABI 与可执行文件格式的约定
此外,操作系统还约定好了程序的默认加载基址 (Base Address / Image Base),Linux (ELF) 传统默认 0x400000
(64位常为 0x400000
)。
链接器在合并目标文件时,会依据一个链接脚本(Linker Script)来决定各个段的布局,链接脚本可以是默认脚本(由编译工具链提供,如 GNU ld 的默认规则),当然程序员也可以自定义。
这里有个简单的链接脚本示例:
SECTIONS {
. = 0x400000; /* 基址 */
.text : { *(.text) } /* .text 段紧接基址 */
. = ALIGN(4096); /* 按 4KB 对齐 */
.data : { *(.data) } /* .data 段紧随其后 */
.bss : { *(.bss) } /* .bss 段放在最后 */
}
链接脚本会明确指定:
各个段( .text
,.data
,.bss
,.rodata
等)的排列顺序;每个段的起始 VMA; 段之间的对齐方式(如 . = ALIGN(4096)
)。
链接器严格按照约定的基址和各个段的布局规则,计算并填充文件中所有代码/数据的最终虚拟地址 (VMA)。
这些段的内存信息则写到了可执行文件 (ELF) 中。
从虚拟到现实
因此我们可以看到,链接器自始至终就没去预测程序在运行时真正的物理内存地址。
程序在真正的运行起来后由加载器将可执行程序(ELF)加载到操作系统分配的虚拟地址空间中,然后操作系统为虚拟地址空间分配真正的物理内存(现代操作系统会将这一过程推迟到程序真正访问该虚拟内存的那一刻),此时才会真正确定程序的物理内存地址。

但是,这个物理内存地址也只有操作系统自己会关心,而对于我们的程序来说它根本就不关心自己真正运行在哪个物理内存地址上,也不需要关心。
这种机制聪明之处在于:链接器、操作系统和硬件(MMU)各司其职,通过约定和分工,实现了看似不可能的任务——在程序运行前就确定其内存布局,这正是现代计算机系统中抽象力量的完美体现。
------------ END ------------

单片机OTA升级中的A/B双分区:经典方案与实现逻辑

几个受欢迎的嵌入式开源项目

深圳行,第一天!