技术

ICS 与 8086 复习 16:装载运行与静态动态链接

ICS 与 8086 汇编复习 · 第 16 篇 系列目录 · 上一篇 · 下一篇

链接已经完成,为什么启动程序时还可能提示找不到共享库?为什么一个可执行文件通常不是从 main 的第一条指令直接开始?这篇把系列末尾的几个环节接起来:链接选取哪些内容,加载器建立什么映像,启动代码怎样把控制交给程序。

本文使用 Linux ELF 语境。地址和段大小算例都是明确给定的教学条件;实际地址会受二进制选项、系统配置和地址空间随机化影响。

静态库是目标文件的集合

静态库 .a 通常是多个 .o 的归档。链接器根据当时尚未解决的引用,从库里选取需要的目标成员;通常不是把整个库无条件复制到程序中。

考虑依赖关系:main.o 调用库中的 f,定义 f 的成员又调用另一成员的 g。传统静态归档扫描中,命令行顺序会影响能否找到定义:

gcc main.o -lx -ly -o demo

若 libx.a 提供 f、f 需要的 g 由 liby.a 提供,这个次序可以让对 g 的新引用在扫描 y 时得到解决。反过来先扫描 y,扫描时尚无对 g 的需要,该成员可能没有被选入;后来扫描 x 才出现对 g 的引用,就可能留下未解析符号。

一种复习方法是在纸上维护集合 U:遇到引用,加入未解析名字;选入提供定义的成员后,从 U 删除相应名字,并加入它引出的新引用。对象文件、归档内部反复搜索和库组会影响具体过程,因此题目应给清所用链接规则。GNU ld 归档选项说明可用来核对传统扫描和库组机制。

共享库把一部分绑定推迟到运行环境

共享库 .so 可以被多个程序使用。可执行文件通常记录依赖关系,并通过动态链接器在装载时或运行过程中完成需要的查找和重定位。

比较项 静态链接库 动态共享库
形成程序时 选中的目标成员进入输出文件 保留共享库依赖和有关引用
部署时 已静态包含部分不另依赖该共享库文件 运行环境须提供兼容共享库
更新某库后 已包含旧代码的程序通常需重新链接 可能使用更新库,仍须接口兼容
内存共享 不因“静态库”名称自动共享 相同文件的只读代码页可被共享

共享不表示多个进程共用一份可写全局变量状态。正常情况下,每个进程仍具有各自的地址空间和可写数据语义。

装载时动态链接由解释器与运行时机制处理;程序主动使用 dlopen、dlsym 等接口则属于运行时动态加载。库存在于磁盘,也不保证版本、架构和搜索路径一定匹配。

PIC GOT 和 PLT 分别解决什么

共享库装到哪里,不能总在构建时写死。位置无关代码 PIC 的目的,是让代码可在不同装载位置使用,减少需要直接修改共享代码页的绝对引用。

GOT 是全局偏移表,可保存符号最终地址等信息;PLT 是过程链接表中的代码入口,常与 GOT 和动态链接器协作完成外部函数调用。在支持延迟绑定的实现里,第一次调用可能触发解析,后续调用利用已经更新的地址信息。

这些是课程需要掌握的机制关系。GOT 项数量、PLT 每项字节数、是否延迟绑定以及寄存器用法,都与架构、工具链和选项有关。不能把某张 IA32 示例表的布局当作所有 ELF 文件的固定格式。

从程序头计算装载后的范围

加载器主要依据 ELF 的程序头建立映像,而不是逐个按节名寻找 .text、.data。对一个可装入段,文件中长度为 p_filesz,内存中长度为 p_memsz。后者更大时,增加的部分需要按规定清零。

设一个教学段具有下列字段,且本例装载偏移为零。PIE 和共享目标的实际运行时地址通常还需加上相应装载偏移:

p_offset = 1000H
p_vaddr  = 08049000H
p_filesz = 600H
p_memsz  = A00H

第一步,文件数据来自半开区间 [1000H,1600H),共 600H=1536 字节。第二步,映射后的对应数据位于 [08049000H,08049600H)。第三步,内存总范围延伸到 08049A00H,所以 [08049600H,08049A00H) 共 400H=1024 字节为零初始化尾部。

这个算例忽略了页对齐等外围细节,专门考查“文件大小”和“内存大小”的区别。它也解释了为什么 .bss 对应的数据可以在文件中不存放一大串零,却仍能在进程内存中存在。ELF 装载规范定义了这些字段。

execve 到 main 中间还有什么

execve 成功时替换调用进程的程序映像,并不会因为名字里有 execute 就自动新建进程。Shell 启动外部命令的常见过程涉及先创建子进程,再由子进程执行 execve;这与 execve 自身的作用要区分。

对带 PT_INTERP 的动态 ELF,系统会安排程序解释器,也就是动态链接器,参与启动;完成依赖库加载和必要处理后再转向主程序入口。可执行文件入口常对应 _start 一类启动代码,运行时准备完成后才调用 main。具体入口流程受文件类型和运行时实现影响,可查 Linux execve 文档。

CPU 随后不断按控制流取指、译码和执行。程序计数器的顺序增量是指令长度;IA32 是变长指令,不能把教学模型中的 PC+1 理解为永远加一个字节。调用、分支、返回以及异常等事件会改变正常流向。

库打桩需要区分发生阶段

课件将函数替换机制分为编译时、链接时和运行时。宏替换在编译前处理源文本;链接器 --wrap 改变特定引用的解析;LD_PRELOAD 可在符合条件的动态链接场景中影响符号搜索。

它们都能用于观测或替换调用,但作用范围不同。被编译器内联的调用、某些内部绑定符号或受限制的运行环境,不一定能被某一种机制截获。复习的重点是辨认发生阶段和引用如何改向,不是记住一个环境变量就认为它能替换任何函数。

自测与简答

  1. libx.a 出现在命令行最前面,为什么仍可能无法解决后面 main.o 对 x 中函数的引用?
  2. p_memsz=1000H、p_filesz=700H,零初始化尾部有多少字节?
  3. execve 成功后,原程序是否继续从下一条 C 语句运行?

答案:① 传统扫描到该归档时可能尚未出现待解析引用,需要成员便没有被选入;应按依赖安排次序或使用适当库组。② 900H=2304 字节。③ 不会,成功调用替换了程序映像;失败时才返回并报告错误。

资料对应

主要依据《5.程序的链接与加载执行-V1.0.pptx》第 51—59、79—97、98—113 页;系统总体运行过程可回看《1.前言&第一章计算机系统概述-V1.0.pptx》第 69—96 页。段范围计算为自编算例,未把课件历史地址图当成当前系统的固定布局。

系列导航

CONTINUE / PATH

从这里继续

顺着当前内容继续,或者换到另一个板块。

讨论这篇文章

关于《ICS 与 8086 复习 16:装载运行与静态动态链接》的补充、不同意见或延伸话题,都可以留在这里。

滚动到这里时自动载入留言。