思考
我们在VS创建一个test.c
文件,输入以下内容:
int main()
{
printf("hello world!!!");
return 0;
}
在VS上运行这段代码,毫无疑问会在我们的控制台窗口打印hello world!!!
在这段文字
那么大家有没有想过,这期间到底发生了什么?
难道仅仅就是:计算机阅读源代码,然后输出结果吗?
实际上,我们.c后缀的源代码
首先会形成一个.exe后缀的可执行文件
,可执行文件
运行之后才会在相关的输出设备
做出反馈。
上面这段代码,其实就是printf
经过翻译环境,形成了相应的二进制指令,这段二进制指令在CPU中运行,调用了系统调用api
,在控制台窗口进行打印。
接下来我们就来详细分析一下,在翻译环境
和运行环境
中到底做了什么。
翻译环境
接下来我们用一个模拟加减计算器的简单程序解释编译的过程:
add.c
extern int Add(int x, int y)
{
return x + y;
}
sub.c
extern int Sub(int x, int y)
{
return x - y;
}
test.c
extern int Add(int x, int y);//加法函数声明
extern int Sub(int x, int y);//减法函数声明
int main()
{
int a = 1;
int b = 2;
int c = 0;
int d = 0;
c = Add(a, b);
d = Sub(a, b);
printf("%d", c);
}
整个翻译环境
大致上可分为编译
和链接
两部分。
在编译阶段,编译器会将所有的源文件(.c格式)
逐个转化成目标文件(.obj格式)
,
所有目标文件
再加上链接库
,经过链接器的链接,形成一个可执行程序
链接库:
在test.c
函数中我们使用了printf
这个函数,但是我们并没有写,它的具体实现方式封装在了库里,如果想让最后的可执行程序正常运行,那必须把这部分文件也链接进去。
以下就是printf
这个库函数对应的链接库
,这些静态库都是以.LIB
作为后缀的。
编译
整个翻译环境
可分为编译
和链接
两个大阶段,而编译
又可细分为三个阶段。
接下来,我们用linux
的文件命名规则,为大家展示一下这三个阶段,和这三个阶段发生的操作。
预编译
注:预处理也可叫预编译
这个阶段做一些文本替换
,替换之后的test.i
文件也是C语言的语法
- 头文件的包含(#include):用头文件的所有内容替换写#include<>的那一行
- 条件编译(#if):判断是否删除或保留#if和#endif中间的代码
- 删除注释:将注释掉的代码用一个空格替换
- 宏替换(#define定义符号的替换):将宏的文本内容替换到代码中
具体预处理指令,如”#define 、 #if 、 #include 、 #pragma……“
,大家可以看一下这篇博客:点这里
编译
这个阶段是把C语言的代码转化成汇编代码
,这里红框框里的就是翻译之后的汇编代码。
在这个过程,做如下事情:
- 语法分析:编译错误就是在这个阶段发现的。
- 词法分析
- 语义分析
- 符号汇总
这里符号汇总就是把整个.i文件
中出现的函数名汇总起来。
汇编
注:这里的.o文件是linux的命名方式,与Windows的.obj相同
把汇编代码转换成二进制的指令,如果说一直到.s文件的汇编语言是我们肉眼能看懂的,那到了.o文件,就成了01组成的二进制指令,就只有CPU能看懂了。
汇编过程我们只区了解一个操作:
- 形成那个符号表。
是否还记得编译过程中的符号汇总
,这里将给汇总的所有符号加一个地址,形成符号表:
如果只有函数声明,那将其地址进行标记,全部置0.
链接
这个过程我们研究两件事:
- 合并段表
- 符号表的合并和重定位
合并段表
在生成.o文件
的过程中,将文件分成了一个一个的段
,每个段有自己的作用,在链接阶段,把每个.o文件
中相同作用的段进行合并。
符号表的合并和重定位
把之前编译阶段生成的符号表进行合并,其中由于申明置的全0,现在把定义位置的地址赋过去。
在这个多文件链接的过程中,同时也会通过符号表查看来自外部的符号是否存在,平时我们遇到的链接错误就是在这里发现的。
我们想想,如果在main函数中没有对Add、Sub进行声明,那么会出错吗?
答案是虽然有警告,但是依然能过。
就是因为即使没有声明,符号表合并之后也能找到它们的地址。
总结
运行环境
程序执行的过程:
-
程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
-
程序的执行便开始。接着便调用main函数。
-
开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
-
终止程序。正常终止main函数;也有可能是意外终止。