一. 程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码
二. 详解编译+链接
1.翻译环境
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
也就是说,翻译本身就包含两个阶段:1.编译阶段 2.链接阶段。那么每个阶段分别做什么事情呢?
对于一个test.c文件,在gcc编译器下测试,翻译阶段做的事情。
#include <stdio.h> #define N 10 int g_val = 10; //this is a comment for test int Add(int x, int y) { return x + y; } int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int i = 0; for(i=0; i< N; i++) { printf("%d ", arr[i]); } printf("%d\n", g_val); return 0; }
下面在centOS环境下使用gcc演示上面的一串简单代码。
第一步,预编译
由于预编译之后的结果会直接在终端显示,我们包含了stdio.h头文件,代码过多,因此重定向到test.i中方便查看。执行代码
gcc -E text.c -o test.i
我们可以看到执行完毕之后产生了test.i文件,打开为文件之后我们可以看到。
我们可以看到有很多行代码,在这里最后面几行很熟悉,这就是我们在test.c中写的代码,仔细观察可以发现有几种变化,1.少了"#include"这一行,这是因为头文件展开,产生了前面的几百行代码。2.我们用于测试的注释也消失了。3.for循环中的N被替换成10了,并且#define消失了。这就是预处理阶段做的事情。
注:由此可以看出头文件重复包含是一件很严重的事情,所以我们要避免头文件重复包含,这个我们后面再说。
第二步,编译
执行代码
gcc -S test.c //或者gcc -S test.i
可以看到生成了一个test.s文件,打开test.s文件
这时可以看到,已经转换成汇编代码了,所以在编译环节,做的事情就是把C语言代码转换成汇编代码,并且做语法分析、词法分析、符号汇总、语义分析。
在符号汇总的过程中,会把全局的符号全部汇总出来,在上述的例子中,会汇总的就是g_val,Add,main三个,其余的局部变量只会在程序执行的过程中产生。
第三步,汇编
执行代码
gcc -c test.c
可以看到生成了test.o文件
打开以后,发现我们已经看不懂里面写的是什么了,这就是已经被翻译成二进制,
汇编代码做的事情就是:1.把汇编代码翻译成二进制。2.形成符号表(上面符号汇总的符号在这里会形成符号表),后边如果使用这些符号的话,就可以对表寻址。
第四步,链接
我们在上面生成的目标文件,如果有多个源文件,最后编译完成以后,会生成多个.o文件,此时整个工程中各个源文件都是单独存在的,互相不知道的,所以在链接操作过程中,会合并段表(将多个源文件和链接库合成),符号表的合并和重定位:在汇编过程中,每个文件会生成一个符号表,,这时候会将这些符号表合并,然后如果发现有重合的符号或者地址无意义的符号,会被重定向,筛选。
2.运行环境
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。