目录
0. 说在前面
你是否有过疑惑 , 你在Dev-C++,VS2022等编译器上写好了一个test.c文件 (或是其他C/C++的代码) , 为什么直接点击开始运行就能够立刻执行进而得出执行的结果呢 , 这中间是不是有什么过程没有被我们看见 , 难道我们是直接执行test.c吗得出的结果吗? 如果你没有深入了解过这个问题 , 或者是还有一个或几个过程没有想明白 , 或者只是浅尝辄止 , 那么恭喜你 , 这篇博客就是为你准备的!!! 来提升你对文件编译执行过程的格局吧! 少年!
本文所有代码已上传至Gitee , 请君自取:
Linuxtwo: This is the second learning of linux (gitee.com)https://gitee.com/onlookerzy123456qwq/linuxtwo
1. 预备知识
在计算机当中 , 计算机能够直接执行的文件都是可执行文件 , 即我们熟悉的.exe文件 。test.c文件是不能被计算机直接执行的! 下面我们简单介绍一下原因。
首先我们的计算机只认识二进制代码即机器语言 , 即我们不能直接执行诸如像汇编语言, C语言 Java C++ 这些非机器语言的代码 , 我们要首先将这些代码翻译成机器代码才能让计算机执行 , 这个翻译的过程叫做编译 , 编译过程包括预处理 , 编译 , 汇编三个步骤 , 翻译生成的文件叫.obj目标文件。不过即使我们将test.c等使用非机器语言的文件翻译成了机器语言 , 这时其实也还是不能直接执行的 , 我们还需一个步骤 叫链接, 链接多个目标文件成功之后形成的文件才是.exe可执行文件 , 这时我们才能去执行并得出结果!!!
所以 , 接下来我们就将 这 3+1 个从.c源文件生成.exe可执行文件的步骤进行逐个的分解说明 , 来看一下每一个步骤都发生了什么 , 即预处理 , 编译 , 汇编 , 链接每个步骤做完之后 , 这个文件里面的内容会做什么修改。
PS : 翻译的过程 , 即把源文件的C语言代码翻译成机器语言代码的这个过程 , 我们更准确的叫做编译 , 编译过程中使用的工具叫做编译器 , VS环境下的编译器是cl.exe , Linux环境下的编译器是gcc/g++ , 编译的这一个大过程又可分为三个步骤, 分别是 预处理 , 编译 和 汇编 , 编译过后就最后生成了机器语言的文件test.obj 。 最后一个步骤叫链接 , 所依托的工具则是链接器 , VS环境下使用的链接器叫做link.exe , 最后将库和多个.obj目标文件经过链接后 , 生成了test.exe可执行文件。
我们要拆解这四个将.c文件变为.exe文件的步骤 , 就需要编译过程的分解 , 这点在集成开发环境的VS2022上我们是难以做到的 , 所以我们选择在Linux环境使用gcc来进行分步骤的实验。
2. 预处理
使用指令:
gcc -E test.c -o test.i
#对test.c进行预处理得到test.i
对test.c所作操作:
1.展开头文件 (#include)
2.宏替换 (#define)
3.去注释 (// /**/)
4.条件编译 (#ifdef #endif)
#我会在之后的博客对宏替换和条件编译做详细的介绍
主要作用:
对头文件的内容展开 , 宏完全替换 , 注释文本的去除 , 以及条件编译。我们通过下图可以看出 , 其实做的都是文本操作。这些文本操作的作用其实就是使得C语言代码能够更加的规范 , 去除不必要的内容 , 得到需要的内容 , 从而使得新处理好的C语言文件能够方便进行下一个步骤(编译)。
产生结果 :
生成了 test.i 文件 , 即对源文件test.c预处理后得到的结果文件 , test.i中存放的是被预处理过的C语言代码内容。
我们举个例子 就比如 #define MAX(X,Y) (((X)>(Y))?(X):(Y)) 我们在test.c中定义的宏 , 如果test.c中我们有一句代码叫做 int ret2 = MAX(2+1,5) , 那么经预处理后在test.i文件里 , int ret2 = MAX(1+2,5);这句代码就会被文本替换处理成 int ret2 = ((2+1)>(5)?(2+1):(5)); 。
3. 编译
现在test.c经过预处理阶段的文本处理 , 得到了更为规范的C语言代码内容 , 形成了test.i文件 , 现在test.i文件里的C语言代码才可以交给编译器执行编译的步骤。
使用指令:
gcc -S test.i -o test.s
#对test.i进行编译步骤得到test.s
对test.i所作操作:
1.语法分析
2.词法分析
3.语义分析 #有1. 2. 3.才能翻译成汇编代码 , 为进一步的汇编做准备
4.符号汇总
#最终将test.i中的C语言内容翻译成为了汇编语言的内容
主要作用:
编译过程的主要作用是 , 先对test.i的代码进行语法分析,词法分析,语义分析(C语言语法词法语义分析这三个准备工作做好之后才能把C翻译为汇编) , 再把test.i中预处理后的C语言代码翻译生成汇编语言。并进行符号汇总 , 为下一步骤(汇编)进行生成符号表做准备。
产生结果 :
生成了 test.s 文件 , 里面存放的是test.i经过编译后生成的的汇编语言的代码内容。
4. 汇编
现在test.s经过预处理和编译 , 里面就是汇编代码的内容 , 而计算机只认识二进制机器语言 , 所以文件就需要经过汇编这一步骤 , 把汇编语言翻译成计算机能够认识的二进制机器语言。
使用指令:
gcc -c test.s -o test.o
#对test.s进行汇编得到test.o
#事实上 , 汇编生成的这个test.o文件和VS2022中生成的.obj目标文件性质是一样的。
对test.s所作操作:
1. 翻译成机器码
2. 生成符号表
主要作用:
汇编过程主要就是把test.s里的汇编语言的内容翻译成机器语言 , 即计算机能够直接认识的二级制语言 。 然后 , 编译那一步我们进行了符号汇总 , 再到汇编这一步我们就会根据汇总的符号生成符号表。
产生结果 :
生成了 test.s 文件 , 里面存放的是test.i经过编译后生成的的汇编语言的代码内容 , 以及里面会生成汇总的符号表。
下面我们从实践的角度来看一下test.s->test.o汇编过程做的这两个操作。(翻译成机器码+生成符号表)
为什么我们无法读懂查看test.o这里面的二进制机器指令呢 ?
那是因为test.o这个文件是elf格式的 , elf格式其实就是一种文件内部的组织方式 , elf格式的文件 , 简单来说就是将文件的内容划分成了一个一个的段 , 每个段中存放不同的内容 , 然后所有.o文件里的段划分的方式都是一样的(.o文件都采用elf格式 , 不同的.o文件仍然保持同样的段划分方式 , 这个实际上是为后面链接步骤的合并段表做准备)。
我们无法直接看懂以多段形式划分的elf格式的test.o文件 , 这时候我们就需要借助Linux下的一个工具 , 叫readelf工具 , 借助这个工具可以读懂elf格式的.o二进制机器指令文件。
符号汇总的都是源代码中带有全局性质的符号名 , 例如 全局变量 ,定义的函数名 ,使用的库函数名 ,这些都是符号表中出现的符号 。
比如我们在test.c中定义了一个全局变量int global_num = 0; , 比如test.c中有一个带有全局性质的函数 , 比如main函数 , 定义的Add函数 , 比如库函数printf , 这些带有全局性质的都是最终被汇总的符号 , 都会出现在test.o的符号表当中。
5. 讲述链接的预备工作
至此 , 我们讲完了编译的全过程 , 预处理-编译-汇编 , 我们现在能将一个.c源文件变成二进制机器语言的.o目标文件 ! 下面我们就要讲链接过程 ,即如何把这些.o文件以及链接库合并成一个可执行文件。
不过在此之前 , 我们需要先做一下准备工作 , 由于我们之前为了讲述方便我们只有一个test.c文件进行编译全过程 , 只得出了一个test.o文件 , 并不能很好的体现后面的链接多个.o目标文件的过程 , 所以我们修改一下代码 , 同时也能更好的帮助我们了解汇编形成的符号表以及链接合并符号表的过程。
然后我们执行编译以及链接 , 生成多个目标文件。
生成的多个目标文件(即Linux下是多个.o文件) , 每一个.obj文件都是由段划分 , 且都有一个符号表。
6. 链接
经过汇编生成了二进制机器语言的目标文件 , 但是即使计算机认识 , 这仍然是不可以执行的。因为缺少最后一步叫做链接。即需要我们将多个.o文件以及链接库 , 交给链接器进行链接 , 链接合并成一个可执行文件。
所作操作:
1. 合并段表
2. 符号表的合并和重定位
主要作用:
每一个.o文件都是elf格式以段形式划分的文件 , 所以会将每个.o文件的按照一段对应一段的方式进行合并 , 合并出新的段表。同时 , 每一个.o文件都有一个符号表 , 链接过程也会把这一个个的符号表进行合并 , 合并出一个总的符号表。
产生结果 :
把所有目标文件的段与所有目标文件的符号表都进行了合并 , 链接所有目标文件和链接库 , 生成了最终的可执行文件。
上面就是段表的合并 以及 符号表的合并 , 最终合并多个目标文件以及链接库形成了一个可执行文件 , 最后我们的.c文件就变成了.exe文件 , 就可以被计算机直接执行了!!!
7. 链接的补充知识1
你是否有过疑惑 , 如果有的函数只声明却没有被定义 , 但是你使用了该函数的话 , 这个文件在生成可执行文件的过程当中 , 是在哪一个步骤被检查出来的?想必我已经提示你出答案了 , 那就是链接过程。
在链接之前 , 每个目标文件里的单符号表中 , 有的函数符号是可以未定义的 , 即地址可以暂时为空 , 因为这个函数可能是在别的源文件当中实现出来的 , 我们下一个过程可以链接使用。但是如果你链接之后生成的符号表某个函数符号仍然是空 , 即该函数没有被实现出来 , 那就不对了 , 因为我们下一步就要生成出可执行文件了 , 需要使用的函数为空的话 , 即不能找到该函数的实现的话 , 我们的可执行文件就不能执行成功 , 也就不能生成可执行文件 。那链接过程是如何检查出该函数没有被定义的呢 , 这就是依托的是合并出的总符号表啦 , 如果最终合并出的总符号表中有的函数为空0x0000 , 那就可以检测出该函数未被定义了。下面我们以一个例子描述这一过程。
所以检查函数是否为定义 , 是最终我们到了链接过程合并出总的符号表后 , 查看有的函数仍然没有被定义(函数地址为0x0000空 , 寻址找不到该函数的定义) , 而且这个函数还被使用了 , 这时链接器就会报错 , 也就无法最终生成可执行文件。