编译链接过程讲解

c/c++程序从原代码到二进制的可执行文件,分为预处理--编译--汇编--链接四个阶段。

一、整体流程概览

cpp 复制代码
//一步完成
gcc -o hello hello.c

//分步完成
gcc -E hello.c -o hello.i  //预处理
gcc -s hello.i -o hello.s  //编译
gcc -c hello.s -o hello.o  //汇编
gcc hello.o -o hello       //链接

二、分阶段详解

阶段 1:预处理

  • 输入.c 源代码文件

  • 输出.i 预处理后的 C 文件

  • 核心操作

    1. 展开所有#define宏定义,执行宏替换
    2. 递归展开所有#include头文件(将头文件内容直接插入到当前文件)
    3. 处理所有条件编译指令(#if/#ifdef/#endif等)
    4. 删除所有注释(单行//和多行/* */
    5. 添加行号和文件名标识(用于编译错误和调试信息)
  • 面试高频考点

    • #include <file.h>#include "file.h"的区别:前者搜索系统头文件目录,后者先搜索当前目录,再搜索系统目录
    • 宏定义#definetypedef的本质区别:宏是文本替换,typedef 是类型别名
    • 宏的副作用:比如#define MAX(a,b) ((a)>(b)?(a):(b)),调用MAX(i++,j)会导致 i 被自增两次

阶段 2:编译

  • 输入.i 预处理后的 C 文件

  • 输出.s 汇编语言文件

  • 核心操作:将 C 代码翻译成汇编指令,是整个过程中最复杂的阶段,分为 6 个子步骤:

    1. 词法分析:将源代码拆分成一个个 token(关键字、标识符、常量、运算符等)
    2. 语法分析:根据语法规则生成抽象语法树(AST)
    3. 语义分析:检查语法树的语义正确性(类型检查、类型转换等)
    4. 中间代码生成:将语法树转换成与平台无关的中间代码(如三地址码)
    5. 代码优化:对中间代码进行优化(常量折叠、死代码消除、循环展开、内联等)
    6. 目标代码生成:将优化后的中间代码转换成特定平台的汇编指令
  • 面试高频考点

    • 编译器优化的常见类型及作用
    • 内联函数和宏的区别:内联函数时编译的时候,有类型检查,宏是预处理的时候展开,五类型检查

阶段 3:汇编(Assembly)

  • 输入.s 汇编语言文件
  • 输出.o 可重定位目标文件(ELF 格式,Linux 下)
  • 核心操作:将汇编指令逐条翻译成机器指令(二进制代码),生成目标文件。

阶段 4:链接(Linking)【字节跳动面试核心中的核心】

  • 输入 :多个.o目标文件 + 静态库 / 动态库
  • 输出:可执行文件(ELF 格式)
  • 核心作用
    1. 解决多个目标文件之间的符号引用问题(比如 A 文件调用 B 文件的函数)
    2. 进行重定位,将目标文件中的相对地址转换为最终的虚拟地址
    3. 合并相同类型的段(比如所有目标文件的.text段合并成一个大的.text段)

链接分为静态链接动态链接两种,下面分别详解。


4.1 静态链接(Static Linking)

静态链接在编译链接阶段 完成,将所有需要的目标文件和静态库(.a文件)打包成一个独立的可执行文件。

4.1.1 步骤 1:符号解析(Symbol Resolution)
  • 什么是符号:函数名、全局变量名、静态变量名
  • 符号表:每个目标文件都有一个符号表,记录了该文件定义的符号和引用的外部符号
  • 符号解析的目的:为每个外部符号引用找到对应的定义
强符号与弱符号规则
  • 强符号:已初始化的全局变量、函数定义
  • 弱符号 :未初始化的全局变量、用__attribute__((weak))修饰的函数 / 变量
  • 规则
    1. 不允许有多个同名的强符号(否则报multiple definition错误)
    2. 如果一个强符号和一个弱符号同名,选择强符号
    3. 如果有多个同名的弱符号,选择占用空间最大的那个
4.1.2 步骤 2:重定位(Relocation)
  • 为什么需要重定位:目标文件中的地址都是相对地址(相对于自身段的起始地址),不知道最终会被加载到内存的哪个位置
  • 重定位的过程:
    1. 链接器为每个段分配最终的虚拟地址
    2. 遍历所有重定位条目,根据重定位类型修改指令中的地址
    3. 将修改后的指令写入最终的可执行文件
4.1.3 静态链接的优缺点
  • 优点:
    1. 运行速度快,不需要在运行时进行链接
    2. 可执行文件独立,不依赖系统的库文件
  • 缺点:
    1. 可执行文件体积大,包含了所有用到的库代码
    2. 内存浪费:多个进程运行同一个静态链接的程序,会在内存中加载多份相同的库代码
    3. 更新困难:库更新后,所有使用该库的程序都需要重新编译链接

4.2 动态链接(Dynamic Linking)

动态链接将链接过程推迟到程序运行时 进行,多个进程可以共享同一个动态库(.so文件)的代码段。

4.2.1 基本原理
  • 编译链接时,链接器不将动态库的代码复制到可执行文件中,而是在可执行文件中记录动态库的名称和符号信息
  • 程序运行时,由操作系统的动态链接器(ld-linux.so)加载动态库,并完成符号解析和重定位
4.2.2 :PIC(位置无关代码)
  • 什么是 PIC:编译动态库时必须使用-fPIC选项生成位置无关代码,使得动态库的代码段可以被加载到内存的任意位置,而不需要修改代码本身
  • 为什么需要 PIC:如果动态库不是 PIC,那么每个进程加载动态库时都需要对代码段进行重定位,导致代码段无法共享,失去了动态链接的意义
  • PIC 的实现原理:通过 **GOT 表(全局偏移表)PLT 表(过程链接表)** 实现
4.2.3 :GOT 表与 PLT 表及延迟绑定机制

这是字节跳动面试动态链接部分最高频的考点,必须能清晰描述整个流程。

  • GOT 表(Global Offset Table)

    • 位于数据段,是一个数组,每个条目存储一个外部符号的绝对地址
    • 编译时,所有对外部符号的引用都被转换为对 GOT 表中对应条目的引用
  • PLT 表(Procedure Linkage Table)

    • 位于代码段,是一个数组,每个条目是一段小的汇编代码
    • 用于实现延迟绑定(惰性绑定):只有当函数第一次被调用时,才会解析该函数的地址并写入 GOT 表
  • 延迟绑定的完整流程(以调用printf为例)

    1. 程序第一次调用printf时,跳转到 PLT [1](printf对应的 PLT 条目)
    2. PLT [1] 跳转到 GOT [3](printf对应的 GOT 条目)
    3. 此时 GOT [3] 中存储的是 PLT [1] 的下一条指令的地址,所以又跳回 PLT [1]
    4. PLT [1] 将printf的符号索引压入栈,然后跳转到 PLT [0]
    5. PLT [0] 将 GOT [1](动态链接器的标识)压入栈,然后跳转到 GOT [2](动态链接器的_dl_runtime_resolve函数地址)
    6. _dl_runtime_resolve函数解析printf的绝对地址,将其写入 GOT [3],然后跳转到printf函数执行
    7. 程序第二次及以后调用printf时,直接跳转到 GOT [3],此时 GOT [3] 中已经存储了printf的绝对地址,不需要再进行解析
4.2.4 动态链接的两种方式
  1. 加载时动态链接:程序启动时,由动态链接器自动加载所有需要的动态库并完成绑定
  2. 运行时动态链接 :程序运行过程中,通过dlopen()/dlsym()/dlclose()函数手动加载和卸载动态库,适用于插件系统等场景
4.2.5 动态链接的优缺点
  • 优点:
    1. 可执行文件体积小,只包含必要的代码
    2. 内存共享:多个进程可以共享同一个动态库的代码段,节省内存
    3. 更新方便:动态库更新后,不需要重新编译所有使用该库的程序
  • 缺点:
    1. 运行速度稍慢,第一次调用函数时需要进行符号解析
    2. 可执行文件依赖系统的动态库,移植性较差
相关推荐
哼?~2 小时前
Socket--UDP 构建简单聊天室
linux·网络·udp
JACK的服务器笔记2 小时前
《服务器测试百日学习计划——Day19:PCIe自动检测脚本,用Python把lspci设备清点标准化》
服务器·python·学习
SPC的存折2 小时前
分布式(加一键部署脚本)LNMP-Redis-Discuz5.0部署指南-小白详细版
linux·运维·服务器·数据库·redis·分布式·缓存
Cx330❀2 小时前
线程进阶实战:资源划分与线程控制核心指南
java·大数据·linux·运维·服务器·开发语言·搜索引擎
铅笔小新z2 小时前
【Linux】进程控制(上)
linux·运维·服务器
AI周红伟2 小时前
Hermes Agent 工具-周红伟
linux·网络·人工智能·腾讯云·openclaw
大卡片2 小时前
linux和IO常见面试题
linux·运维·服务器
zzzyyy5382 小时前
Linux程序地址空间
linux·运维·服务器
RisunJan2 小时前
Linux命令-newusers(用于批处理的方式一次创建多个命令)
linux·运维·服务器