🔖编译
预编译结束接下来就该进行编译了,要得到编译后的文件需要用到下面这条命令:gcc -S text.i
其中-S表示编译结束后就停下来。
text.i也可以换成text.c。
这里不需要-o选项,因为编译器默认会生成text.s文件,当然这里我们也可以用-o选项指定生成的文件名,这里大家可以自行尝试。
看不懂,根本看不懂,这text.s文件中放的到底是啥呀?😭,其实这些都是汇编指令。总结:编译其实就是把C语言代码翻译成了汇编代码,但这仅是我们通过观察现象得到的结论,编译过程究竟干了什么呢?总结一下,其实编译过程干了下面几件事:
语法分析(检查是否有语法错误)
词法分析(会把代码肢解开形成一颗语法树)
语义分析(分析每段代码是干嘛的)
符号汇总(会把代码中涉及到的一些符号,例如:函数名、全局等的符号汇总下来,在下一步汇编中使用,不关心局部变量,因为局部变量只能在局部范围使用)
做完上面四件事产生的结果就是得到汇编代码
🔖汇编
编译结束接下来就到汇编了,汇编需要用到下面这条指令:gcc -c text.s
- 其中-c表示编译结束就停下来。
- 这里不需要
-o
选项,因为编译器默认会生成text.o
文件,当然这里我们也可以用-o
选项指定生成的文件名,这里大家可以自行尝试。 - 这里
.o
结尾的其实就是在gcc环境下生成的一个目标文件,在VS中生成的目标文件后缀是.obj
。 - 通过上图可以看出目标文件是一个二进制文件,这意味着:汇编过程把汇编代码翻译成了二进制指令。同样这是我们通过观察得出来的结论,汇编过程最重要的是形成符号表,这和编译阶段执行的符号汇总是相关联的,符号表把编译阶段汇总的符号与其地址对应起来形成了一张表,这张表就被叫做符号表。符号表在链接这个阶段还要被使用。
🔖链接
链接阶段主要干了下面两件事:
- 合并段表(编译得到的目标文件都是一个独立的ELF文件)
- 符号表的合并和符号表的重定位
- 上面提到过,编译器对每个源文件是进行单独处理的,最终每个源文件都会得到一个目标文件,因此每个目标文件都会有一个符号表,以下面的程序为例:
text.obj中的符号表记录了Add和main,add.obj中记录了Add,但实际上在text.c中没有Add函数的定义,所以text.obj中的符号表并不知道Add函数的真实地址,而add.obj中的符号表,则记录了Add函数的正确地址。在进行符号表合并的时候,两个表中都有Add和地址的对应关系,但text.obj中的对应关系一定是错误的,所以就会舍弃它,保留add.obj中正确的Add和地址的对应关系,同时mani和地址的对应关系也会被保留,最终的可执行程序中的符号表就是合并后的符号表。
🔖符号表的作用
符号表记录了一些函数名、全局的符号和地址的对应关系,最终汇总符号表可以帮助我们实现跨文件函数调用,就像上面的程序,我们在text.c中通过extern关键字声明了外部符号Add,然后就可以在当前的源文件里去调用add.c中的Add函数,其实没有这条声明语句程序也能正常运行,因为就算声明了,text.obj中记录的也是一条无意义的地址对应关系,最终在汇总符号表的时候还是会被删除。但是注意:如果要使用另一个源文件中的全局变量,是一定要声明的,否则会报错。如果没有用extern去声明,在编译阶段进行语法分析的时候就会报错。这里函数和全局变量的差异仅仅是编译器自己对这两种情况的处理方式有所不同,正确的做法是只要使用了另一个源文件中的函数、全局变量等都要通过extern进行声明。
再通过下面的程序看看符号表的作用
text.obj中的符号表记录了一条add和地址的对应关系,最终会汇总到总的符号表当中,当然这个对应关系是不存在的,因为我们压根就没对add进行任何定义,最终到这个“虚假”的地址里面当然就什么都找不到,自然就报了“链接错误”
📖执行环境
程序的执行主要有以下几个过程:
程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统来完成。在独立的环境中,程序的载入必须由手工安排,也可以是通过可执行代码置入只读内存来完成(其实就是平时我们把代码下载到单片机板子上的这个过程)。
程序的执行便开始,接着便调用main函数。
开始执行程序代码。这个时候程序将使用一个运行时堆栈(Stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储在静态内存中的变量在整个程序执行的过程中都会保留他们的值
终止程序。正常终止main函数,也有可能是意外终止。
如上图,我们双击以.exe结尾的可执行文件就会进入到执行环境,此时程序已经被加载到内存中。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!