恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

【C++入门】编译链接模型 - 01 一个 C++ 程序是怎样变成可执行文件的

  • 首页
  • 资讯中心
  • /
  • 【C++入门】编译链接模型 - 01 一个 C++ 程序是怎样变成可执行文件的

相关资讯

深度解析Comet 0.4.0架构:纯Node.js运行时上的确定性Skill引擎是如何构建的 2026/9/30 15:26:36
LLM Agent 长期记忆架构实战:基于 MCP 与 Docker 的分层记忆系统 2026/9/30 15:21:36
逆向拆解实战:从登录框到开源项目的技术还原方法论 2026/9/30 15:21:36

最新资讯

基于LCL滤波器的三相并网逆变器电流环解耦控制仿真
Arch Linux微信安装:AUR沙箱与Distrobox容器路线
浏览器资源加载与缓存机制全解析:原理、策略与工程实践
豆包绘图“骂着用”的三条铁律:细节清单、迭代修正与风格锚定
图片压缩到200KB:上传总提示太大,怎样少走弯路
图像滤波从原理到实践:高斯、中值与双边滤波的选型指南

今日推荐

模型优化器实战:从FP32到INT8的推理加速与精度平衡
LangGraph+FastAPI构建可审计AI编码助手
基于图像预处理与几何特征的人脸脸型发型搭配系统实现

本周热门

从像素到笔画:srt-whiteboard-animation骨架笔迹追踪实现(Zhang-Suen细化+8邻接追踪)
网站建设的英语怎么说?别只背单词,看完这套安全完整流程才敢上线
新手入门看这篇:建设网站加盟避坑指南与SEO实操

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

【C++入门】编译链接模型 - 01 一个 C++ 程序是怎样变成可执行文件的

发布时间:2026/9/30 15:26:36
【C++入门】编译链接模型 - 01 一个 C++ 程序是怎样变成可执行文件的 博主介绍程序喵大人35 - 资深C/C/Rust/Android/iOS客户端开发10年大厂工作经验嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手《C20高级编程》《C23高级编程》等多本书籍著译者更多原创精品文章首发gzh见文末记得订阅专栏以防走丢C基础系列专栏C语言基础系列专栏C大佬养成攻略专栏C训练营个人网站好文推荐【AIAgent项目】从零构建一个代码PRAgent创建一个main.cc写入几行 C 代码然后在终端里敲一行clang命令一个可执行文件就出现了。这件事大多数 C 程序员每天都在做但这一条命令里面到底发生了什么很多人并没有仔细拆过。#includeiostreamintmain(){std::couthello from the build pipeline\n;return0;}clang-stdc20-Wallmain.cc-ohello ./hello输出hello from the build pipeline程序的源码不到十行命令也只有一行从敲回车到看到输出不到一秒。这里面藏着 C 工具链最核心的一条流水线源码文本先经过预处理展开成完整的翻译单元再由编译器转成汇编代码汇编器把汇编转成目标文件的机器码链接器再把目标文件和库拼成操作系统能直接加载的可执行文件。一条clang命令之所以能完成所有这些事是因为它的角色是一个总控程序compiler driver按顺序调度预处理器、编译器、汇编器和链接器把前一阶段的输出转交给下一阶段。本章把这条命令拆开用clang自带的控制参数让它在每个中间阶段停下来把中间产物留在磁盘上。读者看到的行数、文件大小和文件类型都是在一台 Apple Silicon Mac 上用 Clang 实际跑出来的数字不同平台和不同工具链的具体数值会有差异但流水线的逻辑结构是共通的。clang 是总控程序不是单一的编译器很多教程把clang以及g称为“C 编译器”这个说法在日常交流中没问题但从工具链架构的角度看clang本身并不直接做语法分析、代码生成或机器码输出。它是一个 driver负责解析命令行参数、决定调用哪些工具、按什么顺序调用、传递哪些选项。真正干活的是它调度的一组独立程序预处理器preprocessor、编译器前端compiler frontend、汇编器assembler和链接器linker。用一个类比来建立直觉clang像一个项目经理接到“把这份源码变成可执行文件”的需求后它把任务分解成几个阶段分别交给团队里不同的人最后把各阶段的产出串成最终交付物。你可以在任何一个阶段喊停项目经理会把到目前为止的中间产物交给你。从源码到可执行文件的标准流水线是四段。第一段是预处理输入.cc源文件和它通过#include引入的所有头文件输出一个展开后的纯文本文件习惯上叫.i文件。第二段是编译输入预处理后的文本输出汇编代码文本.s文件。第三段是汇编输入汇编文本输出二进制目标文件.o文件。第四段是链接输入一个或多个目标文件以及程序依赖的库输出可执行文件。在 Clang/LLVM 工具链内部预处理通常由 clang 的预处理模式完成编译由clang -cc1这个前端进程完成负责词法分析、语法分析、语义分析、中间代码生成和汇编代码输出汇编由系统汇编器通常是as或 LLVM 内置的集成汇编器完成链接由系统链接器macOS 上是ldLinux 上通常是 GNUld或lld完成。GCC 工具链的调度方式类似只是内部工具的名称和分工略有不同预处理由cppC preprocessor完成编译由cc1或cc1plus完成汇编由as完成链接由collect2调度ld完成。日常开发中不需要关心这些内部工具的具体名称但知道它们存在对于理解后面要讲的编译错误和链接错误分别由哪个环节产生有直接帮助。clang main.cc-ohello这条命令会让 driver 一口气跑完四段中间文件不落盘直接通过管道或临时文件传递。这种全自动模式在日常开发中很高效但学习编译链接模型时需要主动让它停下来把每一段的产物拿出来看。Clang以及 GCC提供了一组控制参数来做这件事-E停在预处理后-S停在编译生成汇编后-c停在汇编生成目标文件后。不传任何停步参数时driver 默认走完全程直到链接出可执行文件。这四个参数就是本章用来观察流水线内部结构的窗口。预处理文本展开先用-E让 driver 在预处理阶段结束后停下来clang-stdc20-Emain.cc-omain.imain.i是预处理后的文本文件。用wc -l看一下行数在本机验证中得到约 93446 行。一个不到十行的源文件经过预处理后膨胀到了近十万行。原因在于#include iostream。#include做的事情非常朴素它把指定文件的内容完整复制到当前文件的#include指令所在位置。iostream本身包含了ios、streambuf、istream、ostream等一系列标准库头文件这些头文件又各自包含更多头文件最终形成一个很大的头文件树。预处理器的输出就是这个树的完整展开结果除了main.cc原来的几行代码前面还塞进了所有被包含头文件的内容。打开main.i翻到最后你会看到自己写的main函数安安静静地待在近十万行展开文本的最末尾。在它前面的是标准库的完整声明类定义、函数声明、模板、inline 函数、类型别名、各种extern声明。这些内容原本分散在几十个头文件里预处理阶段把它们全部拉进同一个文本流形成编译器接下来要处理的“翻译单元”translation unit的完整面貌。预处理阶段不只做#include展开它还处理宏替换#define、条件编译#ifdef/#ifndef/#endif、行号标记#line等指令最终产出一个没有任何预处理指令残留的纯 C 源码文本。不过对于理解编译链接流水线来说现阶段只需要抓住一个关键点预处理器的输出是一个完整的、自包含的文本文件里面不再有任何#include或#define只有展开后的 C 代码编译器拿到它之后就可以开始做真正的语法和语义分析。头文件的组织方式、include guard 的机制、宏对后续编译的影响这些放在下一章展开。编译从 C 到汇编拿到了预处理后的完整文本编译器就可以真正开始工作了。用-S让 driver 在编译阶段结束后停下clang-stdc20-Smain.cc-omain.s传给-S的可以是源文件main.ccdriver 会先自动做预处理再编译也可以是已经预处理好的main.i。对于日常实验来说直接传源文件就行。编译阶段的核心任务是把 C 源码翻译成汇编代码。汇编代码是人类可读的机器指令文本每一行基本上对应一条 CPU 指令。本机验证中main.s大约有 1380 行。相比预处理后的近十万行这个数字缩小了近两个数量级。缩小的原因很直接预处理阶段把大量标准库头文件内容拉了进来但编译器只生成那些真正被用到的代码。std::cout ...虽然背后涉及很多模板实例化和运算符重载但它最终对应的机器指令数量仍然远小于头文件中各种声明、注释、未用到的模板和条件编译分支的文本体量。编译器不会把每个看见的声明都翻译成指令只有实际被引用的实体才会进入代码生成。从编译器的视角看预处理后的近十万行文本里绝大多数是声明告诉编译器某样东西存在、长什么样声明的信息被编译器吸收进内部的符号表和类型系统后就完成了使命真正需要生成机器码的只有源文件里那些实际被调用的函数和实际被操作的对象。编译阶段内部本身也是一个多步骤的过程。编译器先做词法分析把字符流切成一个个 token关键字、标识符、字面量、运算符再做语法分析按 C 文法把 token 序列组织成抽象语法树AST接着做语义分析检查类型是否匹配、函数调用是否合法、访问权限是否正确然后生成中间表示intermediate representationIR在 LLVM 中就是 LLVM IR最后把 IR 翻译成目标平台的汇编代码。这一整套流程全部在编译阶段内部完成从外面看就是“进去一段 C 文本出来一段汇编文本”。内部各步骤的细节会在编译器原理相关的课程中展开本系列关注的是编译阶段在整个构建流水线中的位置和输入输出关系。打开main.s可以看到汇编代码的典型结构文件开头是一些汇编指示字directives比如.section指定代码放在哪个段、.globl声明对外可见的符号。随后是函数体的指令序列比如_main标签下面按顺序排列的stp、sub、adrp、ldr、bl等 ARM64 指令。即使不熟悉 ARM64 汇编也能从结构上看出这些指令在做几件事设置栈帧、准备参数、调用函数、清理返回值。其中blbranch with link指令就是函数调用你能看到对std::cout相关符号和operator的调用。汇编代码已经非常接近机器能执行的形式但它仍然是文本。CPU 不认识文本形式的bl _ZNSt3__14coutECPU 只认识编码后的二进制指令。汇编阶段负责这一步转换。汇编从文本到目标文件用-c让 driver 在汇编阶段结束后停下clang-stdc20-cmain.cc-omain.o-c的意思是“compile only”这个命名在历史上有点歧义它实际上包含了预处理、编译和汇编三个阶段只是不执行最后的链接。产出的main.o是一个二进制目标文件object file。用file命令查看main.o的属性在本机验证中得到Mach-O 64-bit object arm64。这说明它是 Mach-O 格式的 64 位目标文件包含 ARM64 指令。用ls -l查看大小约 9816 字节也就是不到 10KB。从近十万行预处理文本到一千多行汇编文本再到不到 10KB 的二进制目标文件每一步都在从“给人看的文本”向“给机器执行的二进制”逼近。目标文件里有什么它已经不是纯文本了不能直接用编辑器阅读。它里面包含几个关键部分编译生成的机器码放在.text段或类似段里、程序中用到的常量数据放在.rodata之类的只读数据段里、符号表symbol table记录了这个目标文件提供了哪些函数和变量、需要从外部获取哪些函数和变量以及重定位信息relocation告诉链接器哪些地址引用需要在最终链接时修正。为什么要把机器码和数据分开放在不同的段里因为不同段有不同的访问属性和生命周期.text里的机器码在运行时是只读且可执行的.rodata里的常量是只读但不可执行的后续章节讲到的全局变量所在的.data和.bss则是可读可写但不可执行的。段的划分让操作系统和链接器能够对不同类型的代码和数据施加不同的权限和优化策略。每一段机器码和每一个符号表条目在后续章节都会展开讲现阶段先建立总体直觉目标文件是一块已经翻译好的机器码但它还不能直接运行。原因有两个。第一个是地址问题代码里对std::cout等外部符号的引用还是占位状态这些符号的最终地址要等链接器把多个目标文件和库拼在一起之后才能确定。这就像你写了一个函数调用printf(...)汇编器能把调用指令本身编码出来但被调用方的地址还不知道只能先留一个空位等链接时再填。第二个是依赖问题程序要跑起来除了main函数本身的代码还需要启动例程startup routine来初始化标准库、设置全局对象、调用main、处理返回值。这些启动代码不在main.o里它们存在于 C 运行时支持库中只有链接阶段才会被拉进来。用nm工具看一眼main.o的符号表能清楚地看到已定义符号和未定义符号的区分main函数是这个目标文件提供的已定义符号而std::cout、operator、std::ios_base::Init等是未定义符号在nm输出中通常标记为U链接器需要在后续阶段为它们找到定义。链接拼出可执行文件最后一步是把目标文件交给链接器clang main.o-ohello这里没有用-stdc20或-Wall因为这些是编译器参数对于只做链接的步骤来说不需要。clang识别到输入是.o文件后直接把它转交给链接器处理。链接器读入main.o检查符号表里有哪些未定义的符号然后去标准库和其他默认库中寻找这些符号的定义。找到了std::cout、operator、启动例程等符号的定义后链接器把所有需要的代码和数据从各个目标文件和库中提取出来合并到一个文件里修正所有地址引用最终生成操作系统能加载的可执行文件。用file命令查看hello的属性本机验证得到Mach-O 64-bit executable arm64。注意它从 object 变成了 executable这是链接器的核心贡献它把多个不完整的目标文件合成了一个完整的、操作系统能直接装载运行的程序。这里有一个容易被忽视的事实即使是只有一个源文件的程序也必须经过链接阶段。很多初学者会困惑“我就一个main.cc又没有引用其他.cc里的函数为什么还要链接”因为main.cc引用了标准库std::cout的定义在标准库的实现文件里不在main.cc的编译产物里。编译阶段看到#include iostream只是让编译器知道了std::cout的声明它长什么样、怎么用编译器检查完类型和语法就继续往下走了并没有把std::cout的实际实现代码放进main.o。链接器的任务就是把main.o中对std::cout的引用和标准库中对std::cout的定义对接上。这个“声明在头文件里定义在库的实现文件里”的模式是 C 工程组织的基础后续章节讲声明与定义分离以及符号解析时还会反复回到这个主题。除了标准库符号链接器还会自动链接 C 运行时支持C runtime support和 C 运行时启动代码C runtime startup常被称为crt0。启动代码负责在main函数被调用之前完成运行环境初始化包括设置栈、初始化全局对象、配置标准输入输出流等。程序运行时CPU 最先执行的是启动代码的入口点启动代码做完准备工作后才调用mainmain返回后启动代码再负责清理和退出。这一切对写单文件程序的开发者来说是透明的但理解它的存在对后面理解动态库加载顺序、全局对象初始化时机、以及某些诡异的启动崩溃有直接帮助。错误出现在不同阶段理解了四个阶段的输入输出之后一个直接的工程收益是看到编译错误信息时你能判断它卡在哪一关。不同阶段的错误排查方向完全不同。预处理和编译阶段的错误通常表现为语法错误、类型不匹配、找不到声明、头文件路径错误。这类错误的共同特征是链接器还没开始工作就报错了命令行输出里不会出现ld:或linker字样。举个例子写错了std::cout的名字变成std::cot编译器会在语义分析阶段发现cot不是std命名空间里的已知名字报出no member named cot in namespace std漏了语句末尾的分号语法分析阶段就会报expected ; after expression#include的头文件路径不对预处理阶段直接报fatal error: xxx.h file not found。排查这类错误时关注的是代码写法、类型系统和头文件搜索路径-I参数。链接阶段的错误表现为undefined reference未定义引用和multiple definition重复定义。链接器已经拿到了所有目标文件但在符号表里找不到某个被引用的符号或者发现了多个同名的强定义。这类错误信息里通常有ld:前缀在 Clang/GCC 工具链上说明编译和汇编都已经通过了问题出在符号解析。举个例子你在一个.cc文件里调用了add(1, 2)但忘记把定义add函数的那个.cc文件编译出的.o文件传给链接器链接器就会报undefined reference to add(int, int)其中的add(int, int)是经过名字修饰name mangling以后的 C 符号名。排查时关注的是函数和变量是否真的被定义了、库是否被正确链接-l和-L参数是否正确、目标文件是否被遗漏、是否存在违反单一定义规则ODROne Definition Rule的写法。运行时的加载错误在编译和链接都成功之后才可能出现通常涉及动态库。程序启动时操作系统的动态装载器dynamic loader会根据可执行文件中记录的依赖信息去搜索所需的动态库文件。如果动态库不在系统搜索路径里程序会直接启动失败报出的错误类似dyld: Library not loadedmacOS或error while loading shared librariesLinux。这类错误和源码本身无关排查时关注的是动态库的安装位置、运行时搜索路径配置如LD_LIBRARY_PATH、DYLD_LIBRARY_PATH、RPATH。把这三种错误的位置放进同一个心智模型源码的语法和类型问题卡在编译阶段之前或编译阶段之内符号引用和定义匹配问题卡在链接阶段动态库路径和环境问题卡在程序启动时。看到一个错误先分辨它出现在哪个阶段再去对应阶段找原因比直接在几千行错误输出里乱翻高效得多。从流水线视角理解 C 构建回到开头那条命令clang-stdc20-Wallmain.cc-ohello经过本章的拆解这条命令不再是“编译器把源码变成程序”这样一个模糊的黑盒而是一条四段流水线的快捷入口。预处理把分散的头文件合并成一个完整的翻译单元编译把翻译单元转成汇编代码汇编把汇编转成目标文件的机器码链接把目标文件和库拼成操作系统能加载的可执行文件。单文件程序和多文件程序在这条流水线上的差别只在于链接阶段的输入数量。单文件程序只有一个目标文件但它引用了标准库和运行时库链接器仍然需要把这些外部符号的定义拉进来。多文件程序每个.cc文件各自生成一个目标文件链接阶段把它们合并到一起同时接上库。从流水线的角度看并没有“单文件程序不需要链接”这回事只是单文件程序让链接的复杂度被工具链自动处理了开发者感受不到。接下来的章节会沿着这条流水线逐段深入下一章讲预处理和头文件把#include展开、include guard 和宏替换的机制讲透再往后讲翻译单元的本质、声明与定义的分离、单一定义规则、目标文件的内部结构以及链接器如何做符号解析。理解了每一段反过来再看clang这条命令你会清楚地知道每一步在做什么以及出了问题该往哪个方向排查。码字不易欢迎大家点赞关注评论谢谢

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号