前言
编译和链接的内容非常复杂深奥,这里只是笼统的讲解。
翻译环境和运行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令)。
第2种是执行环境,它用于实际执行代码。
怎么理解? C语言的代码是文本信息,比如#include<stdio.h>,本质是字母组成的文本信息。要给计算机下达指令去执行,计算机只能识别二进制信息。
就像现实中一个日本人只懂日语,一个韩国人只懂韩语,他们在交流时就需要翻译。这个翻译可以将日语翻译为韩语,也可以将韩语翻译为日语。
为了让计算机知道我们下达的是什么指令,需要将我们的代码翻译成二进制。这个过程我们依赖的环境就是翻译环境。在这个环境中源代码会被转换成可执行的机器指令。(二进制指令)
执行环境就在我们的计算机上。
每一个.c文件都要经过编译处理生成自己的目标文件,目标文件经过链接生成可执行程序,然后运行出来。可执行程序就是我们看到的**.exe**文件。
翻译环境
翻译环境是怎么将源代码转换为可执行的机器指令的呢?为了了解这一点,就展开讲一下翻译环境所做的事:
翻译环境是由编译和链接两个大的过程组成的,而++编译又可以分解为:预处理(或预编译)、编译、汇编三个过程。++
.obj 结尾的就是目标文件。 从test.c经过编译器处理生成.obj文件的过程就是编译过程,之后就是链接过程。每个.c文件都会单独经过编译器处理。
在vs中, 编译器叫cl.exe,链接器叫link.exe。在Everything中可以找到:
测试每个.c文件生成目标文件:
运行之后来到test_6_6>test_6_6>x64>Debug文件夹,发现确实多出了test.obj与add.obj两个目标文件。
这些目标文件再经过链接处理,生成可执行程序:来到test_6_6>x64>Debug文件夹,发现有.exe文件生成。
像visual studio这样的IDE,将这些细节隐藏了起来。所以我们不会去关注编译、链接等细节。
注意:在Windows环境下的目标文件的后缀是.obj,Linux环境下目标文件的后缀是.o。
多个目标文件和链接库一起经过链接器处理生成最终的可执行程序。
链接库是指运行时库(它是支持程序运行的基本函数集合)或者第三方库。
比如我们在使用printf这样的函数时,就需要链接库。
如果再把++编译器展开成3个过程++,就是下图的过程:
预处理
该阶段主要处理那些源文件中#开始的预编译指令。比如:#include,#define。处理的规则如下:
--将所有的#define删除,并展开所有的宏定义(替换)。
--处理所有的条件编译指令,如#if #ifdef #elif #else #endif
--处理#include预编译指令,将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。
--删除所有的注释(变为空格)。
--添加行号和文件名标识,方便后续编译器生成调试信息等。
--保留所有的#pragma的编译器指令,编译器后续会使用。
总结下来都是一些++文本操作++。
经过预处理后的.i文件中不再包含宏定义,因为宏已经被展开,并且包含的头文件都被插入到.i文件中,所以当我们不知道宏定义或头文件是否包含正确时,可以查看预处理后的.i文件来确认。
编译
编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件。
词法分析
将源代码程序输入扫描器 ,扫描器的任务就是简单地进行词法分析,把代码分割成一系列的记号(关键字、标识符、字面量、特殊字符等)。
比如这段代码:
array[index] = (index+4)*(2+6);
进行词法分析后就是:
语法分析
接下来的语法分析就是语法分析器 对扫描产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为节点的树。
语义分析
由语义分析器来完成语义分析,即对表达式的语法层面分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包括声明和类型的匹配,类型的转换呢。这个阶段会报告错误的语法信息。
汇编
汇编器是将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。就是根据汇编指令和机器指令的++对照表++一一地进行翻译,也不做指令优化。
其实还会做一件事,就是形成一个符号表。也就是将全局的符号的名字汇总下来,举例:
对于这个.c文件,符号表中又Add 以及这个函数的地址0x100(假设的)。
而对于下面这个.c文件:
符号表中有main及其地址0x104(假设),Add及其地址0x000(无效地址)。局部变量是在程序运行起来才有的,这个阶段是根本没有a b c这些变量的。
最终完成的是将汇编代码翻译成二进制(机器)指令。最后生成目标文件,以.o为后缀。(每个.c文件都有一个)
链接
是一个相对复杂的过程,要将一堆文件链接在一起生成最后的可执行文件。
链接过程主要包括:地址和空间分配,符号决议和重定位这些步骤。
链接解决的是一个项目中多文件、多模块之间互相调用的问题。
记得吗,不同文件在链接后要生成++一个++ 可执行程序。所以在链接阶段,会有符号表的合并和重定位。回到上面两张图的例子,两个.c文件符号表里都有Add,首选哪个地址呢?刚才说了test.c里的Add的地址是无效地址,所以编译器决议后选择留下0x100地址。这就是合并,冲突时决议看保留哪个地址。这时生成的新的符号表就在可执行程序中。
当我们想要调用Add函数时就查这个表得到Add地址,就可以调用这个函数了。
假如这时我们在test.c中将Add误写为了add,那么在链接合并符号表时Add add地址都被保留,add的是无效地址,当我们调用时根据这个地址找不到我们要的函数。
所以,在外部文件中定义的全局变量,使用extern,也可以使用。
目标文件和可执行程序的格式其实是一样的,文件的组织形式是一样的,在Linux底下用的都叫elf的格式...
运行环境
(生成可执行程序后,要依赖运行环境将程序运行起来。这个过程其实也是比较复杂的,这里笼统地将。)
1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。(比如单片机的设备里面可能是没有操作系统的。)
2.程序的执行便开始,接着便调用main函数。
3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4.终止程序。正常终止main函数;也有可能是意外终止(断电了、程序卡死奔溃了等)。
(操作系统代劳)
到此,本文结束,祝阅读愉快^_^