当前位置: 首页 > web >正文

【从汇编语言到C语言编辑器入门笔记9】 - 链接器的执行过程

链接器

链接器(Linker):程序编译的“最后一块拼图”

一、链接器的核心定义与作用

链接器(Linker)是编译流程的关键工具,负责将编译器/汇编器生成的目标文件(.obj/.o,包含机器码、符号表、重定位表)与库文件(静态库.a/.lib、动态库.so/.dll)组合成可执行文件(.exe/.out)或共享库。其核心作用是解决两个关键问题:

  1. 符号关联:将程序中的符号引用(如函数调用printf()、变量使用global_var)与符号定义(如printf的实现、global_var的声明)关联起来。
  2. 地址调整:将目标文件的相对地址(如假设代码从0x0000开始)转换为绝对地址(如实际运行时的0x8048000),确保程序运行时能正确访问代码和数据。
二、链接器的工作流程

链接器的工作可分为四大核心步骤,逐步将分散的目标文件整合成可执行文件:

假设我们有 3 个文件,分别是:

  • main.c(主程序,调用自定义函数add和标准库函数printf
  • add.c(实现add函数)
  • 系统标准库libc.so(包含printf函数的实现)
// main.c
#include <stdio.h>
extern int add(int a, int b); // 声明外部函数add(仅引用,未定义)int main() {int res = add(2, 3);printf("Result: %d\n", res); // 引用printf(未定义)return 0;
}
// add.c
int add(int a, int b) { // 定义add函数return a + b;
}

编译过程:gcc -c main.c add.c 生成 main.oadd.o(目标文件),再通过 gcc main.o add.o -o app 调用链接器生成可执行文件 main

1. 符号解析(Symbol Resolution):解决“找不到定义”的问题

符号解析是链接器的第一步,目标是处理所有未解析符号(Unresolved Symbols),即程序中引用但未定义的符号(如调用了一个未实现的函数)。

  • 核心逻辑:链接器维护三个集合:
    • E(Executable Set):待合并的目标文件集合(初始为空);
    • U(Unresolved Symbols):未解析的符号引用集合(初始为空);
    • D(Defined Symbols):已定义的符号集合(初始为空)。
  • 处理流程:
    • 逐个处理输入文件(目标文件或库文件):
      • 目标文件:将其加入E,遍历其符号表,将定义的符号(如main函数、global_var变量)加入D,将未解析的符号引用(如调用了printf)加入U
      • 库文件(如静态库.a):遍历库中的每个目标模块,检查其定义的符号是否能匹配U中的未解析符号。若匹配,将该目标模块加入E,并更新U(移除匹配的符号)和D(添加该模块的符号)。
    • 终止条件U为空(所有符号都找到定义),否则链接失败(报错“未定义符号”,如undefined reference to printf)。
  • 以main.o和add.o为例:
    • main.o的符号表中,add和printf是 “未定义符号”(U 集合),main是 “已定义符号”(D 集合);
    • 处理add.o时,链接器发现其定义了add(加入 D 集合),此时 U 集合中的add被移除;
    • 处理 C 标准库libc.so(动态库)时,链接器发现其中定义了printf(加入 D 集合),U 集合中的printf被移除;
    • 若遗漏add.o(如编译命令写成gcc main.o -o app),链接器会报错:undefined reference to ‘add’(U 集合非空,解析失败)
  • 补充:符号解析中 “多重定义” 的实际案例 —— 若在main.c中也定义int add(int a, int b) { return a*b; },链接器会报multiple definition of 'add'(D 集合中出现重复符号),因为main.oadd.o都定义了add
2. 重定位(Relocation):解决“地址不正确”的问题

目标文件是独立编译的,使用的是相对地址(如假设main函数在0x100位置),但实际运行时,程序会被加载到内存的绝对地址(如0x8048000)。重定位的目的是调整这些地址引用,确保程序运行时能正确访问代码和数据。

  • 核心步骤:
    • 分配地址空间:链接器为每个目标文件的(Section)分配绝对地址(如将目标文件A的.text段(代码段)分配到0x8048000-0x8048FFF,目标文件B的.text段分配到0x8049000-0x8049FFF);
    • 修补地址引用:遍历目标文件的重定位表(Relocation Table),找到需要调整的指令(如函数调用的跳转地址、变量的加载地址),将其从相对地址改为绝对地址(如将call 0x100改为call 0x8048100)。
  • main.o中调用add的指令为例:
    • 编译生成的main.o中,call add的机器码是e8 fc ff ff ff(十六进制),其中fc ff ff ff是补码表示的-4(占位符,实际是 “待修正” 标记);
    • 链接器分配地址时,假设main函数起始地址为0x400500add函数在add.o.text段中,合并后地址为0x400550
    • 重定位时,链接器计算call指令的实际偏移:指令所在地址(0x400518)到add地址(0x400550)的距离是0x38(56),因此将call指令修正为e8 38 00 00 00(机器码对应偏移0x38),确保执行时能正确跳转到add
  • 补充:重定位类型影响地址计算方式 ——x86 的R_386_PC32类型(PC 相对寻址)计算 “指令地址到符号地址的偏移”,而R_386_32类型(绝对寻址)直接使用符号的绝对地址(如访问全局变量时)。
3. 段合并(Section Merging):减少内存碎片化

目标文件包含多个(如.text代码段、.data初始化数据段、.bss未初始化数据段),链接器会将相同类型的段合并(如所有目标文件的.text段合并成可执行文件的.text段,.data段合并成.data段)简单来说就是有一堆零散的小文件,将它们中的同类文件整理到同一个文件夹。

  • 目的:
    • 减少内存碎片化(连续的段更易被缓存);
    • 简化操作系统加载(只需加载几个大段,而非多个小段)。
  • main.oadd.o为例:
    • 合并前:main.o.text(30 字节)、.rodata(12 字节);add.o.text(20 字节)、.data(0 字节);
    • 合并后:可执行文件的.text段是两者.text的总和(50 字节,地址连续),.rodata段保留 12 字节(仅main.o有);
    • 效果:操作系统加载时只需读取 2 个大段(.text.rodata),而非 4 个小段,减少 IO 次数,且连续地址更易被 CPU 缓存命中。
  • 关键补充:段合并时会按 “权限” 分组 ——.text(可执行)、.data(可读写)、.rodata(只读)等,确保内存保护机制(如.text段不可写,防止代码被篡改)。
4. 生成可执行文件:输出“能运行的程序”

链接器将合并后的段、重定位后的指令、符号表(可选,用于调试)等信息写入可执行文件,遵循目标平台的文件格式(如Windows的PE格式、Linux的ELF格式、macOS的Mach-O格式)。

  • 关键输出:
    • 入口点地址(如main函数的地址):操作系统加载程序时的起始执行位置;
    • 段表(Section Table):记录可执行文件的段信息(如.text段的起始地址、大小);
    • 依赖库列表(动态链接时):记录程序需要加载的动态库(如libc.somsvcr100.dll)。
三、链接器的类型:静态链接 vs 动态链接

链接器分为静态链接(Static Linking)和动态链接(Dynamic Linking),二者的核心区别在于库文件的处理方式

1. 静态链接(Static Linking):“把库打包进程序”
  • 原理:将静态库(.a/.lib)中的函数代码直接复制到可执行文件中,生成独立的可执行文件(不依赖外部库)。
  • 优点:
    • 运行稳定(不会出现“库缺失”错误,如Windows的“找不xxx.dll”);
    • 执行速度快(无需动态加载库)。
  • 缺点:
    • 可执行文件体积大(包含所有库代码,如静态链接的hello程序可能比动态链接大10倍);
    • 库更新麻烦(如库修复了一个bug,所有使用该库的程序都要重新链接)。
  • 例子gcc -static main.c -o main(生成静态链接的可执行文件)。
2. 动态链接(Dynamic Linking):“运行时再找库”
  • 原理:不复制库代码到可执行文件,而是在程序运行时加载器(Loader)加载所需的动态库(.so/.dll/.dylib),解析符号引用。
  • 优点:
    • 可执行文件体积小(仅包含程序自身代码和库依赖信息);
    • 库更新方便(只需替换动态库文件,程序无需重新编译);
    • 节省内存(多个程序可共享同一个动态库的内存副本,如libc.so被所有Linux程序共享)。
  • 缺点:
    • 运行时依赖外部库(若库缺失或版本不兼容,程序无法运行,如“error while loading shared libraries: libhello.so: cannot open shared object file”);
    • 执行速度略慢(需要动态加载和解析符号)。
  • 例子:
    • 生成动态库:gcc -shared hello.c -o libhello.so
    • 动态链接:gcc main.c -ldl -o main(生成依赖libhello.so的可执行文件);
    • 运行:./main(加载libhello.so并执行)。

两者的核心差异在于 “库代码是否被打包进可执行文件”,结合实际命令和文件大小对比更直观:

场景静态链接(gcc -static main.c add.c -o app_static动态链接(gcc main.c add.c -o app_dynamic
文件大小约 800KB(包含libc库的代码,如printf实现)约 16KB(仅包含自身代码,printf依赖libc.so
运行依赖无(可复制到任何同架构 Linux 系统直接运行)依赖系统中的libc.so.6(缺失则报错:error while loading shared libraries: libc.so.6: cannot open shared object file
库更新libc修复漏洞,需重新编译链接app_static只需替换系统中的libc.so.6app_dynamic无需重新编译
内存占用每个进程加载时都复制一份libc代码,占用内存多所有进程共享同一份libc.so的内存副本,节省内存

实际案例:嵌入式设备(如路由器)常用静态链接,避免 “库缺失” 问题;PC 应用常用动态链接,减小安装包体积。

四、链接器的关键概念
  • 符号表(Symbol Table):目标文件中的符号记录(如定义的符号、未解析的符号),是链接器进行符号解析的依据;
  • 重定位表(Relocation Table):目标文件中的地址调整记录(如哪些指令中的地址需要修改),是链接器进行重定位的依据;
  • 库文件(Library):包含预编译函数的文件(静态库是.a/.lib,动态库是.so/.dll),是程序依赖的“外部代码”。
五、关键概念补充:用工具验证链接结果

通过工具可直观看到链接器的输出结果:

  • 查看可执行文件的依赖库(动态链接):ldd app_dynamic,会显示libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
  • 查看符号表(静态链接):nm app_static | grep printf,会显示00401234 T printfT表示符号在.text段,已被打包);
  • 查看段信息:readelf -S app_dynamic,可见合并后的.text.rodata等段的地址和大小。
总结:链接器的价值

链接器是程序从“源代码”到“可执行文件”的最后一步,它将分散的目标文件和库文件整合成一个能运行的程序,解决了“符号引用”和“地址调整”的问题。没有链接器,程序只能是一堆分散的机器码,无法被操作系统加载和执行。

链接器的工作可概括为 “三步骤拼图”:

  1. 找碎片(符号解析):为每个未定义符号找到唯一的定义;
  2. 拼碎片(段合并):将同类段合并为连续的内存块;
  3. 标位置(重定位):修正所有指令的地址,确保能找到正确的 “碎片位置”。

静态链接是 “把所有碎片一次性粘成完整拼图”,动态链接是 “拼图核心部分自己保留,边缘部分临时向别人借”。这样的流程让分散的代码最终成为能被操作系统加载执行的 “完整程序”。简单来说,链接器的工作就是:把“碎片”拼成“完整的程序”

http://www.xdnf.cn/news/17567.html

相关文章:

  • Docker部署到实战
  • K8s四层负载均衡-service
  • Python爬虫实战:研究BlackWidow,构建最新科技资讯采集系统
  • 【话题讨论】GPT-5 发布全解读:参数升级、长上下文与多领域能力提升
  • log4cpp、log4cplus 与 log4cxx 三大 C++ 日志框架
  • MPLS对LSP连通性的检测
  • 力扣559:N叉树的最大深度
  • 【力扣198】打家劫舍
  • Ubuntu 24.04 适配联发科 mt7902 pcie wifi 网卡驱动实践
  • 联邦学习之------VT合谋
  • 计算机网络:路由聚合的注意事项有哪些?
  • 【嵌入式】Linux的常用操作命令(2)
  • 米哈游笔试——求强势顶点的个数
  • [概率 DP]808. 分汤
  • 第4章 程序段的反复执行2 while语句P128练习题(题及答案)
  • pytorch llm 计算flops和参数量
  • Gltf 模型 加载到 Cesium 的坐标轴映射浅谈
  • 深入理解C++构造函数与初始化列表
  • Python训练营打卡Day27-类的定义和方法
  • AudioLLM
  • 专题二_滑动窗口_找到字符串中所有字母异位词
  • 第二十天:数论度量
  • 前端Web在Vue中的知识详解
  • 数据溢出ERROR L107:ADDRESS SPACE OVERFLOW
  • 11. 为什么要用static关键字
  • 【C++】string 的特性和使用
  • Python(13) -- 面向对象
  • 【面试场景题】通过LinkedHashMap来实现LRU与LFU
  • Java+Vue打造的采购招投标一体化管理系统,涵盖招标、投标、开标、评标全流程,功能完备,附完整可二次开发的源码
  • 标准IO实现