目录
1.预编译
2.编译
3.汇编
4.链接
5.加载/运行
6.一键编译指令
C++代码从编译到最终执行,中间大致分为5个步骤:预编译、编译、汇编、链接、运行。
1.预编译
预编译,又叫预处理。执行指令过程为:g++ -E a.cpp -o a.ii。这个过程,主要做的事情有:
1.清理代码注释。
用// 或/* */注释的代码,都会被清空。
2.替换宏定义。
比如 #define PI 3.14,代码中使用了这个宏,如 double value = PI。那么预编译时候,PI会被直接替换为3.14,即double value = 3.14。
3.插入include引入的文件内容
把引入头文件的所有内容,直接复制粘贴到当前文件中。所以a.ii通常会比a.cpp大不少。
4.清理条件编译代码
条件编译关键字有#ifdef、#ifndef、#if、 #else、#elif 、#endif,会按具体条件逻辑保留部分代码,删除另外部分代码。
5.添加行号
便于debug调试使用或报错时定位哪一行代码。
2.编译
执行指令过程为:g++ -S a.ii -o a.s。主要用来对预编译后的文件代码,进一步检查语法、语义、词法等是否正确。比如变量类型错误、缺少“}”、缺少“;”等等,都是在这里检查。
此外,编译还会对代码进行优化,比如,int a=4+6。编译时候会被改为int a=10。
3.汇编
执行指令过程为:g++ -C a.s -o a.o。主要用来将编译生成的汇编代码转换为二进制机器指令。不通平台或系统,这里生成的肯定是不一样的。目标文件格式Windows下是.obj,linux下是.o。
汇编在生产二进制指令时,还会做一个重要事情,就是生成符号表。那什么是符号表呢?顾名思义,符号表存储的就是一堆符号。而符号,指的就是代码中的变量、函数名、全局常量等。符合有很多属性,包括名称、类型、作用域、地址、行号等。
比如debug时候,生成的可执行文件比较大,就是因为里面包含了符合表信息。在debug时候,能看到函数调用关系,也能知道哪一行报错了。如果没有符合表,比如release版本,保存时候,看到的都是各种16进制地址。
4.链接
执行指令过程为:g++ a.o b.o -o main。主要用来将目标文件、系统库、第三方库.o/.a/.so文件连接起来,生成一个可执行的文件。这里面又可以细分为几个功能。
1.合并段
主要用来合并各个目标文件的代码段 (.text)、数据段 (.data/.bss),统一布局内存地址。
可以用如下命令查看可执行文件中各个段的情况。
readelf -S main查看结果如下。里面的代码段.text和数据段.data和.bss都能看到。
2.符合解析
匹配引用符号(函数 / 全局变量)和真实符号定义
3.重定位
修正代码里的地址占位符,把外部函数调用、全局变量访问地址改成真实内存偏移地址
4.库链接
静态链接(.a/.lib):把库直接切入到可执行程序中 。下次可以独立运行,不需要额外依赖库。缺点就是体积比较大。
动态链接(.so/.dll/.dylib):体积小,但运行时必须有额外配套的依赖库,本身独立运行不了。
5.加载/运行
当执行命令./main时,程序开始运行。中间可以细分为如下几个过程。
1.创建进程虚拟地址空间
2.将可执行文件的代码段、数据段映射等载入进程虚拟内存
3.处理动态链接:动态链接器(ld-linux.so/msvcrt.dll 等)加载依赖的动态库,完成运行时符号重定位。
4.初始化全局变量、静态变量、初始化栈 、堆等
5.设置程序入口(不是直接 main 函数!是系统启动函数 _start / CRT 运行时库入口)
6.一键编译指令
直接用 -o 命令,一步生成可执行文件。
gcc main.c -o mian