计算机中所有的文件都是以01数字形式保存的,我们常见的程序文件也是如此,以常见的入门程序Hello.c为例,其代码为代码段1所示:
代码段1
//hello.c #include <stdio.h> int main() { printf("hello, world\n"); }
机器中所有的字符都是用数字进行表示,可以按照ascii码表对比一下,将该Hello.c文件中每个字符用ascii代码表示,如下所示:
在文件中存储的都是绿色行表示的数字,当然,在实际的存储过程还不是这些十进制数字,而是’0’、’1’.
上述Hello.c文件需要经过以下几个步骤才能变成可执行文件,这是一种特殊的能被机器识别并执行的文件。
为什么可执行文件能被机器识别并运行,而其他的文件例如上面的Hello.c或者自己定义的world文档、txt文档就不能被机器识别呢? 简而言之,可以认为这些可执行文件是按照机器能够识别的格式进行存储的。以说话为例,假如张三会说汉语和日语,如果你对着他说英语或韩语,他也就听不懂了。这种特殊的可执行文件的格式一般都是COFF(common file format)格式,该格式在不同操作系统下具体的实现方式还是不一样的,例如linux系统下可执行文件就是ELF文件格式,Windows下面的可执行文件exe就是PE文件格式(例如main.exe),这两种格式都COFF文件格式的变种。 |
程序的编译过程大致要经过图1 所示的步骤之后才能变成一个可以被机器直接识别和执行的“可执行文件”。
图1、编译过程
编译过程是以源文件为单位进行的,头文件不会单独编译而是放在包含它的头文件中一起编译。例如代码段2总共有“test.h”、“test.c”、“main.c”三个文件,在编译的时候,编译器将对源文件“test.c”和“main.c”进行编译,而头文件“test.h”将不会被作为编译单元进行编译。
代码段2
//test.h int add(int iNum1, int iNum2); //test.c #include "test.h" int add(int iNum1, int iNum2) { return iNum1 + iNum2; } //main.c #include <stdio.h> #include "test.h" #define NUM1 10 #define NUM2 20 int main() { int iRes =0; iRes = add(NUM1,NUM2); printf("NUM1+NUM2=%d",iRes); }
1、预处理,该过程主要将一些头文件(例如test.c中的test.h文件)、宏定义之类的替换过来,也即,如果你包含了哪些头文件就把那些头文件拷贝过来,因此经过这一步处理之后,test.h就被复制到了文件“test.c”和“main.c”中,同时“main.c”中define的两个宏NUM1和NUM2也被替换成所表示的数字10和20;如果程序中当然程序中的注释也会在这一步被去掉,所以程序中适当的加入注释并不会影响程序的性能也最终生成可执行文件的大小的。以linux为例,使用下面两条命令:
gcc –E test.c –o test.i
gcc –E main.c –o main.i
就会将“test.c”和“main.c”进行预处理,然后生成“test.i”和“main.i”两个文件,这两个文件。
2、编译,该过程主要对源文件进行一系列的处理,例如词法分析、语法分析、语义分析和优化等,也就是编译原理课程上面讲述的那些步骤,经过这些步骤之后,每一个编译单元都对应生成了一个汇编文件。在linux下,使用如下两个命令:
gcc –S test.i –o test.s
gcc –S main.i –o main.s
可将预处理之后的.i文件生成对应的汇编文件。
3、汇编,该过程主要是将编译生成的汇编文件翻译成机器码,由于每个汇编命令就对应一个特定的机器指令,因此只需要将汇编文件中的汇编质量替换成机器指令就可以了。在linux下,使用如下两个命令:
gcc –c test.s –o test.o
gcc –c main.s –o main.o
可将编译过程生成的汇编文件生成目标文件。
4、链接,理论上将,如果不依赖于外部的东西,经过汇编之后的目标文件里就是机器可以识别的机器码了。再回头看看生成的“test.o”和“main.o”两个目标文件,由于我们在“main.c”文件中使用了函数add,在编译的时候也包含了对应的头文件“test.h”,但是add函数是在另一个编译单元“test.c”中定义的,在机器执行main函数中调用add函数时就需要知道add的地址在哪里?这是就需要连接器了,连接器会搜索所有的编译单元生成的目标文件,看看这个add函数在哪个目标文件里,找到之后就将add函数的地址填在调用它的地方。这样程序在调用add的时候就知道去哪里找它了。
参考《深入理解计算机系统》、《程序员的自我修养——动态链接、装载和库》