想必大家编写的第一个程序都是hello world,到后来编写越来越多的程序,那我们是否了解一个源文件是如何编译链接为.exe的可执行程序的呢?下面我们就来深入了解一下
在C/C++中,一个程序要运行起来,要经历四个阶段:预处理、编译、汇编、链接,最后形成可执行程序
由于windows下的vs系列是集编辑器、编译器、调试器等为一体的IDE环境,所以我们在Linux下演示
选项-E
:让 gcc 在预处理结束后停止编译过程。只进行预处理,不会生成对应的文件,所以需要-o选项输出到指定文件
选项-o
:文件输出到文件,也就是将结果输出到指定文件
gcc -E test.c -o test.i #将test.c预处理后的结果输出到test.i文件
所以预处理阶段完成了:
头文件展开、去注释、宏替换、条件编译
选项-S
: 让gcc在编译阶段结束后停下来
gcc -S test.i -o test.s #将编译阶段结束后的结果输出到test.s的文件
这里由于预处理后printf函数在main函数外调用所以报错,所以这里我们错误的调用printf函数就会在编译阶段报错
而汇编阶段的主要工作就是进行:词法分析、语法分析、语义分析、符号汇总,最后将c代码优化后变为汇编代码
那为什么要进行这些语法语义分析呢?
如果我们要将下面的英语翻译成翻译成汉语,首先需要判断这些字符串哪些能组成一个单词,然后在判断单词是否正确以及每个单词的意思,最后得到对应的语义。
而编译器也是如此,因为程序本质上就是一定字符集上的一字符串
,所以需要规定哪样的字符串是一个单词符号,检查代码的规范性、是否有语法错误等,还是需要判断单词符号的语法意义,是while循环,还是if判断或是其他语义,最后还有相应的优化生成中间代码最后翻译成汇编,当然这个过程是非常复杂的
gcc test.c Add.c -S #汇编阶段结束后停止编译,生成test.s和Add.s文件
符号汇总是非常重要的,最终是为了形成符号表,然后在链接阶段进行符号合并和重定向,而符号表包含名字(标识符)和此名字的有关信息
gcc -c test.s Add.s #程序编译到汇编阶段后停下来,将.s文件转换为.o的二进制目标文件
汇编阶段:将汇编文件转换成二进制文件,生成.o的目标文件(类似于windows下的.obj目标文件)且是可重定向目标文件,不可以直接执行,需要通过链接后才能变成可执行程序
可以看到test.o和Add.o二进制文件都是乱码
那么有的小伙伴就疑惑了,不应该是二进制吗?为什么都是乱码?
在linux下目标文件是以elf格式组织的,而我们通过vim编辑器查看是以文本形式查看的,所以都是乱码
readelf命令 #查看elf格式的文件信息
readelf -a [目标文件] #查看elf文件的所有信息
我们通过readelf命令可以看到Add.o和test.o文件的段表,链接的时候就需要合并段表
以及Add.o和test.o文件的符号表
在test.o中,main函数的size是67,而Add和printf函数的size都是0,因为这两个函数只有声明没有定义,所以需要通过下一步链接找到对应的函数,且它们是没有地址的,函数只有定义了才有地址
在Add.o文件定义了Add函数,所以size为20
objdump -S [文件名] #反汇编目标文件或可执行程序的命令
链接完成:合并段表,符号表的合并和重定位
在汇编阶段形成二进制文件后为什么不能直接执行,还需要链接呢?
因为要将程序中的各种功能模块组合起来,将代码中的函数调用、外部数据和库关联起来,比如通过包含头文件将相关的库链接起来,简单来说程序运行需要的目标文件和所依赖的库链接起来才能形成可执行的文件
前面我们在预处理阶段看到头文件stdio.h的展开,里面存放的只是一些库的所在路径,函数的声明等,那我们就需要在链接阶段链接库中对应的目标文件,以及我们在其他文件定义的函数
而可执行程序只有一个,所以将多个.o的目标文件链接起来,合并段表以及进行符号表的合并和重定位,链接库,最后形成可执行程序
而链接阶段就要涉及动态库、静调库以及动态链接和静态链接的概念了,我将在下一篇文章中介绍
今天的分享到这里结束了,希望的的位置对你有所帮助,欢迎点赞 ,评论,关注,⭐️收藏