编译型语言和解释型语言的基本原理
汇编语言 - Assembly Language
先看Wiki关于编译器的定义。
In computing, a compiler is a computer program that translates computer code written in one programming language (the source language) into another language (the target language). The name “compiler” is primarily used for programs that translate source code from a high-level programming language to a lower level language (e.g. assembly language, object code, or machine code) to create an executable program.
编译器(compiler)是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。换句话说,编译器就是一个翻译家,负责将一种语言转换为另一种语言,以便某种计算机系统能够理解并执行。
对于计算机语言来说,无论是低级语言,还是高级语言,都是基于汇编语言(Assembly Language)来构建的。
汇编语言是任何一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。在不同的设备中,汇编语言对应着不同的机器语言指令集。一种汇编语言专用于某种计算机系统结构,而不像许多高级语言,可以在不同系统平台之间移植。
目前市场上主流芯片的机器语言指令集包括:X86、ARM、RISC-V、MIPS等四种。
而HP的PA-RISC、IBM的PowerPC、DEC的Alpha、SUN的SPARC等指令集已经被淘汰。
CPU机器语言的指令都是二进制的,称为操作码(opcode),比如加法指令就是00000011。对于人类来说,二进制程序是不可读的,根本看不出来机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞生了汇编语言。
汇编语言是二进制指令的文本形式,与指令是一一对应的关系。比如,加法指令00000011写成汇编语言就是ADD。只要还原成二进制,汇编语言就可以被 CPU 直接执行,所以它是最底层的低级语言。
汇编语言使用助记符(Mnemonics)来代替和表示特定低级机器语言的操作。特定的汇编目标指令集可能会包括特定的操作数。许多汇编程序可以识别代表地址和常量的标签(Label)和符号(Symbols),这样就可以用字符来代表操作数而无需采取写死的方式。普遍地说,每一种特定的汇编语言和其特定的机器语言指令集是一一对应的。
许多汇编程序为程序开发、汇编控制、辅助调试提供了额外的支持机制。有的汇编语言编写工具经常会提供宏,它们也被称为宏汇编器。
编译型语言 - Compiled Lanuage
编译语言是一种高级编程语言类型,首先通过编译器(Compiler)将代码编译为机器代码,然后再加以运行。
在此过程中,编译器一次将整个程序转换为机器代码。如果有任何语法或语义错误,编译器将给出错误警告并退出,如果不纠正这些错误,就不可能执行程序。
C、C++、Object C都是典型的编译语言为例,而gcc、Clang等就是其编译器。
GCC(GNU Compiler Collection)是一套完整的开源编译器软件,包括编译器前端和后端,不仅支持C和C++,还可以支持Java、Ada和Fortran等多种语言。
Apple原来也采用GCC,但由于认为对其支持不力,而自行开发了Clang,并宣称编译速度有显著提高。
注意,Clang只是一个编译器前端,通常使用LLVM(Low Level Virtual Machine)作为编译器后端,LLVM利用虚拟技术使编译时期、链接时期、运行时期和闲置时期达到最优化,广泛支持多种语言的编译和优化。如果你准备开发自己的编译器,这是一个非常重要的基础组件。
以cpp语言开发为例,使用gcc生成可执行文件需要经历四个相互关联的步骤∶
- 预处理(也称预编译,Preprocessing):调用
cpp进行预处理,对源代码文件(后缀一般为.cpp)中的文件包含(include)、预编译语句(如宏定义define等)进行分析 - 编译(Compilation):调用
cc1进行编译,根据输入文件生成以.o为后缀的目标文件 - 汇编(Assembly):调用
as进行工作,一般来讲,.S为后缀的汇编语言源代码文件和汇编、.s为后缀的汇编语言文件,经过预编译和汇编之后都生成以.o为后缀的目标文件 - 连接(Linking):在连接阶段,所有的目标文件被安排在可执行程序中的恰当的位置,同时,该程序所调用到的库函数也从各自所在的档案库中连到合适的地方
在Linux中,cpp代码编译中常见的文件名后缀包括:
.i是对源代码进行预处理后的中间文件,主要工作包括:处理宏定义、预编译指令、删除注释、添加行号和文件标识等.o是编译中间目标文件,相当于win上的.obj.a是静态库文件,由多个.o文件链接得到,用于静态链接.so是共享库文件,用于动态链接(shared object),相当于win上.dll
解释型语言 - Interpreted language
解释语言(Interpreted language)是一种编程语言类型,会将代码一句一句直接运行,需要哪些源代码就转换哪些源代码,不会生成可执行程序,这种编程语言称为解释语言(Interpreted language),使用的转换工具称为解释器。
解释器就像一位“中间人”,一边解释一边执行,因此依赖于解释器的程序运行速度比较缓慢。
解释器的好处是它不需要重新编译整个程序,从而减轻了每次程序更新后编译的负担。
常见的解释语言的示例是Basic、PHP,Ruby,Python和JavaScript。
以Basic为例,其代码和运行结果示例:
1 | 10 INPUT "What is your name: ", U$ |
关于类型安全(Type Safety)的讨论
尼古拉斯·沃斯的一个著名的公式:程序 = 数据结构 + 算法。
其中,数据是可以被记录或存储的信息,数据结构则说的是一种组织或存储数据的方式,而算法则是通过对输入的处理从而输出能解决问题的有效方法。
也就是说,任何一种编程语言,都需要为变量、常量、函数(方法)提供并定义数据类型(Type),本质就是提供一块内存的存储区域。
类型错误(type error)是错误或不期望的程序行为,由不同数据类型的差别所引起,如把整型(int)当作了浮点型(float),由此可能引发数据越界而造成安全漏洞。
以C语言的指针类型为例,由于其类型定义不严格,非常容易引发数组溢出、堆栈溢出、缓冲区溢出、野指针(void *p)等安全问题,最终将指向内存的某个不可知区域造成系统崩溃或安全漏洞。
为了解决类型安全问题,从不同的角度产生了多种方案,以下是几个较为常见的维度:
动态语言 Vs 静态语言
动态语言(Dynamic programming language):是一类在运行时可以改变对象结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
主要动态语言:Object-C、JavaScript、PHP、Python、Erlang。静态语言(Statically programming language):与动态语言相对应的,运行时对象结构不可变的语言就是静态语言。如Java、C、C++、C#。
动态语言的常见特征有:eval函数、对象运行时修改属性或方法、反射和宏。
大部分动态语言都使用动态类型,但也有些不是。
强类型语言 Vs 弱类型语言
强类型语言(Strong typing language):即没有强制类型转化前,不允许两种不同类型的变量相互操作。
强类型语言包括:Java、.net、Python、C++等语言。其中Python是动态语言,是强类型定义语言,是类型安全的语言,Java是静态语言,是强类型定义语言,也是类型安全的语言。弱类型语言(Weak typing language):即变量定义时可以忽略数据类型,且该变量可以根据环境变化自动进行隐式类型转换,比如直接将整型变量与字符变量相加。
弱类型语言包括:VB,PHP,JavaScript等语言。其中VBScript是动态语言,是一种类型不安全的原因。
强类型语言和弱类型原因其判断的根本是:是否会隐形进行语言类型转变。
使用弱类型语言可节省很多代码量,有更高的开发效率。而对于构建大型项目,使用强类型语言可能会比使用弱类型更加规范可靠。
动态类型语言 Vs 静态类型语言
类型安全可以静态方式实施,及早在编译阶段(Compiling)就捕捉到潜藏的错误;或者以动态方式,推迟到运行时期(Runtime)检测即将发生的错误。
- 动态类型语言(Dynamically Typed Language):是指在运行期间才去做数据类型检查的语言。
在用动态语言编程时,不用给变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来。Python和Ruby就是一种典型的动态类型语言;
1 | a = 1 |
- 静态类型语言(Statically Typed Language):与动态类型语言刚好相反,它的数据类型检查发生在编译阶段,也就是说在写程序时要声明变量的数据类型。
C/C++、C#、Java都是静态类型语言的典型代表,下面以C语言为例说明其原理:
1 |
|
小结
1. 编译器和解释器的核心差异在于是否编译和执行过程是否是同时进行
编译器所干的事,将一门语言 X 编译为另一门语言 Y (可以是语言 X、高级语言、低级语言等),整个编译过程涉及词法分析、语法分析、语义分析。该过程往往由程序员在编写程序时完成。
而解释器则直接将语言 X 编写的程序在目标机器上运行,并输出解释后运行的结果。
| 编译器 - Compiler | 解释器 - Interpreter | |
|---|---|---|
| 典型产品 | gcc、VC | Basic,bash |
| 优化目标 | 更快的运行速度 | 更少的内存占用 |
| 外部存储 | 必须的 | 可选的 |
| 错误检查 | 运行前的编译阶段 | 程序运行过程中 |
| 源代码 | 直接运行二进制机器码 | 运行环节必须依赖源代码 |
| 运行环境 | 操作系统提供的Runtime Library | 必须依赖于解释器 |
| 跨平台支持 | 维护不同版本的源代码,并需要编译环节 | 通过解释器(或虚拟机)适配不同平台,源代码无需修改 |
| 运行效率 | 安装(编译)速度慢,运行速度快 | 启动速度快,运行速度慢 |
2. 随着虚拟机技术的发展,越来越多的现代语言同时采用了编译技术和解释技术
需要注意的是,随着计算机技术的发展,编译器的目标代码已经不仅是机器代码(Machine code),还可能是某种虚拟机(Viutual Machine)所支持的字节码(Bytecode),编译器和解释器正在不断地融合发展。
以Java为例,就是同时使用了编译技术和解释技术,JavaScript也从客户端浏览器运行的脚本语言发展为高效复杂的V8引擎驱动。在智能终端方面,Android先后采用了Dalvit和ART等不同类型的编译器,华为更是基于方舟编译器推出了Harmoney操作系统。
后续我们还将深入探讨编译技术的最新发展。
3. 尽管存在争议,动态语言在开发效率的巨大优势使之成为现代高级语言的必然趋势
我们讨论了许多编程语言的类型安全问题,以下是一个粗糙的示意图,仅供参考。
可以肯定的是,动态语言对于Runtime环境的依赖越来越高,未来跨CPU平台的需求重要性也将越来越重要。
附录一:运行时库 - Runtime library
运行时库(Runtime library,又称运行时库),在计算机程序设计领域中,是指编程语言程序运行时(执行)所需要的一种特殊的计算机程序库,编译器会调用运行时库至已编译的可执行二进制代码中。这种库一般包括基本的输入输出或是内存管理等支持。它是一群支持正在运行程序的函数,与操作系统合作提供诸如数学运算、输入输出等功能,让程序写作者不需要“重新发明轮子”,并善用操作系统提供的功能。
运行时库由编译器决定,以面向编程语言,提供其最基本的执行时需要。比如Visual Basic需要复杂的运行时库支持而C的运行时库则相对简单。当然这还是由编译器厂商决定的。运行时库中的函数可能对程序员透明,也可能不透明。这也是由编译器厂商对语言执行环境的需求而决定的。
早期的运行期库(例如Fortran)提供了数学运算能力。其他语言增加了诸如垃圾回收的先进功能,用于支持对象数据结构。许多近代语言设计了更大的运行环境并添加更多功能。很多面向对象语言也包含了分派器与类别读取器。Java虚拟机(JVM)便是此类的典型运行环境:它也在运行期直译或编译具可携性的二进制Java程序。而.NET架构也是另外一个运行时库的实例。
以C语言为例,其运行时库称为C Run-Time library,简称为CRT。
由于C语言其是所谓的小内核语言,就其语言本身来说很小(不多的关键字,程序流程控制,数据类型等)。所以,C语言内核开发出来之后,Dennis Ritchie 和 Brian Kernighan 就用C本身重写了 90% 以上的 UNIX 系统函数,并且把其中最常用的部分独立出来,形成头文件和对应的库文件,C Run-Time library(简称CRT)就是这样形成的。
C run-time library里面含有初始化代码,还有错误处理代码(例如divide by zero处理)。你写的程序可以没有math库,程序照样运行,只是不能处理复杂的数学运算,不过如果没有了C run-time库,main()就不会被调用,exit()也不能被响应。因为C run-time library包含了C程序运行的最基本和最常用的函数。
CRT和操作系统密切相关,从某种程度上来讲是,CRT是语言程序和不同操作系统平台之间的抽象层(接口是统一的标准,实现由各个平台自己实现)。
Linux:glibc(GNU C Library)
在Linux平台上最广泛使用的C运行库是glibc,其中包括C标准库的实现,也包括所有系统函数,并在其基础上增加了线程操作等扩展。
几乎所有C程序都要调用glibc的库函数,基本上除了Linux自身的API以外,所有的库都依赖于glibc,所以glibc是Linux平台C程序运行的基础。
glibc的动态库文件位于/lib/libc.so.6,静态库文件位于/usr/lib/libc.a
Windows:MSVCRT(Microsoft Visual C Run-time)
在Windows环境下,Visual C提供的运行时库又分为动态运行时库和静态运行时库(注意:动态链接库或静态链接库与运行时库的分类角度不同,不得相提并论)。:
- 动态运行时库:主要是DLL库文件
MSVCRT.DLL(or MSVCRTD.DLL for debug build),
对应的Import library文件是MSVCRT.LIB(MSVCRTD.LIB for debug build) - 静态运行时库:对应的主要文件是:
LIBC.LIB(Single thread static library, retail version)LIBCMT.LIB(Multithread static library, retail version)
附录二:静态链接 Vs 动态链接
静态库是在链接阶段将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。
在Linux环境下,静态库文件后缀是.a,Windows是.lib,其主要特点是:
- 静态库对函数库的链接是放在编译期(Compile)完成的
- 程序在运行时与函数库再无瓜葛,移植方便
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
在Linux环境下,动态库文件后缀是.so,Windows是.dll,其主要特点是:
- 动态库把对一些库函数的链接载入推迟到程序运行时(Runtime)的时期
- 可以实现进程之间的资源共享,因此动态库也称为共享库
- 将一些程序升级变得简单
- 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显式调用)
附录三:脚本语言 - Scripting language
脚本语言(Scripting language)是让程序员快速完成程序的编写工作而创建的计算机编程语言,具有简单、易学、易用的特性。早期的脚本语言经常被称为批处理语言或作业控制语言。
脚本语言与解释型语言并不是一个范畴的概念,但脚本代码通常是解释运行而非编译,开发脚本的目的就是快速连接多个软件组件。
一些众所周知的例子:
- Lisp,最早的脚本语言,是一系列用于特定应用程序的通用语言和扩展语言,例如Emacs Lisp,用于Emacs编辑器
- Bash,一种用于Unix和类 Unix 操作系统和环境的解释性脚本语言。
- PowerShell,一种用于Microsoft Windows操作系统的脚本语言。
- sed和AWK,两种主要用于类 Unix 环境的文本处理语言。
- Perl,一种文本处理语言,后来发展成为一种通用语言,也用作各种应用程序的扩展语言。
- Python是一种通用脚本语言,也用作扩展语言。
- JavaScript(后来的:ECMAScript),最初是一种非常小的、高度特定于领域的语言,仅限于在 Web 浏览器中运行以动态修改正在显示的网页,后来发展成为一种广泛可移植的通用编程语言。
- Visual Basic for Applications,一种专用于Microsoft Office应用程序的扩展语言。
- Lua,一种设计用于一般应用程序的扩展语言的语言,并被许多不同的应用程序使用。