Appearance
程序的链接与装入
2026 大纲 一(三)4 程序的链接与装入。
编译器写代码的时候,并不知道这段代码将来住在哪
前面四节讲的是 OS 怎么管住正在跑的程序。这一节换个角度往前看一步: 一个程序是怎么从源代码变成"能跑的东西"的——而这中间横着一个绕不开的麻烦。
编译程序处理每个源文件时,并不知道这个模块最终会被放到内存的什么位置, 也不知道它前面还会拼进来多长的其他模块。它只能假定"我从 0 开始"。 于是每个目标模块内部都是一套从 0 编号的地址,而它们最终要挤进同一块内存里。
这就必须改两次地址,而且是两个不同层次上的两次:
第一次是链接:把多个模块首尾相接拼成一个,那么除第一个之外, 其余模块内部的地址全是错的,得整体挪。挪完之后整体仍然从 0 编号。 第二次是装入:这个从 0 编号的整体在内存里的实际起点并不是 0,还要再挪一次。
这两次改址解决的不是同一个问题——链接管"模块之间",装入管"整体相对于物理内存"。 把它们混成一层,后面三种链接方式、三种装入方式的区别就全都对不上了。
这一节的落点在最后一步:装入时到底把地址写死没有。写死了程序就再也不能移动, 而"不能移动"会一路影响到内存管理那一章能用哪种分配方式。
一、前提与产物
编译程序处理每个源文件时,并不知道这个模块最终会被放到内存的什么位置,也不知道它前面还会拼进来多长的其他模块,所以只能从 0 编号。于是:多个模块拼成一个装入模块时,除第一个外其余模块的内部地址全是错的,必须重新编号(链接干的事);拼好后整体仍从 0 编号,而内存中的实际起点不是 0,还要再挪一次(装入干的事)。
链接产物不是一段裸机器码:
| 组成 | 内容 | 服务于哪一步 |
|---|---|---|
| 代码段(.text) | 机器指令,只读 | 装入内存后成为进程映像的代码段 |
| 数据段(.data) | 已初始化的全局/静态数据 | 装入时按原样搬进内存 |
| BSS 段说明 | 未初始化的全局/静态数据,只记长度不存内容 | 装入时由系统清零开辟,不占文件空间 |
| 符号表 | 本模块定义了哪些符号、引用了哪些外部符号 | 链接时解析外部引用要靠它 |
| 重定位表 | 哪些位置上的地址将来需要被修改 | 链接与装入改地址时按表逐条改 |
装入程序不需要"看懂"指令,只要按重定位表列出的偏移量逐个加上一个增量即可——这也解释了静态重定位虽然要改很多处却并不昂贵:改的是一张表上列好的位置,不是全文扫描。
二、链接程序做的三件事
设三个模块 A、B、C 长度分别为 L、M、N,按 A→B→C 拼接:
① 装配库函数——printf、sqrt 在自己的目标模块里只是未定义的外部符号,真正的实现在库里,链接程序把用到的取出来一并拼进去。 ② 相对地址重编址——B 的起点是 L、C 的起点是 L+M,于是原 B 中所有相对地址统一加 L、原 C 中统一加 L+M。 ③ 外部符号引用解析——A 里的 CALL B 此刻还只是符号名,链接程序查符号表得知 B 的起点是 L,把目标地址改写成 L;B 里的 CALL C 改写成 L+M。

图源:汤小丹《计算机操作系统》图 4-4(b) 装入模块,p125
链接与装入两级改址完整走一遍:一条跨模块引用的地址是怎么被改两次的(想在具体数字上看清两层改址各改了什么时展开)
模块 A、B、C 长度分别为 800 B、1200 B、500 B,按 A→B→C 静态链接。模块 B 内相对地址 300 处有一条指令,引用模块 C 起始处的一个数据符号。
第 1 步:算各模块在装入模块中的起点。 链接就是把模块首尾相接排成一条一维地址线,某模块的起点等于它前面所有模块长度之和:B 的起点 = 800,C 的起点 = 800 + 1200 = 2000。
第 2 步:重编址。 B 内部的 300 是"相对于 B 的起点",换算成"相对于装入模块的起点"要补上 B 起点这段偏移 → 指令的逻辑地址 = 800 + 300 = 1100。
第 3 步:解析外部符号。 外部符号在编译期无法定值,只有等 C 被摆到确定位置后才能换算成具体逻辑地址 → 该引用被改写成 2000。
第 4 步:静态重定位装入到物理地址 10000。 装入模块整体从 0 编号,装到 10000 处等于整条地址线右移 10000,按重定位表逐条 +10000:指令所在物理地址 = 11100,取数的物理地址 = 12000,且这个 12000 被直接写回到内存中的这条指令里。
第 5 步:改用动态重定位,程序被紧凑移到 30000。 内存中的指令里存的仍然是 2000(逻辑地址),只需把重定位寄存器由 10000 改成 30000,下一次访存时硬件自动算出 30000 + 2000 = 32000,内存中的指令一个字节都不用改。动态重定位把"加基址"从装入期推迟到每次访存,基址存在一个可随时改写的寄存器里,移动程序的代价就从"改遍全部地址"降成了"改一个寄存器"。
三、三种链接方式
判据是外部模块是在哪个时点被拼进来的:
| 方式 | 拼进来的时点 | 换来了什么 | 付出了什么 |
|---|---|---|---|
| 静态链接 | 运行之前,一次拼完,以后不再拆开 | 装入即可运行,无运行期开销 | 可执行文件大;每个用到该库的程序都含一份拷贝;改一个模块要重新链接整个文件 |
| 装入时动态链接 | 装入内存时边装边链接 | ① 便于修改和更新——各目标模块分开存放,改一个不必重开整个装入模块;② 便于实现对目标模块的共享——OS 可把同一个目标模块链接到多个应用上 | 装入过程变慢;用不到的模块照样被装进内存 |
| 运行时动态链接 | 执行中真正调用到该模块时才链接 | 加快装入过程,并节省大量内存 | 首次调用有链接延迟;需要运行期的链接支持 |
四、三种装入方式
| 比较 | 绝对装入 | 静态重定位 | 动态重定位 |
|---|---|---|---|
| 判据:指令里留下的是什么地址 | 物理地址(编译时就是) | 物理地址(装入时改成的) | 逻辑地址(一直没改) |
| 地址转换时机 | 编译/汇编时 | 装入时,一次性 | 运行时,每次访存 |
| 装入后可否移动 | 否 | 否 | 是 |
| 是否必须一次分配全部内存 | 是 | 是 | 否(可配合虚拟内存按需调入) |
| 需要的硬件 | 无 | 无 | 重定位寄存器 + 界地址寄存器 |
| 配套的内存分配方式 | 单一连续分配 | 固定分区 | 可变分区、紧凑、离散分配 |
| 适用系统 | 单道 | 早期多道 | 现代 OS |
绝对装入要求事先知道程序驻留在哪,只有单道程序环境做得到;程序一旦被修改导致长度变化,可能要改动其中所有地址。
静态重定位留下三条硬约束,后两条比"不能移动"更致命:① 装入后不能再移动——指令里留下的已是最终物理地址;② 必须在装入时一次性分配全部内存——地址已被写死,每个字节将来落在哪装入那一刻就得全部定下来;③ 运行期不能再申请内存——新申请的空间没出现在装入时的地址换算里。
动态重定位靠两个寄存器配合:
重定位寄存器(基址寄存器)存本程序在内存中的起始物理地址;界地址寄存器(限长寄存器)存逻辑地址空间的长度,访存前先比较。只记住加法那一半,就解释不了"越界为什么会被发现"。
五、装入方式决定了内存能怎么分配
可变分区用久了,内存被切成许多互不相邻的小空闲区,它们的容量总和可能远大于新作业的需求,却因不相邻而无法使用。唯一的解法是把所有作业移动到一起使其相邻接,即紧凑(拼接)。而移动就意味着物理位置变了,于是形成一条因果链:
反过来说,在只有静态重定位的系统里紧凑在工程上不成立,可变分区一旦碎片化就只能干等着。
考点速记
- 目标模块内部一律是从 0 开始的相对地址,这是两级改址的共同前提。链接摆平模块之间的相对位置(重编址 + 解析外部符号,做完仍是逻辑地址),装入摆平装入模块整体相对于物理内存的位置。两者不在同一层。
- 链接程序做三件事:装配库函数、相对地址重编址、外部符号引用解析。后两件必须同时做——只做重编址,调用指令还指着旧的 0;只做符号解析,模块内部跳转全部错位。
- 三种链接的差别在"外部模块何时拼进来"。运行时动态链接最省内存,根因是大量分支(最典型是错误处理模块)本次运行一次也不执行;库能被共享的前提是它必须是纯代码(可重入、不改自身)。
- 三种装入的判据是"指令里留下的是不是最终物理地址"。动态重定位需重定位寄存器 + 界地址寄存器,缺了后者地址照样算得出来,但越界查不出来——内存保护失效。
- 紧凑只有在动态重定位下才成立:静态重定位下每移动一次就要把全部写死的地址逐条重算,开销抵消收益。所以静态重定位只能配合固定分区。
- 编译程序、链接程序、命令解释程序都在用户态;装入程序在内核态。
这一节在真题里被考过的形式:
这一节至今只单独出过一道题,且考的不是链接装入的机制本身,而是"这几个程序各在哪一态"。 它的主要价值在于给内存管理那一章打地基—— 重定位寄存器、界地址寄存器、紧凑这几样都是在那里被反复使用的。
- 问编译、链接、装入、命令解释四个程序中哪个在内核模式执行(2026-23)。答装入程序。判据是"要不要直接动页表、物理内存或特权指令"——编译和链接只是读写文件做数据加工,命令解释程序自己也只是个普通进程,三者都在用户态;只有装入程序要分配物理页框、建立页表、初始化 PCB,它本质上是内核的一部分。⚠️ "听起来和 OS 相关"不是证据,链接程序也和 OS 密切相关,但它在用户态。
复习优先级:理解为主,不必抠细节。本节的三种链接、三种装入至今没有单独成题, 但它们的结论会在内存管理章被当作前提使用——尤其是"动态重定位 ⇒ 程序可移动 ⇒ 紧凑可行" 这条因果链,以及界地址寄存器的作用。把这两处和上面第六条记住即可。
易错:认为链接程序在内核态执行。它只做文件读写和符号计算,在用户态;装入程序才在内核态。
易错:把链接改址和装入改址当成同一层。链接后整体仍是从 0 编号的逻辑地址,装入才补上内存起始地址。
易错:只记住动态重定位的加法公式。界地址寄存器同样必需,缺了它越界访问查不出来。
易错:认为静态重定位下也能做紧凑。移动一次就要把全部写死的地址逐条重算,工程上不成立。
易错:认为动态链接库天然可共享。前提是它必须是纯代码——不修改自身、不在代码段保存跨调用状态。
教材出处
- 汤小丹《计算机操作系统》4.2.1 程序的装入(p123~p124):绝对装入、可重定位装入与静态重定位、动态运行时装入的定义与限制。
- 同书 4.2.2 程序的链接(p124~p126):链接程序的功能是"将一组目标模块及其所需库函数装配成一个完整的装入模块";静态链接需解决"对相对地址进行修改"与"变换外部调用符号"两个问题;装入时动态链接的两个优点(便于修改和更新、便于实现对目标模块的共享);运行时动态链接以错误处理模块为例说明未被执行的模块不会调入内存。图 4-4 程序链接示意图见 p125。
- 同书 4.3.6 动态可重定位分区分配(p133~p134):紧凑的定义,以及"每紧凑一次就要对移动了的程序或数据的地址进行修改,这不仅麻烦而且大大影响系统效率"、动态重定位下"只要用该程序在内存的新起始地址去置换原来的起始地址即可"。
- 孙钟秀、费翔林《操作系统教程》(第 6 版)3.1.2 存储管理的功能(p84):静态地址重定位与动态地址重定位的对照,动态地址重定位"具有允许程序的主存移动、便于程序共享、主存利用率高等优点"。