C++:5.编译加载过程

目录

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