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

C++编译流程

编译器其实就是一个翻译器,把我们的文件内容翻译成机器能够看懂的指令,但如何合理翻译是核心。

C语言编译

需要经过以下几步:

  1. 词法分析:扫描代码,确定单词类型,比如是变量还是函数,是标识符还是运算符等等,这样操作后每个token都有自己的性质
  2. 语法分析:根据语法规则构建分析树
  3. 语义分析:主要是检查代码里是否有语义错误,比如函数返回值是否一致/是否访问超出作用域的变量/变量函数是否已经声明等
  4. 生成中间代码
  5. 汇编器生成目标机器语言

在这里插入图片描述

C++编译流程

在这里插入图片描述

预处理

将一些预处理指令的东西进行处理,比如#include、#pragma #define
对于include而言,预处理器会直接打开这个文件,然后将其copy进我们的cpp文件里。 (宏是直接进行字符串替换的)
此时生成的文件是.i文件

编译

通过编译器和汇编器将.cpp文件编译成.o文件,这里的.o其实就是一些01码,为了方便理解,可以先让编译器输出汇编指令(其实本质都是差不多的,做过CPU的应该知道汇编转01码怎么做)
换句话说,如果代码没有语法错误,编译是不应该报错的,比如缺少main函数入口等

// MyClass.cpp

class MyClass {
public:
    int value;

    void setValue(int v) {
        value = v;
        helper();  // 调用自己的成员函数
    }

    void helper() {
        // 空函数,只为了演示调用过程
    }
};

实际编译后.o:

[ MyClass.o ] ←←←←← 编译器输出
┌──────────────────────────────┐
│          .text 段            │←─ 代码段(真正的机器码存放区)
│                              │
│ 0x0000: MyClass::helper()    │←─ helper 的汇编已生成在此位置
│        push rbp              │
│        mov rsp, rbp          │
│        ...                   │
│        ret                   │
│                              │
│ 0x0040: MyClass::setValue()  │←─ setValue 的汇编在这里
│        mov [this], v         │
│        call helper()         │←─ 这条 call 指令地址还没填
└──────────────────────────────┘

┌──────────────────────────────┐
│         符号表(.symtab)     │←─ 存储函数名、地址、段类型等元信息
│                              │
│ _ZN7MyClass6helperEv  → 0x0000helper() 的地址
│ _ZN7MyClass8setValueEi → 0x0040setValue() 的地址
└──────────────────────────────┘

附上如何只编译文件+看汇编结果

 g++ -c -O0 -fno-inline -o main.o main.cpp
 objdump -d main.o  

需要注意的是,每个.o文件独立,函数留的地址也是相对地址,链接器还需要解决相对地址转绝对地址的问题。

链接

那么问题来了,如果我只有一个cpp文件,那么到编译这一步就可以了。但是在大型工程文件里,我们往往是有多个模块的,多个模块之间彼此还有调用关系,如何能生成正确的指令呢?其实就是依赖链接器,将多个cpp的.o文件给链接在一起。
多个模块之间的调用关系有:

  1. 你去访问别人的变量
  2. 调用别人的成员函数:在汇编指令中这一句话是call xxx,这个xxx就是函数签名
    如果你在一个文件里声明并且定义了函数func,在另一个文件里也定义了func,对于include了这两个文件的文件来说,链接器不知道要链接具体哪一个,因此会报错称重复定义xxx。
    举个具体的例子
    MyClass.h代码:
class MyClass {
public:
    int value;
    void setValue(int v);
    void helper();
};


MyClass.cpp代码:

#include "MyClass.h"
void MyClass::setValue(int v) {
    value = v;
    helper();
}
void MyClass::helper() { }

main的cpp代码:

#include "MyClass.h"
int main() {
    MyClass obj;
    obj.setValue(42);
    return 0;
}

main的汇编:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 30          	sub    $0x30,%rsp
   8:	e8 00 00 00 00       	callq  d <main+0xd>
   d:	48 8d 45 fc          	lea    -0x4(%rbp),%rax
  11:	ba 2a 00 00 00       	mov    $0x2a,%edx
  16:	48 89 c1             	mov    %rax,%rcx
  19:	e8 00 00 00 00       	callq  1e <main+0x1e>
  1e:	b8 00 00 00 00       	mov    $0x0,%eax
  23:	48 83 c4 30          	add    $0x30,%rsp
  27:	5d                   	pop    %rbp
  28:	c3                   	retq   
0x08构造初始化
0x16设置this指针
0x19调用setValue

编译器在生成这段机器码的时候,先写了一条机器指令,但是不知道setValue的地址,因此在e8后面跟的都是00000000。同时在.o的重定位表里添加记录:

偏移地址:0x19
类型:CALL
目标符号:_ZN7MyClass8setValueEi

myclass.o的汇编

0000000000000000 <_ZN7MyClass8setValueEi>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 20          	sub    $0x20,%rsp
   8:	48 89 4d 10          	mov    %rcx,0x10(%rbp)
   c:	89 55 18             	mov    %edx,0x18(%rbp)
   f:	48 8b 45 10          	mov    0x10(%rbp),%rax
  13:	8b 55 18             	mov    0x18(%rbp),%edx
  16:	89 10                	mov    %edx,(%rax)
  18:	48 8b 4d 10          	mov    0x10(%rbp),%rcx
  1c:	e8 07 00 00 00       	callq  28 <_ZN7MyClass6helperEv>
  21:	90                   	nop
  22:	48 83 c4 20          	add    $0x20,%rsp
  26:	5d                   	pop    %rbp
  27:	c3                   	retq   

0000000000000028 <_ZN7MyClass6helperEv>:
  28:	55                   	push   %rbp
  29:	48 89 e5             	mov    %rsp,%rbp
  2c:	48 89 4d 10          	mov    %rcx,0x10(%rbp)
  30:	90                   	nop
  31:	5d                   	pop    %rbp
  32:	c3                   	retq   
  33:	90                   	nop
  34:	90                   	nop
  35:	90                   	nop
  36:	90                   	nop
  37:	90                   	nop
  38:	90                   	nop
  39:	90                   	nop
  3a:	90                   	nop
  3b:	90                   	nop
  3c:	90                   	nop
  3d:	90                   	nop
  3e:	90                   	nop
  3f:	90                   	nop

因此,在链接器处理.o时会读取每个.o文件的重定位表,当看到

main.o:
  offset 0x19 → CALL → _ZN7MyClass8setValueEi

记录时,就会查找所有.o文件中是否有这个定义的,然后获取它的地址,并且计算当前call指令的位置到这个地址的相对偏移。每个.o文件会有符号表,用来存储每个函数的入口地址,方便补地址

动态链接和静态链接

此前提到的都是静态链接,直接打包进可执行文件中。那什么是动态链接呢?动态链接就是在运行的时候将程序用到的函数、库从外部共享库中加载进来。
优势就是节省空间,只需要加载一份,可以共享。支持更新和修复,但是稳定性和独立性差,性能也不好

生成exe文件

链接需要为exe指明程序的入口位置,这也就是main的作用


http://www.kler.cn/a/595267.html

相关文章:

  • UNIX网络编程笔记:一些网络协议的相关知识
  • 【Android】基础架构(详细介绍)
  • WordPress 性能优化技术指南:打造快速加载的网站
  • 【python】OpenCV—Hand Landmarks Detection
  • 能源监控软件UI界面设计:平衡功能性与审美性的艺术
  • 针对耳鸣患者推荐的一些菜谱和食材
  • 透析Vue的nextTick原理
  • uniapp小程序,输入框限制输入(正整数、小数后几位)
  • Umi-OCR 实践教程:离线、免费、高效的图像文字识别工具
  • 家庭网络安全:智能设备与IoT防护——当“智能家居”变成“僵尸网络”
  • Java 记忆链表,LinkedList 的升级版
  • PostgreSQL_数据表结构设计并创建
  • 使用 Ansys Fluent 评估金属管道腐蚀
  • 1204. 【高精度练习】密码
  • 《Python实战进阶》No42: 多线程与多进程编程详解(上)
  • 【漫话机器学习系列】153.残差平方和(Residual Sum of Squares, RSS)
  • LeetCode 2680.最大或值:位运算
  • 如何在IPhone 16Pro上运行python文件?
  • 【UI设计】一些好用的免费图标素材网站
  • el-select下拉框,搜索时,若是匹配后的数据有且只有一条,则当失去焦点时,默认选中该条数据