上一篇:从0开始学c语言-38-题目练习_阿秋的阿秋不是阿秋的博客-CSDN博客
说起来,你有没有想过你写的程序是如何运行起来并让你看到结果的?
这一篇,我们就学这个。但是说实话,这个运行过程通过vs编译器去实现不够细节,所以主要展示了liux环境(在总结处放上了学习VIM的学习链接)。
目录
1. 程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第 1 种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第 2 种是执行环境,它用于实际执行代码。
2. 详解编译+链接
2.1 翻译环境
首先写一段简单的代码,
#include <stdio.h>
int main()
{
printf("%s\n", "阿秋");
return 0;
}
运行后,我们可以看到,生成了一个后缀为(.exe)的文件,这个文件就是我们说的可执行程序,也是通过翻译环境后的成果。
这里画个图示意一下翻译过程。总的来说,翻译环境分为两大部分:编译和链接。而编译对应的就是把源文件生成目标文件,编译依赖的是编译器(就是cl.exe,可在安装目录中寻找),链接依赖的是链接器(link.exe)。
1·组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code )。2·每个目标文件由链接器(linker )捆绑在一起,形成一个单一而完整的可执行程序。3·链接器同时也会引入标准C 函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
这里链接库需要介绍一下,比如我们用到了printf函数,它所对应的静态库(.lib)就是我们的链接库。
2·2编译
编译环节分为三步:预编译(预处理)、编译、汇编。
先写一段代码
1. 预处理 选项 gcc - E test.c - o test.i预处理完成之后就停下来,预处理之后产生的结果都放在test.i 文件中。2. 编译 选项 gcc - S test.c编译完成之后就停下来,结果保存在test.s 中。3. 汇编 gcc - c test.c汇编完成之后就停下来,结果保存在test.o 中。
预处理
为了能够把每一步产生的结果分开,便每次保存到不同的文件中。
比如:输入 gcc test.c - E > test.i
这个意思就是把预处理产生的结果放在test.i文件中。
打开文件如图。
你会发现前面的一些东西你可能看不懂,但是留意一下这些名称。
在观察一下,这些词似乎被(stdio.h)包含着。通过指令,可以看到我们引用的头文件。
打开后,可以找到我们刚刚看不懂的名词。
所以我们引用头文件的时候,就是把头文件的内容拷贝过来。
预处理阶段:
1·会产生一个没有头文件(都已经被展开了)
2·宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了)
3·没有特殊符号的输出文件,删掉注释
这个文件的含义同原本的文件无异,只是内容上有所不同。
编译
这个在《编译原理》中介绍很详细。
输入:gcc test.c -S
编译完成之后就停下来,结果保存在test.s中。
这个文件中放的是汇编代码,所以编译后产生了汇编代码。
将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件(符号汇总)。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
汇编
输入:gcc test.c -c(小写)汇编完成之后就停下来,结果保存在test.o(类似test.obj) 中。
把汇编代码转换成机器指令(二进制指令),这个阶段生成符号表 ,同样也不是你肉眼就能看懂的文件,test.o文件是elf格式的文件,可以用readlf文件查看。 test.o把文件内容划分为不同的段,每个段中存放的数据不同。
生成的符号表(test.o) 用readlf文件查看,可以看到把编译阶段汇总的符号表(汇总了全局符号)进行了符号表的生成。
为了更加理解符号表,我们用这样一段代码进行阐述。
画个图示意一下吧。经过编译汇总的符号不会包含局部符号,因为局部符号只有进入函数后才会生成。经过汇编后,对前面汇总的符号生成对应的符号表。而这个符号表就是我们所谓的test.o(类似test.obj)的目标文件。
在目标文件中可以查到我们的符号,可以看到在add.o文件中确实可以看到Add符号。
2·3 链接
在链接阶段把多个目标文件和链接库进行链接,主要完成了两件事:
1·合并段表
2·符号表的合并和重定位
1·合并段表
你可能不知道段表是什么,别忘了之前我们曾说过,是什么文件对数据进行了分段处理呢?
是汇编阶段生成的 test.o文件 把文件内容划分为不同的段,每个段中存放的数据不同。
而我们现在是有两个源文件的,那么会生成两个后缀为(.o)的文件。这两个文件都会进行分段,而合并段表就是把相同段的数据进行合并,合并为一个a.out文件(也是elf格式的文件,意味着可以用readlf文件查看),其实就是我们的可执行程序(.exe)。
2·符号表的合并和重定位
我们知道现在有两个符号表,因为在test.c文件中只有ADD的声明,所以它的地址并没有什么实际意义。所以在进行符号表的合并和重定位的时候,我们选择了add.c文件所生成的Add符号。
(简单来说,进行了符号表的合并,选择了有效符号)。
那么在程序执行的时候,就可以通过符号表上的地址找对应的函数。
有可能现在感受不到这些过程有什么用,但实际上这些过程对于这个程序能否正常运行是十分重要的。正是因为有了符号表,才可以实现跨文件使用函数的操作。
宏观来说:
通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。
到此,翻译环境的编译和链接两个部分就介绍完了,想知道更多,请看《程序员的自我修养》。
总结
翻译环境:从源文件变到可执行程序,经过两步:编译、链接。
编译:分为三步,预处理、编译、汇编
预处理:
1·会产生一个没有头文件(都已经被展开了)
2·宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了)
3·没有特殊符号的输出文件,删掉注释
编译:编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
1·产生了汇编代码。
2·词法分析、语法分析、语义分析及优化后
3·产生相应的汇编代码文件(符号汇总)。
汇编:把汇编代码转换成机器指令(二进制指令),生成符号表(目标文件.o)。
链接:合并段表(可执行程序,其实就是生成了test.exe)、符号表的合并和重定位。
提示:
以上内容如果想复现,请学习VIM、gcc或者linux。
VIM 学习资料简明 VIM 练级攻略:https://coolshell.cn/articles/5426.html给程序员的 VIM 速查卡https://coolshell.cn/articles/5479.html
3·运行环境(执行环境)
在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
这个时候程序将使用一个运行时堆(栈stack),使用运行时堆栈存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。