欢迎光临
我们一直在努力

编译四大阶段

一.预编译阶段(预处理,gcc -E main.cpp main.i)

输入:.cpp源码;

输出:.i预处理后的文本文件,仍然是C/C++源码文本,不是机器码。

1.#define宏展开,删除#define定义

例子:#define PI 3.14,代码里所有PI全部替换成3.14;宏请按不原地展开,之后删掉#define这一行。

注意:只是纯文本替换,不做语法检查!写错宏,预处理阶段不会报错,等到后面编译阶段才报错。

2.处理条件预编译 #if / #ifdef / #ifndef / #endif

根据条件判断,直接删掉不满足条件的代码行。

例:#ifdef WIN32 如果不是Windows平台,这一段代码直接被删掉,不会参与后续编译。

3.处理#include头文件

把#include "xxx.h"对应的整个头文件内容,原封不动复制粘贴到当前代码这个位置。

头文件会递归处理:头文件里面还包含别的头文件,继续复制。

4.删除所有注释

//单行注释和/*块注释*/全部删掉,替换成空格。注释对程序没有任何作用,预处理直接清理掉。

5.添加行号,文件名标识

就是# 行号 “文件名”标记。后面编译报错,gdb调试的时候,才能告诉你main.cpp第25行出错。如果没有这个标记,编译器分不清代码来自哪个文件哪一行。

6.保留#pragma指令

#pragma是留给特定编译器的指令,不是标准C语法。预处理不会删掉,交给后面编译器处理。

例:#pragma once防止头文件重复包含。

预处理总结:纯文本操作,不做语法检查,只是增删替换。

二.编译阶段(gcc -S main.i -o main.s)

输入:预处理后的.i文件;输出:.s汇编代码文件(文本)

一共5步:词法分析->语法分析->语义分析->代码优化->汇总符号

1.词法分析

把连续代码切成一个个[单词token],把代码字符串切割:关键字if,变量名a,运算符+,数字10。

比如:int a = 1+2;拆成int  a  =  1  +  2  ;

识别非法字符,比如中文标点,这里就报错。

2.语法分析

按照C语法,生成语法树(抽象语法树AST),检查句子语法是否合法。

比如:int = 10;  变量名确实,语法错误,直接报编译错误。

3.语义分析

检查类型是否合法(重点!)

语法没问题,但类型不对在这里报错。

例如:int a = "abc";  把字符串赋值给int,语法没问题,语义错误。

做类型检查,类型转换。

4.代码优化

对AST做优化,简化代码。

比如:int a = 1+2;  直接优化成a=3,不用运行时再计算。

5.汇总符号

收集所有符号,建立符号表。

符号:变量名,函数名。记录函数,变量在那定义,类型是什么。

编译阶段产出:汇编代码.s,是人类刻度的汇编指令文本。

三.汇编阶段(gcc -c main.s -o main.o)

输入:.s汇编代码;输出:目标文件.o(Windows叫.obj),二进制文件。

汇编器as,把汇编指令翻译成CPU能识别的二进制机器码。

1.将汇编指令翻译成二进制,划分各个section(段)

  .test段:存放代码指令(函数二进制机器码);

  .data段:已经初始化的全局变量;

  .bss段:未初始化全局变量,不占用目标文件空间,只记录大小。

2.生成符号表

  记录本.o里面的函数,变量符号。

***重点:此时只是单个目标文件,符号没有分配最终虚拟地址!!!

如果调用别的文件的函数,汇编阶段只标记[这个符号待解析],不知道它的真实地址。

举例:main.o调用func(),func在另一个test.o里面。汇编阶段不知道func在哪,先记下来,留给链接阶段处理。

四.链接阶段(link,gcc把多个.o合并成可执行文件)

输入:多个.o目标文件+静态库.a;输出:可执行文件a.out/main.exe

两大核心步骤:

1.合并段,合并符号表,符号解析,分配虚拟地址

(1)把所有.o里面同名section合并:所有.text合并成一个大代码段;所有.data合并成数据段。

(2)合并所有目标文件的符号表。

(3)符号解析:找到每一个符号唯一的定义。

比如:main.o引用func,链接器去其他.o里面找func的定义,找到func对应的符号。

(4)给每一个符号分配最终的虚拟地址。

2.符号重定位(最核心)

重定位:把代码里所有引用符号的地方,把原来临时占位的地址,替换成刚刚分配好的虚拟地址。

举个例子:

main函数调用func(),在main.o里面call指令的地址是占位符。

链接找到func的虚拟地址,修改call指令,填入func真正的虚拟地址。

如果链接找不到符号定义:报未定义引用undefined reference,这是链接期错误,不是编译错误!

链接的两种类型:

1.静态链接

多个目标文件.o打包归档在一起的文件,不是可执行程序。

链接阶段行为:链接器扫描你的.o目标文件,发现你调用了库里的函数,直接把被调用函数对应的那一段机器码,从静态库里面拷贝出来,合并到最终的可执行文件中。

特点:

(1)运行时不再依赖原静态库文件,库代码已经完整复制exe/a.out,拿到这个可执行文件就能直接跑,移植方便。

(2)可执行文件体积更大,因为包含了库的机器码。

(3)缺点:多个程序如果都用同一个静态库,每个程序都会单独复制一份库代码,磁盘上存在多份重复代码,浪费磁盘空间。

(4)库升级:如果静态库升级,必须重新编译链接整个项目,重新生成可执行程序,否则程序依旧使用旧版本库代码。

总结:静态链接在编译链接期,把所需要的库代码复制进可执行文件,运行不依赖库,程序体积大,多程序会重复保存库代码。

2.动态链接

动态库后缀:Linux .so,Windows .dll

链接阶段行为:

不会把库的机器码复制进可执行文件。链接器只做两件事:

1.检查你调用的函数在动态库里面是否存在,完成符号校验;

2.在可执行文件里,仅仅记录依赖哪个动态库,用到哪些函数,只留下一张“函数索引表”。

程序运行时行为:

操作系统加载可执行文件到内存之后,再根据记录的依赖信息,去磁盘找到对应的.so/.dll动态库,把动态库加载进内存,然后完成符号重定位,把函数调用绑定到动态库内函数的真实地址。

特点:

(1)磁盘上只保存一份动态库,多个应用程序可以共享内存里这一份动态库代码,节省磁盘和物理内存。

(2)可执行文件本身体积更小。

(3)库升级:替换新版本的.so/.dll文件即可,不需要重新编译链接你的程序,下次运行程序自动加载新版本库。

(4)缺点:运行环境必须存在对应的动态库文件,缺少库直接运行失败(经典报错:xxx.so not found),移植时需要连带部署动态库。

总结:动态链接编译时只记录库依赖,不复制代码;程序运行时操作系统才加载动态库,多程序共享同一份库,程序体积小,但运行环境必须有对应动态库。

赞(0)
未经允许不得转载:171主机测评 » 编译四大阶段
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址