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

【程序构建流程】以具体函数为例

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、核心概念铺垫
  • 二、完整流程拆解(以 Linux + g++ 为例)
    • 阶段 1:预处理(Preprocessing)—— 处理“编译前的文本替换”
      • 工具:预处理程序(cpp,g++ 内置)
      • 作用:
      • 命令:
      • 对函数的影响:
      • 输出:
    • 阶段 2:编译(Compilation)—— 把 C++ 代码转换成汇编语言
      • 工具:编译器(g++ 的编译模块)
      • 作用:
      • 命令:
      • 对函数的影响:
      • 输出:
    • 阶段 3:汇编(Assembly)—— 把汇编代码转换成目标文件(机器码)
      • 工具:汇编器(as,g++ 内置)
      • 作用:
      • 命令:
      • 对函数的影响:
      • 输出:
    • 阶段 4:链接(Linking)—— 合并目标文件,生成可执行文件
      • 工具:链接器(ld,g++ 内置,默认会调用 ld 并链接标准库)
      • 核心问题:
      • 作用:
      • 命令:
      • 对函数的影响:
      • 输出:
  • 三、关键补充:链接的两种方式
    • 1. 静态链接(Static Linking)
    • 2. 动态链接(Dynamic Linking,默认方式)
  • 四、常见问题与链接错误
    • 1. 未定义引用(Undefined reference to `_Z3addii`)
    • 2. 多重定义(Multiple definition of `_Z3addii`)
  • 五、总结:函数的“旅程”
  • 总结


前言

结合之前 add.cppmain.cpp例子记录下程序构建经典的四个步骤


要理解 C++ 中函数从源代码到可执行文件的完整流程,核心是四个关键阶段:预处理 → 编译 → 汇编 → 链接。其中前三个阶段聚焦“单个源文件→目标文件”的转换(重点处理函数的代码转换和符号记录),最后一个阶段聚焦“多个目标文件+库文件→可执行文件”的合并(重点处理函数的地址解析和重定位)。

下面结合具体例子,分阶段拆解每个过程的细节、工具、输出结果,以及函数在各阶段的形态变化。

一、核心概念铺垫

在开始前,先明确几个关键术语,避免后续混淆:

  • 目标文件(.o/.obj):单个源文件经过预处理、编译、汇编后生成的二进制文件,包含函数的机器码符号表(记录函数/变量名与地址的映射)、重定位表(记录需要链接时修正的地址)。
  • 符号(Symbol):函数名、全局变量名的“编译后标识”(C++ 会进行名字修饰,比如 add(int, int) 会变成 _Z3addii,用于区分重载函数)。
  • 重定位(Relocation):目标文件中调用其他文件的函数时,地址是“占位符”,链接时需要替换为函数的真实地址,这个过程叫重定位。
  • 可执行文件(.exe/ELF):链接后生成的文件,包含所有函数的机器码、完整的地址映射,可直接被操作系统加载执行。

二、完整流程拆解(以 Linux + g++ 为例)

我们用两个简单的源文件演示:

// add.cpp(定义一个加法函数)
int add(int a, int b) {return a + b; // 核心逻辑:两数相加
}// main.cpp(调用 add 函数)
#include <iostream>
int add(int a, int b); // 函数声明(告诉编译器存在这个函数)int main() {int x = 3, y = 5;std::cout << add(x, y) << std::endl; // 调用 add 函数return 0;
}

阶段 1:预处理(Preprocessing)—— 处理“编译前的文本替换”

工具:预处理程序(cpp,g++ 内置)

作用:

  • 展开 #include 头文件(比如 #include <iostream> 会替换为标准输入输出流的全部声明代码);
  • 替换 #define 宏定义(如果有 #define MAX 10,会把代码中所有 MAX 替换为 10);
  • 删除注释(///* */ 中的内容);
  • 处理条件编译(#if/#else/#endif 等)。

命令:

g++ -E add.cpp -o add.i   # 生成预处理后的文件 add.i
g++ -E main.cpp -o main.i # 生成预处理后的文件 main.i

对函数的影响:

  • 函数的定义和声明结构不变(比如 add(int a, int b) 的原型、main 中的调用逻辑都保留);
  • 仅做“文本层面的替换”,不涉及语法分析或代码转换。

输出:

  • 生成 .i 后缀的文本文件(本质还是 C++ 代码,但体积会变大,因为展开了头文件)。

阶段 2:编译(Compilation)—— 把 C++ 代码转换成汇编语言

工具:编译器(g++ 的编译模块)

作用:

  • .i 文件进行语法分析、语义分析、优化(比如删除无效代码、简化运算);
  • 将合法的 C++ 代码转换为汇编语言指令(汇编代码是机器码的“人类可读形式”);
  • 关键操作:C++ 特有的名字修饰(Name Mangling) —— 因为 C++ 支持函数重载(比如 add(int,int)add(double,double)),编译器会把函数名+参数类型编码成唯一的符号(避免链接时冲突)。

命令:

g++ -S add.i -o add.s   # 生成汇编文件 add.s
g++ -S main.i -o main.s # 生成汇编文件 main.s

对函数的影响:

  • 函数被转换为汇编指令序列。以 add(int a, int b) 为例,add.s 中的汇编代码(x86-64 架构)大概是这样:
    .file   "add.cpp"
    .text
    .globl  _Z3addii        # 名字修饰后的符号:_Z3addii(3表示函数名长度,add是函数名,ii是int+int)
    .type   _Z3addii, @function
    _Z3addii:
    .LFB0:.cfi_startprocpushq   %rbp       # 栈帧初始化.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp.cfi_def_cfa_register 6movl    %edi, -4(%rbp)  # 把第一个参数 a 存入栈帧movl    %esi, -8(%rbp)  # 把第二个参数 b 存入栈帧movl    -4(%rbp), %eax  # 把 a 加载到 eax 寄存器addl    -8(%rbp), %eax  # eax = a + b(核心运算)popq    %rbp            # 栈帧恢复.cfi_def_cfa 7, 8ret                     # 返回结果(eax 寄存器存返回值).cfi_endproc
    .LFE0:.size   _Z3addii, .-_Z3addii
    .ident  "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
    .section        .note.GNU-stack,"",@progbits
    
  • main.s 中会包含调用 add 的汇编指令,但此时 add 的真实地址未知,编译器会用“占位符”标记(比如 callq _Z3addii),并记录该符号为“未定义符号”。

输出:

  • 生成 .s 后缀的汇编文件(文本文件,可直接用文本编辑器打开查看)。

阶段 3:汇编(Assembly)—— 把汇编代码转换成目标文件(机器码)

工具:汇编器(as,g++ 内置)

作用:

  • 读取 .s 文件中的汇编指令,将其翻译成机器码(二进制指令,CPU 可直接执行);
  • 将机器码存入目标文件的 .text 段(代码段,专门存放函数的执行指令);
  • 生成 符号表(Symbol Table)重定位表(Relocation Table)
    • 符号表:记录目标文件中所有函数/全局变量的符号(比如 add.o 的符号表中,_Z3addii 是“已定义符号”,地址是相对于 .text 段的偏移量,比如 0x0);
    • 重定位表:记录目标文件中“地址未确定”的位置(比如 main.o 中调用 _Z3addii 的指令地址),告诉链接器“这里需要后续修正”。

命令:

g++ -c add.s -o add.o   # 生成目标文件 add.o(或用 as add.s -o add.o)
g++ -c main.s -o main.o # 生成目标文件 main.o

对函数的影响:

  • 函数的汇编指令被转换成二进制机器码,存入 .text 段(比如 add 的机器码是 55 48 89 e5 89 7d fc 89 75 f8 8b 45 fc 03 45 f8 5d c3,对应上面的汇编指令);
  • 目标文件是二进制文件(无法直接用文本编辑器查看,需用 objdump 工具分析,比如 objdump -d add.o 可查看 .text 段的机器码)。

输出:

  • 生成 .o 后缀的目标文件(ELF 格式,Linux 下的标准目标文件格式;Windows 下是 .obj,PE 格式)。

阶段 4:链接(Linking)—— 合并目标文件,生成可执行文件

工具:链接器(ld,g++ 内置,默认会调用 ld 并链接标准库)

核心问题:

  • 单个目标文件无法独立运行(比如 main.o 依赖 add.o 中的 add 函数,但 main.o 中没有 add 的机器码,且 add 的地址是占位符);
  • 还需要依赖标准库(比如 std::cout 来自 C++ 标准库 libstdc++)。

作用:

  1. 合并段(Section Merging):将所有目标文件的 .text 段(代码)、.data 段(已初始化全局变量)、.bss 段(未初始化全局变量)等分别合并成一个大的段(比如所有 .text 合并为可执行文件的 .text 段);
  2. 解析符号(Symbol Resolution):查找所有未定义符号的定义(比如 main.o 中的 _Z3addii 未定义,链接器在 add.o 的符号表中找到其定义);
  3. 重定位(Relocation):根据合并后的段基地址,修正重定位表中的占位符地址(比如合并后 .text 段的基地址是 0x400520addadd.o 中的偏移是 0x0,则 add 的真实地址是 0x400520,链接器会把 main.o 中调用 add 的指令地址改成 0x400520);
  4. 链接库文件:如果代码依赖标准库(比如 std::cout),链接器会从系统库路径中找到 libstdc++.so(动态库)或 libstdc++.a(静态库),将所需代码合并到可执行文件(静态链接)或记录库依赖(动态链接)。

命令:

g++ main.o add.o -o main  # 链接生成可执行文件 main(Linux 下)
# Windows 下:g++ main.obj add.obj -o main.exe

对函数的影响:

  • 所有函数的机器码被合并到可执行文件的 .text 段,且地址完全确定(CPU 可直接跳转执行);
  • 比如 main 函数调用 add 时,指令中的地址已经是 0x400520(合并后的真实地址),不再是占位符。

输出:

  • 生成可执行文件(Linux 下是 ELF 格式,无后缀;Windows 下是 PE 格式,.exe 后缀)。

三、关键补充:链接的两种方式

链接器链接库文件时,有两种核心方式,影响可执行文件的体积和运行依赖:

1. 静态链接(Static Linking)

  • 原理:将库文件(比如 libstdc++.a)中的所需代码直接复制到可执行文件中;
  • 优点:可执行文件不依赖外部库,单独运行即可;
  • 缺点:可执行文件体积大,多个程序会重复包含相同库代码,浪费磁盘空间。

2. 动态链接(Dynamic Linking,默认方式)

  • 原理:不复制库代码,仅在可执行文件中记录“依赖哪个动态库(.so/.dll)”,程序运行时由操作系统加载动态库并解析地址;
  • 优点:可执行文件体积小,多个程序共享同一个动态库,节省磁盘空间;
  • 缺点:运行时必须依赖对应的动态库,否则会报错“找不到库文件”。

四、常见问题与链接错误

1. 未定义引用(Undefined reference to _Z3addii

  • 原因:main.o 中调用了 add 函数,但链接时没找到 add.o(比如忘记链接 add.o),或 add 的声明与定义不一致(比如 add(int a, int b) 声明,add(double a, double b) 定义,导致名字修饰后符号不同);
  • 解决:确保所有被调用的函数都有对应的目标文件,且声明与定义完全一致。

2. 多重定义(Multiple definition of _Z3addii

  • 原因:同一个函数在多个目标文件中被定义(比如 add 函数在 add1.oadd2.o 中都有定义);
  • 解决:确保函数仅在一个源文件中定义,其他文件用 extern 声明。

五、总结:函数的“旅程”

阶段工具输入文件输出文件函数的形态变化
预处理cpp.cpp.iC++ 源代码(展开头文件、替换宏)
编译g++.i.s汇编语言指令(名字修饰后符号)
汇编as.s.o机器码(存入 .text 段,符号表+重定位表)
链接ld.o + 库可执行文件确定地址的机器码(合并到可执行文件)

整个流程的核心是:从“人类可读的源代码”逐步转换为“CPU 可执行的机器码”,并通过链接器解决跨文件的函数依赖,确定函数的最终地址


总结

程序构建经典四步骤看过很多博客也问过很多次AI,这个以具体例子询问得到的结果是看过文章里比较好的,记录下来!

http://www.dtcms.com/a/615498.html

相关文章:

  • 《Generative Agents: Interactive Simulacra of Human Behavior》论文详解
  • 福州网站微信公众号企网
  • 一个门户网站的建设流程织梦动漫网站模版
  • 高端网站建设公司好不好网站规划对网站建设起到
  • 做网站前端后端ui什么意思外加工活怎么直接找厂家接单
  • 备案通过后怎么做网站石家庄建设局官方网站
  • Sharding-jdbc 有20年数据,每年分一张表,查询指定日期近两年数据,跨年查询如何处理
  • php ajax网站开发典型实例 pdf网络架构方案规划设计和实施
  • 平台网站建设方案模板开通网站需要多少钱
  • 网站制作 太原合肥做网站好的公司哪家好
  • 东莞网站设计与网站制作西安有什么好玩的游乐园
  • 《PyTorch深度学习建模与应用(参考用书)》(零)——深度学习综述
  • 深耕C语言动态内存:realloc实战与数组求和综合练习
  • 网站建设 会议纪要网店运营的基本流程
  • 前端微前端应用加载策略,预加载与懒加载
  • 杭州滨江网站制作东莞网络公司招聘信息
  • 嘉兴网站建设低价推荐网站建设公司效果
  • HTTP , Websocket 和SSE三者的区别
  • 监控做斗鱼直播网站三水网站制作
  • 做百度网站需要多少钱网站维护知识
  • 掌握C语言:全局变量的完整生命周期
  • 网站架构设计师月薪多少缪斯设计
  • 网站建设用什么网站描述更改
  • 个人网站成功案例淘宝客网站备案号
  • 建站排行榜兰州网站seo
  • 怎么建设一个网站并顺利打开浏览信息网站建设
  • 易语言模块反编译工具 | 深入探讨易语言反编译技术及应用
  • 营销型网站制作建设怎样建设一个能上传数据的网站
  • 江门网站建设价格怎么在手机上自己开发软件
  • 宁波微信公众号开发公司seo网站首页优化排名怎么做