Linux 库制作与原理(二)深入动静态链接原理

Linux 库制作与原理(二)深入动静态链接原理 目录一、如何理解链接与加载链接与加载链接器与加载器二、理解静态链接​编辑​编辑静态链接的本质:三、理解动态链接图一:库函数调用的本质动态库的相对地址图二:动态链接的本质四、动静态库的对比五、总结一、如何理解链接与加载链接与加载链接是在程序编译阶段由链接器将多个目标文件、依赖库进行符号解析与地址重定位合并代码段、数据段等节区为程序中的指令、变量、函数分配固定的虚拟地址并生成具有规范格式的可执行文件如 ELF的过程。链接完成后可执行文件中已包含完整的段布局信息与虚拟地址规划存储于磁盘中等待运行。加载是用户启动程序如双击执行时操作系统内核读取磁盘上的可执行文件解析其程序头表根据文件中预设的虚拟地址为程序创建独立进程初始化 task_struct 与 mm_struct 结构建立虚拟内存区域与页表映射将文件中的各个段按指定地址映射到进程的虚拟地址空间。首次加载需从磁盘读取文件并完成内存布局初始化耗时相对较长后续再次启动时系统会利用文件缓存机制直接从内存读取数据无需重复访问磁盘加载速度显著提升。加载完成后CPU 中的程序计数器 EIP/RIP 指向程序入口虚拟地址通过 MMU 结合 CR3 寄存器查询页表将虚拟地址翻译为物理地址实现对物理内存的访问程序正式进入执行阶段。链接器与加载器链接器Linker: 链接器是编译过程中的核心系统工具属于编译链的一部分。它负责将编译器生成的多个目标文件.o以及静态库、动态库等依赖文件进行符号解析、地址重定位、段合并为程序中的代码、数据分配确定的虚拟地址并最终生成格式规范、可被操作系统识别的可执行文件如 ELF 格式。链接器工作在程序运行之前输出结果是存储在磁盘上的可执行文件不涉及内存执行与进程创建。加载器Loader: 加载器是操作系统内核中的一段程序逻辑属于操作系统的内存管理与进程管理组件。当用户启动程序时加载器负责解析磁盘上的可执行文件格式读取程序头表中的段信息与虚拟地址布局为程序创建独立进程初始化 PCBtask_struct与虚拟地址空间结构体mm_struct建立页表映射并将可执行文件中的代码段、数据段等按指定虚拟地址映射到进程地址空间完成程序从磁盘到内存的装载过程使程序具备被 CPU 执行的条件。二、理解静态链接我们都知道静态链接的本质是把库中的代码拷贝到我们的可执行文件中。无论是自己的 .o 还是静态库中的 .o 本质都是把 .o 文件进行连接的过程所以研究静态链接本质就是研究 .o 文件是如何链接的。我们分别在 main.c 文件和 run.c 文件中写了些代码其中在 run.c 文件中写了 run 函数的定义在 main.c 中调用这个 run 函数下面我们先将这两个文件编译成 .o 文件然后使用反汇编进行查看:观察上面两张图片现在我有几个问题:1. call 是干啥的它指的是具体函数吗call 是 x86-64 汇编里的函数调用指令作用是跳转到目标函数执行执行完后再回到当前位置继续往下走。在 .o 文件里它还不知道具体函数地址第一个 main.o 文件中前两个 call 指令代表分别要调用main.c文件中的 printf 和 run 函数同样地址是占位符。run.o 文件中的 call 则表示要调用run.c文件中的 printf 函数。2. call 指令前面的一堆 00 00 00 00 是什么这些 00000000 并不是真实的地址而是占位地址也就是说在不是真实地址之前这些0只是有占位的作用并不是 printf 和 run 函数的真实地址链接器会在最后一步将其替换为真实地址。这是 .o 文件的典型特征没有虚拟地址只有展位地址等链接后才会变成 0x400440 这种固定虚拟地址。3. 没链接前main.o 和 run.o 里的函数互相认识吗完全不认识它们是“互相看不见”的状态。main.o 里只知道“我要调用一个叫 run 的函数”但不知道 run 在哪、长什么样。run.o 里也不知道自己会被谁调用只知道“我要调用 printf ”同时也不知道 printf 在哪。它们只保留符号名run 、 printf等待链接器去“牵线搭桥”。也就是说在未链接前不同 .o 文件里的函数互不认识只知道对方的符号名所有地址都未确定只有经过链接器的符号解析和重定位才能变成可执行文件里的真实虚拟地址让函数互相找到对方。4. 这里突然想起来上一篇文章中的一些疑问我们都知道可执行程序和 .o 文件都是ELF格式的文件但是它们用反汇编查看完全不一样:可执行程序(如 a.out)已经完成了链接链接器为所有指令、函数已经分配了固定的虚拟地址(逻辑地址)比如 0x400440 这种。反汇编时看到的地址就是进程运行时真正使用的虚拟地址call 指令里也已经是目标函数的真实地址。而 .o 目标文件还没进行链接所以没有任何虚拟地址所有地址都是从 0 开始的相对偏移只表示指令在函数/节内部的位置。 call 指令里的机器码是 e8 00 00 00 00 后面的 00 00 00 00 是地址占位符等待链接器在链接阶段替换成目标函数的真实虚拟地址。我们要弄清这两者的区别。为了进一步弄清楚链接阶段是如何将全0的占位地址变成可执行文件里的真实虚拟地址我们继续使用 readelf -s 命令读取一下这两个 ELF 文件中的符号表Symbol Table)也就是文件里所有符号(函数、变量、段等的详细信息包括符号的地址、类型、作用域、所在位置等。这是 main.c 编译成 main.o 后的符号表第一个核心符号是第四行的 main 函数类型是 FUNC (函数)全局可见位于 .text 段Ndx1)是 main() 函数的符号。第二个核心符号是下面的puts类型是 NOTYPE全局可见 NdxUND (未定义)对应代码里的 printf(hello world!\n) (编译器优化后printf 被替换成了 puts。第三个核心符号是 run 类型是 NOTYPE 全局可见NdxUND (未定义)对应代码里的 run(); 函数的调用。这是 run.c 编译成 run.o 后的符号表第一个核心符号是 run 类型是FUNC(函数全局可见位于 .text 段Ndx1)是 run() 函数的实现符号。第二个核心符号是 puts 类型也是NOTYPE全局可见NdxUND(未定义)对应代码0里的 printf(running...\n) (同样被优化为 puts。问题:1. NOTYPE 是什么意思为什么会是 NOTYPE 在 ELF 符号表的 Type 列里 NOTYPE 表示这个符号的类型未被明确指定通常用于未定义的外部符号。比如 puts 和 run在 main.o )里它们在当前 .o 文件里只有调用、没有实现编译器不知道它到底是函数、变量还是其他所以标记为 NOTYPE等链接时再确定具体类型。但是比如 main 和 run它们在当前 .o 文件里有完整实现所以类型是 FUNC表示“这是一个函数”。当我们在 main.c 里写 run(); 时编译器只知道“我要调用一个叫 run 的东西”但看不到 run 的实现所以无法确定它是函数、变量还是宏只能先标记为 NOTYPE。链接时链接器才会找到 run.o 里 run 的实现类型是 FUNC然后把 main.o 里 run 的 NOTYPE 修正为 FUNC并绑定到真实地址。2. UND 是什么意思UND 是 Ndx (Section Index列的特殊取值全称是Undefined(未定义。它表示这个符号在当前 .o 文件里只有引用、没有实现需要在链接阶段去其他文件或库中找到它的定义。main.o 里的 run 和 puts 都是 UNDrun 是在 run.o 里实现的main.o 只调用它所以未定义。puts(即 printf是在 C 标准库libc.so里实现的main.o 只调用它所以未定义。run.o 里的 puts 也是 UND同样是调用了 C 标准库的 puts自己没有实现。3. Value 列里的 0000000000000000 是什么意思?Value 列里的 0000000000000000 和之前看到的 call 机器码里的 00 00 00 00 本质是一样的表示没有实际地址所以固定填 0表示这个符号还没有位置等链接再分配。弄清楚这些概念之后我们下一步就将这两个 .o 文件链接成可执行程序然后再安装上面的指令观察是否有变化:上图是动态链接后生成的可执行文件(链接了 main.o 和 run.o)的符号表分为两个关键表第一个表是 .dynsym 动态符号表专门存放动态链接相关符号(主要是未定义、需要从动态库加载的符号。第二个表是 .symtab 完整符号表包含所有符号静态/动态、定义/未定义。我们可以看到 run 和 main 函数的类型已经由 UND 变为了 FUNC 并且 Value 对应的地址值也从全0变成了有效地址 这也说明表示 run 函数和 main 函数已被链接到可执行文件分配了固定虚拟地址不再是 UND 。但是为什么 puts 还是 UND 核心原因是 puts 是动态链接符号实现代码在系统的 libc.so 动态库中没有被复制到可执行文件里。可执行文件只保留对 puts 的引用标记为 UND未定义。程序运行时动态链接器ld-linux.so会加载 libc.so找到 puts 的真实内存地址完成地址绑定让程序能正常调用。其他 UND 符号如 __libc_start_main 、_end 等均为系统或动态库符号也是同理。如果我们改为静态链接的话这个 puts 就是 FUNC 了因为静态链接链接器会把 libc.a静态库)中 puts 的机器码实现直接复制到你的可执行文件里。Value 列会变成它在可执行文件中的虚拟地址。因为包含了 puts 及依赖的库代码。和我们自己写的 run 、main 一样被嵌入到可执行文件的代码段中不再依赖动态库。所以我们就可以推测我们运行代码时如果出现了 UND 或者未定义符号相关的报错本质是链接阶段出了问题而不是编译阶段。原因就是代码里引用了某个符号函数/变量)但编译器/链接器找不到它的实现代码。下面我们将程序的反汇编和符号表放在一起看一下:所以我们可以得出 : 程序在编译生成 .o 时call 函数地址是未知的所提先用 0 占位链接阶段链接器给所有函数、变量分配真正的虚拟地址它会把所有 .o 拼在一起给每个函数分配地址把之前所有call 0x00000000的 0 全部替换成真地址比如func()最终地址是0x401130链接器就把指令改成 call 0x401130链接完成后所有地址都是真实的虚拟地址程序可以直接运行这个过程叫地址重定位Relocation。静态链接的本质:经过上面的详细讲解与演示我们可以总结出静态链接的本质:(重要)在程序从源代码到可执行文件的过程中链接阶段的核心工作就是符号解析与地址重定位。以我们的 main.o 和 run.o 为例 main.c 中只有 run 函数的声明没有具体实现因此编译生成的 main.o 里 run 符号会被标记为 UND (未定义调用 run 的指令地址也只是占位的 0此时两个目标文件之间互不认识、没有任何关联。到了链接阶段链接器会先扫描所有参与链接的 .o 文件收集所有函数、变量等符号信息当发现 main.o 存在未定义的 run 符号时就会在其他目标文件中查找匹配的实现最终在 run.o 里找到 run 函数的完整代码与确定地址。找到之后链接器会把 main.o 中原本占位的指令地址替换成 run 函数在最终可执行文件里的真实虚拟地址同时将 main.o 里 UND 状态的 run 符号修正为已定义状态让调用指令能够准确跳转到对应位置从而让不同文件里的函数能够互相找到并正常调用。静态链接的过程中有加载的过程吗?为什么静态链完全没有程序运行阶段的加载过程它的所有工作都发生在编译链接阶段。 在编译生成 .o 目标文件后链接器会读取静态库.a/.lib内部打包的目标文件将程序实际用到的函数代码完整复制出来和我们自己的 .o 文件合并成一个独立的可执行文件同时完成所有函数地址的重定位把之前的占位地址替换为真实虚拟地址。这个合并完成后静态库本身的使命就结束了它不会以任何形式参与程序运行。 当程序启动时操作系统只需要加载这一个最终生成的可执行文件即可文件内部已经包含了程序运行所需的全部代码不需要再去寻找、读取、映射静态库文件也不存在任何针对静态库的加载操作。所以静态链接是链接阶段完成代码合并运行时不加载任何库文件可执行文件完全独立不再依赖静态库。三、理解动态链接动态库也叫共享库和静态库不同的是静态库(.a)在编译期直接嵌入进可执行文件运行时不独立加载。而动态库/共享库(.so)在编译期不嵌入运行时由操作系统动态加载多个进程可共享同一份库文件节省内存。这里有个问题 :动态链接的程序是先加载它的程序还是先加载程序依赖的库答案是动态链接器是先加载程序依赖的库然后再加载序本体本身。在程序启动运行(./a.out或者双击程序)时操作系统先让动态链接器ld-linux.so介入动态链接器会先解析并加载程序所有依赖的动态库.so到内存等所有依赖库加载完成、符号解析完成后再把程序本体的代码和数据映射到虚拟地址空间之后才开始执行入口_start。就像我们上面已经有了可执行程序 main.exe 了为什么里面的 puts 还是 UND 未定义的呢?那是因为在编译生成可执行文件(main.exe)的阶段puts 等外部函数符号必然是显示为 UND(Undefined)是动态链接的标准设计。 编译阶段动态链接器(ld-linux.so)不会在编译期把库代码链接进你的程序。所以在可执行文件内部 puts 的地址永远是空的(0)状态显示为 UND。真正的链接发生在“程序运行”的时候程序运行阶段时当我们双击运行程序时操作系统的加载器(Loader会介入读取程序的依赖清单比如 libc.so )找到对应的动态库文件加载到内存解析符号把 puts 等所有 UND 符号替换为动态库中函数的真实内存地址。之后程序才开始正式执行。所以动态库优先加载程序本体后加载。图一:动态库在磁盘上动态库(.so 文件)本质上就是一个普通的磁盘文件。它开始时静静地躺在磁盘里和你的 main.exe 一样只是一堆二进制数据。当进程加载启动后内核的动态链接器会把它从磁盘读取到物理内存中。一旦被加载进内存它就不再是磁盘上的死文件了而是变成了物理内存里的一段数据。和普通进程的代码段、数据段一样都是物理内存里的实体。并且因为它是共享库系统内存中只会存一份 libc.so 然后让所有用到它的进程比如进程A、进程B去共享这一份内存。下一步就是最关键的一步 : 建立“映射”MMU 页表)因为进程A不知道动态库在物理内存的具体位置所以必须建立映射关系首先内核建立页表项操作系统维护一张页表。它会记录“动态库 XXX.so 在物理内存的第 X 页”。然后内核会给进程A的 mm_struct(虚拟地址空间 中划分一块区域共享区。最后通过页表把进程A虚拟地址空间里的“共享区”映射到物理内存中存放 XXX.so 的那一页。这样当进程A运行到需要调用 puts 时程序发出请求执行call puts 。CPU 使用 puts 对应的虚拟地址属于进程A的共享区。MMU内存管理单元查页表发现这个虚拟地址对应的是物理内存里的 XXX.so 数据。最终成功访问CPU 直接去物理内存中读取动态库的代码并执行。疑问1 : 物理内存中是物理地址还是虚拟地址?物理内存里存储的程序/数据本质上都是“物理内存”的实体使用的是物理地址但我们在程序中看到的一切(代码、变量、指令)都是“虚拟地址”。当你的程序main.exe、动态库 (libxxx.so被加载进物理内存后它们在硬件层面上占用的是物理内存的物理地址。也就是说物理内存是真实存在的一块硬件区域。每一块内存都有一个 物理地址(Physical Address。操作系统内核管理进程时就是直接操作这块物理内存。所以库、程序、数据在物理内存中使用的就是物理地址。为什么我会觉得是“虚拟地址”因为我们平时看到的一切指令访问、变量寻址、代码跳转)全部都是虚拟地址Virtual Address)。这些地址全部都是进程自己的虚拟地址空间里的编号。进程永远不知道物理内存的真实物理地址它只知道我在我的虚拟地址空间里运行。操作系统通过页表(Page Table把进程的“虚拟地址”映射到硬件的“物理地址”。疑问2 : 为什么动态库(.so 文件)会被映射到进程虚拟地址空间中的共享区?首先我们要清楚每一个进程都有自己独立的进程虚拟地址空间。每一个进程也都有自己独立的代码区、数据区、堆区、栈区以及自己的共享区(动态库映射区。举个例子 : printf 函数属于库函数它是在动态库中的因为 printf 这种函数同时可能会被多个进程使用并且它自己又不是一个独立程序所以动态库文件必须自己先加载到物理内存只存一份省空间)再让每个进程把它“映射进自己的虚拟地址空间”也就是共享区)这样每个进程都能通过自己的虚拟地址透过页表去访问这同一份动态库。所以会被映射到各个进程的虚拟地址空间中的共享区中。库函数调用的本质当我们的程序运行在自己的虚拟地址空间里正常执行自己的代码。当执行到 printf 这种库函数时程序就从自己的代码区跳到共享区里去找 printf 的实现。库已经被映射到这个虚拟地址空间里了所以调用就是在同一个地址空间内直接跳转。整个过程都在你进程的内存里不需要跳出也不需要切换到别的空间。这就是库函数调用的本质。动态库的相对地址动态库为了随时进行加载为了支持并映射到任意进程的任意位置对动态库中的各个方法进行统一编址采用相对编址的方案进行编制的 (其实可执行程序也一样都要遵守平坦模式只不过exe是直接加载的)。进程的虚拟地址空间角度: 动态库在编译时不是固定在某个虚拟地址的。它假设自己是从地址 0 开始的里面所有函数、变量的地址都是相对于基址的偏移量。例如puts 距离库起始地址的偏移 0x1234printf 距离库起始地址的偏移 0x5678库内部的跳转指令都是基于这个相对偏移所以这个地址也叫做逻辑偏移地址。进程加载库时只要给它一个“基址”它就能运行当动态库被加载到某个进程的共享区时内核给它分配一个虚拟基址比如 0x7ffff7a00000)。库内部所有函数的真实虚拟地址 基址 偏移量所以库可以被加载到任何进程的任何虚拟地址因为它内部只用偏移不关心基址这就是动态库的核心。这是在进程的角度来讲的因为是在进程的虚拟地址空间中。动态库本身角度: 同样在动态库本身的视角来看动态库本身在物理内存中也是同样逻辑在物理内存中也是库的物理基址 偏移量 物理地址但进程永远不会直接用物理地址它只通过虚拟地址 页表去查物理地址。所以物理层同样用相对偏移只是进程不知道。使用这种编址方式的目的就是为了实现动态库的共享与复用如果不用这种相对偏移的方式动态库就必须被加载到固定的内存地址。一旦多个程序同时需要加载动态库就很容易出现地址冲突而且也无法实现多个进程共用同一个动态库实例的效果。而通过“基址 偏移”的方式无论动态库被加载到哪个进程的哪个内存地址只要知道了基址和内部偏移就能精准定位到目标内容实现了一次编译到处运行共享复用。举个例子:这里的 0xe 就是相对偏移地址。它的含义就是 “从 run 函数的起始地址开始往后偏移 0xe 个字节就是我要跳的目标。它又和前面的机器码里的 e8 00 00 00 00 有什么关系?e8 是 call 指令的操作码告诉 CPU 这是一个调用。后面那四个 00 00 00 00是占位符。它的含义是“这里本来应该填一个地址但现在还不知道填什么先填 0 占位等后面来补。”在 .o 文件里编译器只知道相对偏移所以生成了 0xe。但它不知道 run 函数的最终绝对地址所以把跳转地址写成了 00 00 00 00 。随后在链接阶段链接器linker会确定 run 函数的最终绝对地址。用这个绝对地址加上 0xe计算出目标地址。把计算出的目标地址替换掉原来的 00 00 00 00 。比如假设链接器确定 run 函数的起始地址是 0x400500 。偏移量是 0xe。那么目标地址 0x400500 0xe 0x40050E。链接器会把机器码里的 00 00 00 00 替换成 0x40050E 对应的字节。这样CPU 执行的时候就能直接跳到 0x40050E 了。图二:这张图完整地还原了动态库从磁盘到进程运行的全链路。我们按照 4 个步骤把这个过程从头到尾拆解开:阶段一 磁盘定位与加载准备阶段)在进程运行前库文件静静地躺在磁盘里进程还没出生。首先是内核要先找到库文件当我们运行程序时内核根据程序的依赖信息比如 libc.so去磁盘上查找。这一步会通过 dentry (目录项) 和 inode索引节点找到磁盘上的库文件数据块。inode 就像库的“身份证”记录着库在磁盘上的具体位置。接下来库就要被加载进物理内存中内核 libc.so 从磁盘读取到物理内存中。此时内核会维护一个内核缓冲区。如果是第一个进程加载 libc它会从磁盘读入内存如果是第二个进程加载直接用内存里已有的不重复读磁盘。现在物理内存里有了一份完整的 libc 代码和数据。阶段二建立映射核心关联)现在库在物理内存里但进程还看不见它。内核必须建立一座“桥”。这座桥就是页表映射操作系统为当前进程建立页表。把物理内存里的 libc 代码页和进程虚拟地址空间里的“共享区”页框对应起来。这就是“映射”。通过页表进程的虚拟地址就有了通往物理内存库数据的路径。然后就会获取库的虚拟基址操作系统给每个进程分配一个随机的虚拟地址作为这段共享区的起始点(基址。比如上图中假设映射后库的起始虚拟地址是0x44332211。阶段三链接与执行运行时刻)现在映射建好了但程序里的 call 指令还不知道怎么跳。下面就涉及到指令地址计算在编译期我们的代码里写了 call puts。编译器不知道 puts 在哪只知道 puts 相对于库的基址的偏移量。puts 距离库起始地址的偏移是 0x112233 。此时汇编代码显示为call libc.so0x112233 。然后就是运行时动态链接阶段最终地址)程序运行到这一行指令时动态链接器ld-linux)介入。它把刚才分配好的库虚拟基址(0x44332211加上函数偏移量(0x112233 )。最终计算出真实地址0x44332211 0x112233 0x45454544。原来的 call 指令被修补变成了直接调用这个计算出的绝对虚拟地址。阶段四CPU 执行指令CPU 拿到计算好的真实虚拟地址。通过 MMU内存管理单元)查页表。页表告诉 CPU 这个虚拟地址对应的是物理内存里那一份 libc 代码。CPU 执行库里的 puts 函数打印内容。我们在看这个图我有个疑问就是如果有多个库文件同时在物理内存中那操作系统会对这些库文件进行管理吗?怎么管理的?答案是会管理的而且同样也是按照先描述再组织的方式进行管理库文件(比如 libc.so)在物理内存中是通过 vm_area_struct 来管理的但它管理的是“库的内容”而不是“库的位置”。也就是说物理内存中的库文件是被当作一段“数据”来管理的。这段数据的管理依赖于 vm_area _struct 这个结构体。但是vm_area_struct 本身是属于“进程”的而不是属于“库文件”的。PCB 里存 mm_structmm_struct 存进程的“虚拟地址空间信息”虚拟地址空间分成很多区(代码区,数据区)每个区用一个 vm_area_struct 描述。vm_area_struct 描述的是“进程虚拟地址空间里的一段区域”不是描述库本身。动态库如 libc.so被映射进进程的虚拟地址空间后内核会给它分配一段 vm_area_struct。这个 vm_area_struct 结构体通过可以用来管理每个库文件但是本质上是属于进程的不是属于库的。先描述 : 库文件(比如 libc.so)被加载到物理内存后系统会为它创建一个 vm_area_struct 结构体这个结构体是用来描述库的内容库的属性库的位置。简单说物理内存中的库文件就是通过 vm_area_struct 来描述的就像你进程中的代码区、数据区一样。再组织 : Linux 用“双向链表 红黑树”组织进程的所有 vm_area_struct 结构体。链表用于遍历红黑树用于快速按地址查找。这就是底层的组织形式。动态链接的本质动态链接的本质是将符号解析与地址重定位的核心工作从编译链接阶段推迟到程序加载或运行阶段通过动态加载器完成动态库的查找、加载与地址修正可执行文件仅保留对库函数的符号引用与依赖信息不直接复制库代码动态库以位置无关代码设计可被加载到进程虚拟地址空间的任意位置运行时再完成符号绑定与地址重定位最终实现多进程间代码共享、内存占用节省以及库文件的独立更新让程序与库实现解耦。这里我们需要注意的是静态链接和动态链接都涉及到符号解析与地址重定位只是时机、方式和场景完全不同。静态链接中的符号解析与重定位是在编译后的链接阶段生成可执行文件时完成。链接器扫描所有.o文件和静态库找到每个符号比如外部函数、全局变量的定义位置。为所有代码和数据段分配固定虚拟地址。将之前编译时留下的占位地址比如call 0x0替换为真实的虚拟地址。最终可执行文件包含所有代码运行时不再需要任何链接操作。动态链接中的符号解析与重定位是在推迟到程序加载时或运行时由动态链接器完成。可执行文件只保留符号名和依赖库信息运行时动态链接器如ld-linux.so去查找.so文件解析符号对应的地址。动态库被加载到进程地址空间的任意位置基地址不固定。根据实际加载基地址修正库内的函数跳转、变量访问地址。同时修正可执行文件中对库函数的调用地址。程序和库是分离的运行时才完成地址绑定。四、动静态库的对比对比维度静态链接Static Linking动态链接Dynamic Linking链接时机编译链接阶段生成可执行文件时程序加载 / 运行时库代码处理直接复制到可执行文件中不复制只保留符号引用符号解析 重定位链接阶段一次性完成推迟到加载 / 运行时完成可执行文件大小较大较小运行依赖完全独立不依赖任何库文件必须依赖外部动态库.so/.dll运行时加载库不加载任何库必须加载并映射动态库内存共享不共享每个程序独占库代码多进程共享同一份库代码库更新必须重新编译程序直接替换库文件即可生效静态链接编译时拷贝 定位运行时完全独立动态链接运行时加载 定位运行时依赖库文件五、总结本文系统讲解了程序链接与加载的核心机制。静态链接在编译阶段完成符号解析与地址重定位将库代码直接复制到可执行文件中动态链接则推迟到运行时通过动态加载器完成库加载和地址绑定实现多进程共享。文章详细分析了.o文件与可执行文件的ELF格式差异通过符号表解析了静态链接的地址重定位过程并深入剖析了动态库从磁盘加载到内存映射的全链路机制。最后对比了动静态链接在链接时机、内存共享、更新维护等方面的本质区别揭示了现代操作系统管理程序执行环境的底层原理。谢谢大家的观看!