什么是编译器,它是如何工作的?

   在前一章节我们简单的谈论了编译器和解释的区别,因为说的比较简单,所以我打算用这一章的篇幅去谈一下编译器,然后再谈一下解释器。

    我个人认为,作为一个程序员我们需要去了解编译器的一个工作流程,这样对我们代码的组织会有清晰的一个认知,增加我们对程序本身的理解。很多人可能并不关心编译,我尽量用比较浅显明白的语言将程序编译的流程说清楚,如果非必要,我不太会插入太多的图表去解释,因为我觉得文字的描述才是最精确的。

    现在我们都用IDE去写代码。然后编译程序,最后成个一个可执行的文件(在windows系统中,我们看到的是.exe结尾的文件,在linux系统中,可执行文件没有后缀名)或者一个war包,jar包(注意这里的war包或者jar包也可以理解为编译,将java的源码编译成了字节码,在前一章我们讲过,字节码是与平台无关的,所以你的jar包可以部署在windows上,也可以部署在linux,mac上,无需再次编译)。

        如果早期写过c或者c++的同学,其实编译的时候,生成后缀名为lib或者dll(linux中的动态链接库为.so后缀结尾)的文件,这两个文件都可以称之为库文件,库文件一般都是被别的代码调用的文件。lib我们称之为静态库,dll为动态链接库,这两个文件的不同之处是加载的方式。静态库lib顾名思义,就是在编译成可执行文件的时候就需要将这个库文件要嵌入到可执行文件中,所以可执行文件包含了静态库的内容;动态链接库相反,在编译的时候并不需要加载到可执行文件中去,只有在程序运行的时候根据需要再从外部加载到内存中。那么问题来了,为什么需要这样做?

    先说静态库,它的特点就是1,比较稳定,因为可执行文件本身就包含了静态库内容,不存在兼容性的问题(版本不对的问题);2,加载的时间很短,因为动态库文件在程序启动的时候就加入了内存;3,无法被多个程序共享,如果多个程序都需要这个静态库,那么这个静态库对每个程序来讲,都需要一份独立的拷贝,这样会占用内存。4,如果你静态库需要更新,需要重新编译所有使用该库的代码,所以如果是可执行文件,就需要重新编译生成可执行文件。

   动态链接库,他的特点是1,可能存在兼容性问题,因为程序运行的时候加载动态库,这时候可能动态库对应的版本和程序的版本不一致;2,加载时间长,因为程序运行的时候需要从磁盘加载入内存,所以时间会比较长;3,可以被多个程序共享,如果这个动态链接库已经在内存中了,那别的程序需要的时候无需再次加载,这样节省了内存;4,动态链接库需要更新,无需编译调用该动态链接库的代码,只需要替换动态链接库的新版本即可。

   从上面的描述来看,静态库和动态库各有优缺点,所以到底是使用动态链接库还是静态库需要根据情况而定,一般是如果这个库只能被一个程序使用,那么可以直接考虑用静态库,如果是被多个程序使用,那么可以考虑用动态链接库。

    对于Java来讲,有没有动态链接库概念呢?答案是肯定的,在java虚拟机里面,有一个叫本地栈的概念,就是java调用本地函数用到的(这里的本地函数,基本上都C的函数),这个本地函数就可以是动态链接库。关于java虚拟机,这又是一个庞大的话题,这个是一两句话说不清楚的,如果大家有需要,我们可以单开出一个话题来讨论java虚拟机。

    Python和java是很相似的,也可以调用动态链接库。

    谈了这么多,我们回到编译的这概念,从传统角度来讲,所谓编译就是将代码翻译成机器可以识别的指令。因为Java和Python此类解释性语言,又让编译的这个概念有点模糊。Java或者python中存不存编译呢?肯是存在的,其实我们探究编译的过程,代码不是直接从一个文本可读性的形态直接转换为机器指令的,中间经历了很多过程。接下来,我们来谈编译的过程,然后对比下java编译器所做的工作。

    以C编译器变为例,典型编译过程分为:预处理,编译,汇编,链接。为什么要以C编译器作为讲解呢,因为C编译器是一个很典型的编译器,包含了上述的四个过程,在这个讲解的过程中我们会对比java来说明。

    再顺便说一下,我觉得这个是很重要但是被很多人忽略的东西,那就是C语言。C语言在我上学的时候,是大学的必修课,如果很多人对C不了解或者学了已经忘记了,我强烈建议如果有时间可以学习下C语言,虽然可能在后面的工作中你用不到,但是C相对于java是更接近于底层的东西,如果你有了C的知识,相信对你计算机的基础知识的学习是很有帮助的,而且我觉得更有益于你去更深入的了解java体系,这里强调的是java体系,而不是java语言本身。如果对java体系感兴趣,可以给我留言或者私信,我可以找时间整理一下,单独讲解。接下来我们继续谈编译的过程。

    在额外说明一下,如果你经常使用linux,有可能会遇到一些工具需要从源码安装。你会将源码下载下来,用make&install这样的命令来安装软件,这个时候就会调用gcc编译器,然后就会发生从预处理->编译->汇编->链接这个过程。

    预处理:对与C语言来讲,这就是处理代码中的预处理指令。主要包括展开宏定义,条件编译指令以及头文件包含指令。JAVA本身没有预处理的过程,但是我们讲解的过程中,会对比一下预处理指令对应的java实现。

    什么是宏定义?如下面代码所示,这就是定义了一个宏MAX,然后这个MAX的值为65535,然后可以在代码中引用这个宏MAX,我们定义了一个变量a等于MAX,在预处理的时候,预处理器会将这个MAX替换为65535,这叫做展开宏定义。

#define MAX 65535

int a = MAX

    这个宏定义在java中可以理解为定义了一个常量,当然这个常量和宏定义又有区别,宏定义可以认为只是一个无数据类型的替代,也就是说MAX代表65535,但是这个65535只是表示65535这几个字符,至于是int或者是long,这个取决的出现的具体语境,但是常量是有数据类型的,这在定义的时候就已经确定了。

    接下来谈条件编译,典型的是#ifdef,我们看下面的例子,这段代码的意思是,如果定义DEBUG这个宏,那么编译 printf(“Debug is on.\n”);这句代码,否则编译printf(“Debug is off.\n”);这句代码。所以我们定义了DEBUG这个宏,那么在编译的时候就会编译printf(“Debug is on.\n”);这句代码。

#define DEBUG

#ifdef DEBUG
    printf("Debug is on.\n");
#else
    printf("Debug is off.\n");
#endif

        在java中可以实现和预编译指令相同效果的是if+常量(常量对应宏定义)来实现,只不过这个稍稍不同的是,编译器是看不到#ifdef这个指令的,因为预处理器处理过后,给编译器的只剩下printf(“Debug is on.\n”);这个语句,但是我们在java中用if+常量实现这个相似的功能的时候,java编译器是可以看到if的完整语句的,而不是只看到printf(“Debug is on.\n”);。

      头文件包含,头文件以.h结尾,里面一般定义了一些宏,还有一些全局变量的定义,以及函数和类的声明,这里不做过多的解释。这里做的预处理就是将#incude指定的头文件里面的内容拷贝到当前#include指令的位置,替换掉#include指令。

     至此,预处理就说完了,这个是C/C++编译器的特色,在java或者python是没有预处理这个过程的。

编译:对于C/C++来讲,这个过程是将第一步预处理之后的代码,编译成汇编代码。首先编译器会做的工作就是词法分析,语法分析,语义分析,这样确保你代码书写的正确性,如果这一步有错误,则会终止,如果检查没有问题,则会按照要求编译成汇编代码。

     对于java来讲,词法分析,语法分析,语义分析,这几个过程也都是有的,只是java的编译过程生成的结果是CLASS文件,这就是字节码。在之前也讲过,字节码是实现平台无关和语言无关的关键所在。不论是java或者是其他语言例如ruby,groovy等,通过对应编译器都可以生成字节码,这些字节码都可以在java虚拟机上运行。

    我把java从代码生成字节码的过程称之为编译,这个过程对应C的编译这个过程,他们有相同的处理,就是词法分义,语法分析,语义分析,只是最后生成物不太一样。 C的编译器一般生成的是汇编代码,而java的编译器生成的是字节码。java编译生成字节码后,我们可能会打出一个jar包或者是一个war包,后续就交给虚拟机解释执行了。但是对于C编译器来讲,还有后续的步骤需要进行,汇编语言是很接近机器语言了,但是它仍是可读的文本语言,和java代码或者C代码一样。但是java的字节码已经是二进制了,里面按照固定的格式和语法进行排列,这个字节码可以被虚拟机识别和执行,具体细节不在本次讨论范围了。

汇编:汇编的过程就是汇编器将汇编语言转化为二进制机器指令,生成目标文件,一般以.o或者.obj结尾。注意这时候成的也是二进制的文件,但是和java字节码还是不一样的。这里的二进制文件直接是机器可以识别的指令,这个是和机器相关的,所以你在windows上生成的这种二进制机器指令在linux或者mac上是无法运行的。所以我们经常在pc上安装一些软件,我们经常需要指明是windows上还是linux,就是这个原因。java的字节码就没有这个问题,因为字节码是和机器无关的,只要你安装了java虚拟机,同样的字节码不论在windows或者linux上都是可以运行的。这个过程java是没有的。

链接:这个过程是将我们汇编生成的目标文件和库文件合并为可执行文件。具体的过程又分为:

    1,符号解析:解析目标文件中的符号引用,例如你在这个文件中调用了另一个文件中的函数,那么链接器就是保证你这个调用在别的文件中能找到,连接器会便利所有的目标文件和库,保证这些符号引用都有定义。

    2,地址分配:说到地址分配可能觉得比较抽象,我们都知道,进程是资源分配的单位,这个进程中会包含数据段,代码段,我们将这些目标文件链接在一起的时候,就会将这些文件的数据段,代码段重新整理合并,也就是数据段和数据段放一起,代码段和代码段放一起,当然这只是很粗略的说明,然后整理好之后,就开始为这个数据段和代码段分配地址。可能有的人会疑问,这没有开始执行,如何就能分配地址呢?这里面就涉及到操作系统的一些知识,操作系统有一个概念就虚拟内存,当一个进程运行的时候,操作系统就会这个进程分配了一块内存。从进程的视角开看,他看到的是虚拟内存,虚拟内存是从地址0开始的,但是实际的内存的地址可能不是从0开始的。这就是为啥进程不能访问别的进程的资源,这是操作系统分配的机制,每个进程看到自己的内存地址都是从零开始的。这个对每个进程来讲,它独享了内存。所以这时候连接器可以给所有代码和数据分配地址,将来运行的时候,因为进程看到的是虚拟内存,所以地址不会改变。

    3,重定位:既然所有的代码和数据有跟配了地址,那么之前的符号引用的位置,就需要重定向地址。比如我们在一个函数中去调用另一个函数,那么这个函数的地址我们已经知道了,这时候只需要将这个函数的引用地址修改即可。

    4,生成可执行文件:如果前面的三步都没有问题,就会生成可执行文件。

    对于链接,之前我们讲过动态链接和静态链接的区别,这里不再赘述。 对于java来讲,在生成字节码的时候,没有这种意义上的链接。只有在交给虚拟机运行的时候,涉及到本地方法调用的时候,会产生类似动态链接的行为。

编译器我们就介绍到这里了,希望大家看完之后对编译的过程有个比较清晰的了解。由于这个编译器的篇幅太长了,解释器的说明会新开一章。

   

   

   

   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白快快跑哦

您的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值