天天看点

【1.2】系统漫游——“程序”被其他程序翻译成不同格式

        计算机语言按照层次划分为:机器语言(0/1)、汇编语言(arm、x86、thumb)和高级语言(c++、java、c)。计算机真正执行的“程序流”是机器语言,早期的程序员都是用机器语言编程,后来为方便记忆发明了汇编语言,但仍然需记住很多硬件相关的操作指令,而且代码规模有限,人们又发明了高级语言。高级语言的出现才真正把程序员从千变万化的硬件操作中解脱出来。综上:编程语言越来越“智能”,抽象层次越来越多,生产率会越来越高。

       我们的程序(高级语言)要想被计算机执行,就必须通过某种方式转化成机器语言(低级语言),然后打包成可执行目标程序(executable object program)(知道为什么windows的可执行文件都是.exe结尾了吧)。

以linux为例,输入代码:"gcc  -o  hello  hello.c "   (-o hello表示生成的可执行文件名称为hello)代码转化过程如下:

【1.2】系统漫游——“程序”被其他程序翻译成不同格式

整个过程分为4个阶段(预处理、编译、汇编、链接),以代码文件hello.c为例:

#include <stdio.h>
#define PI 3.14

int main()
{
    float test = PI;
    printf("hello world\n");
}
           

1、预处理:把hello.c(机器无关)源程序中以“#”开头的预处理项,进行包含替换,生成预处理文件"test.i"(机器无关),当然具体工作还不只这些。例如:#include “stdio.h”就是把“stdio.h”文件内容替换到该行位置;“#define PI 3.14”就是把该宏展开,并且去掉这里的宏定义。总体说来就是不对代码进行任何转化,只进行替换和包含工作。验证结果如下:

输入“gcc -E hello.c -o hello.i” //gcc处理源程序,只进行预处理生成hello.i文件

输入“cat hello.i”   //显示hello.i文件内容

【1.2】系统漫游——“程序”被其他程序翻译成不同格式

        可以看到,以“#”开头的文件包含指令和宏指令都已经消失,被相应内容替换掉了,其他程序代码没有任何改变,注意:hello.i 仍然是文本文件,与hello.c 基本无异。

注意:可以结合前面C语言宏定义的分析对比理解:玩儿转C语言:宏定义(1)和玩儿转C语言:宏定义(2)

2、编译:根据处理器指令集(x86、mips、51、arm),把经预处理的“test.i”(机器无关)文件转换成汇编文件“test.s”(机器相关)。不同型号的处理器架构不同,造成他们之间的汇编指令集互不兼容,那同样一段高级程序生成的汇编程序就不相同;反过来讲,对于同一种处理器,即便选择的高级语言种类不同(c++、.net),编译也会产生相同的汇编程序。这么做的好处是:把高级语言和汇编语言之间的连线切断,就可以制造出不同类别的“编译程序”,只修改“编译程序”,就可以为不同的高级语言创建合适的编译系统。分层就有这个好处:抽象级别高,容易修改(只要接口一致即可)。验证结果如下:

输入命令“gcc -S hello.i -o hello.s” //把hello.i文件转换成hello.s文件

输入命令“cat hello.s”

【1.2】系统漫游——“程序”被其他程序翻译成不同格式

        可以看到,把与机器无关的代码“hello.i”进行转化,生成了机器相关的汇编文件“hello.s”。注意:“hello.s”仍然是文本文件,可以用vi直接编辑。

3、汇编:把与机器相关的汇编文件“hello.s”(文本文件)转化成机器相关的可执行(内含机器指令,也叫可重定位的目标文件)文件“hello.o”(二进制文件)。二进制文件hello.o基本上就十分接近可执行的机器文件了,但是还不能正常运行,需要经过下一步的“链接操作”才能真正被执行。验证结果如下:

输入“gcc -c hello.s -o hello.o” //把汇编文件转换成目标文件

输入“cat hello.o”

【1.2】系统漫游——“程序”被其他程序翻译成不同格式

         可以看到,这次已经无法用文本编辑器来正常解析出该文件了,因为二进制文件的解析方式不再是单个字节为单位,而是根据自己的规则调整的。

4、链接:把“hello.o”(二进制文件)转化成“hello”(可执行的二进制文件)。前面讲到“hello.o”几乎接近可执行二进制文件了,但仍然缺点什么。看第一幅图,在链接时,加入了printf.o文件,两个文件输出了一个文件。这是因为我们调用了printf库函数,该函数并不是我们自己定义的,而是在系统库中。既然想用这个函数,肯定也需要把这个函数的相关二进制代码也添加进来才行,这里就是做了这个工作。系统自己已经编译好了目标文件,我们只需要进行“链接”,把调用printf函数的地方,强制跳转到printf.o文件中即可,工作完成再跳回来,这就是为什么hello.o目标文件也叫可重定位文件,因为hello.o文件中还有一些执行代码的跳转地址并没有最终敲定。这里只有一个hello.c文件,如果自己定义了多个源文件,文件之间相互调用的话,也会进行这项工作的。

        把printf封装成库是有好处的,因为它是二进制文件(见上图)打开时乱码,这样的话别人就不能看到它的源代码,也就无法模仿可以保护知识产权;也能防止随意修改,造成系统错误。

输入“./ hello” //执行该程序,因为linux的权限管理较严格,即便是执行本目录下的程序也需要加上“./”表示本目录,windows不是这样,权限管理相对较松

系统输出:hello,word

        总结:这4个步骤下来,才算完成源程序向可执行程序的转化。中间各个步骤的工作在这里只是大致描述了一下,并不代表他们只做这些事情,具体工作还得研究相关编译器才行。

        本文中多次提到了“文本文件”和“二进制文件”,关于它们的概念和异同,可以参考上一篇文章:【1.1】系统漫游——信息就是位+上下文