❝学习,说到底是一个「学」和「练」,以及学以致用的过程 ❞
大家好,我是「柒八九」。
今天,我们继续「计算机底层知识」的探索。我们来谈谈关于「运行环境&可执行文件」的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
❝
❞
好了,天不早了,干点正事哇。
「应用软件」能够运行,是需要依赖指定的运行环境的。而运行环境是「操作系统」和「计算机硬件」两者的综合。也就是说「操作系统」和「硬件」决定了程序的运行环境。
这就是说明了,从应用市场上下载软件是,一般都需要按照你「自身本地」的操作系统而选择对应的软件包。
❝
CPU
只能解释其「自身固有」的「机器语言」 ❞
不同的CPU
能解释的机器语言的种类也是不同的。例如,CPU
品牌分为Intel
和AMD
两种;,它们各自的机器语言是完全不同的。
「机器语言」的程序称为本地代码Native Code。程序员用C/Java
等编写的程序,在「编写阶段」仅仅是「文本文件」。
文本文件(排除文字编码问题)在「任何环境」下都能显示和编辑。我们称之为「源代码」
❝通过对源代码进行「编译」,就可以得到本地代码 ❞
同样机型的计算机,可安装的操作系统类型也会有多种选择。也就意味着,应用软件则必须根据不同的操作系统类型来专门开发。
CPU
的类型不同,所对应的机器语言也不同,同样的道理,「操作系统的类型不同,应用程序向操作系统传递指令的途径也是不同的」。
应用程序向操作系统传递指令的途径称为「API」(Application Programming Interface
)。Windows
及Unix
系列操作系统的API
,提供了任何应用程序都可以利用的「函数组合」。
因为不同操作系统的API
是有差异的,因此,将同样的应用程序移植到其他操作系统时,就必须重写应用中利用到API
的部分。
在同类型操作系统下,不管硬件如何,API
基本上没有差别。因此,针对某特定操作系统的API
所编写的程序,在任何硬件上都可以运行。
当然,由于CPU
种类不同,机器语言也不相同,因此本地代码也不同。这种情况下,就需要利用能够生成各CPU
专用的本地代码的「编译器」,来对源代码进行重新编译。
❝程序(
本地代码
)的运行环境是由操作系统和硬件来决定的 ❞
Java
能够提供「不依赖于特定硬件及操作系统」的程序运行环境。
针对Java
有两个层面。一是作为「编程语言」,另一个是作为「程序运行环境」。
同其他编程语言相同,Java
也是将Java
语法记述的「源代码」编译后运行。不过,编译后生成的「并不是特定CPU
使用的本地代码」,而是名为「字节代码」的程序。
字节代码的运行环境被称为「Java虚拟机」(Java Virtual Machine
)。
❝
Java
虚拟机是一边把Java
字节代码逐一准换成本地代码,一边运行的。 ❞
「编译器」将程序员编写的「源代码」(xx.java
)转换成「字节代码」(xx.class
)。而Java
虚拟机(java.exe
)则会「把字节代码变成本地CPU
适用的本地代码」,然后由本地CPU
负责实际的处理。
从操作系统方面来看,Java
虚拟机是一个应用,而从Java
应用来看,Java
虚拟机就是运行环境。
程序的运行环境中,存在着名为「BIOS」(Basic Input/Output System
)的系统。
❝
BIOS
存储在ROM
中,是预先「内置」在计算机主机内部的程序。 ❞
BIOS
除了键盘、磁盘、显卡等基本控制程序外,还有启动「引用程序」的功能。
❝「引导程序」是存储在「启动驱动器」起始区域的小程序。 ❞
开机后,BIOS
会确认「硬件是否正常运行」,没有问题的话就会启动「引导程序」。引导程序的功能是把在硬盘等记录的OS
加载到内存中运行。
源代码完成后,就可以编译生成「可执行文件」了。负责实现该功能的是「编译器」。
假设我们通过「高级语言」(语言类型不限),编写一个把123
和456
的平均值289.5
显示出来。
function Main(){
let ave;
ave = (123 + 456)/2;
alert(ave);
}
类似上述的代码,用「某种」编程语言编写的程序被称为「源代码」,保存源代码的文件被称为「源文件」。用JS
编写的源文件的扩展名通常是.js
。
上述的源代码是无法直接运行的。这是因为,CPU
能直接解析并运行的不是源代码而是「本地代码」的程序。作为计算机大脑的CPU
,也「只能解释已经准换成本地代码的程序内容」。
本地这个术语有母语
的意思。「对CPU
来说,母语就是机器语言,而转换成机器语言的程序就是本地代码」。用任何编程语言编写的源代码,最后都要翻译成本地代码,否则CPU
就不能理解。也就是说,「即使是用不同编程语言编写的代码,转换成本地代码后,也都变成用同一种语言(机器语言
)来表示」。
Windows
中EXE
文件的程序内容,使用的就是本地代码。本地代码的内容是人类无法理解的,也正是因为如此,才有了用人类容易理解的C
语言等编程语言来编写源代码,然后再将源代码转换成本地代码。
我们可以把EXE
文件的内容Dump
一下。Dump
是指把文件的内容,每个字节用2位十六进制数来表示的方式。本地代码的内容就是各种数值的罗列,而这些数值就是本地代码的真面目。每一个「数值」都表示某一个命令或数据。
本地代码Dump
之后的样子
能够把C/Java
等高级编程语言编写的源代码准换成本地代码的程序称为「编译器」。每个编写源代码的编程语言都需要其「专用」的编译器。将C
语言编写的源代码转换成本地代码的编译器称为C编译器
.
❝编译器首先读入代码的内容,然后再把源代码转换成本地代码。 ❞
编译器中就好像有一个源代码同本地的对应表。但实际上,仅仅靠对应表是无法生成本地代码的。读入的源代码还要经过「语法解析」、「句法解析」、「语义解析」等才能生成本地代码。
根据CPU
类型不同,本地代码的类型也不同。因此,编译器不仅和编程语言的种类有关,和CPU
的类型也是相关的。「同样的源代码就可以翻译成适合不同CPU
的本地代码」。
因为「编译器本身也是程序的一种,所以也需要运行环境」。例如,有Windows
用的C编译器
、Linux
用的C编译器
等。此外,还有一种「交叉编译器」,它生成的是和运行环境中的CPU
不同的CPU
所使用的本地代码。
编译器转换源代码后,就会生成本地代码。不过,本地文件是无法直接运行的。为了得到可以运行的EXE
文件,编译之后还需要进行「链接」操作。
我们拿C语言
在Windows
环境下举例。
编译后生成的不是EXE
文件,而是扩展名为.obj
的「目标文件」。xx.c
编译后,就生成了xx.obj
目标文件。虽然目标文件的内容是本地代码,但却无法直接运行。其原因就是「当前程序还处于未完成状态」。
把多个目标文件结合,生成1个EXE
文件的处理就是「链接」,运行链接的程序就是链接器Linkage Editor。
Windows
以「函数的形式」为应用提供了各种功能。这些形式的函数称为「API」(Application Programming Interface
,应用程序接口)。
Windows
中,API
的目标文件,并不是存储在通常的库文件中,而是存储在名为「DLL」(Dynamic Link Library
)文件的特殊库文件中。「DLL文件是程序运行时动态结合的文件」。
与此相反,存储着目标文件的实体,并直接和EXE
文件结合的库文件形式称为「静态链接库」
❝
EXE
文件是作为「单独的文件」存储在硬盘中的。通过资源管理器找到并双击EXE
文件,就会把EXE
文件的内容加载到内存中运行。 ❞
这里有一个疑问?本地代码在对程序中记述的变量进行读写时,是参照数据存储的内存地址来运行命令的。在调用函数时,程序的处理流程就会跳转到存储着函数处理内容的内存地址上。EXE
文件作为本地代码的程序,并没有指定变量及函数的「实际内存地址」。在类似于Windows
操作系统这样的可以加载多个可执行程序的运行环境中,每次运行时,程序内的变量及函数被分配到的内存地址都是不同的。
那么,在EXE
文件中,变量和函数的内存地址的值,是如何来表示的呢?
那就是EXE
文件中给变量和函数分配了「虚拟的内存地址」。在程序运行时,「虚拟的内存地址会转换成实际的内存地址」。
❝链接器会在
EXE
文件的开头,追加转换内存地址所需的必要信息。这个信息称为「再配置信息」。 ❞
EXE
文件的再配置信息,就成了变量和函数的「相对地址」。相对地址表示的是相对于「基点地址」的偏移量,也就是相对距离。
链接后的EXE文件的构造
EXE
文件的内容分为「再配置信息」、「变量组」和「函数组」。不过,当程序加载到内存后,除此之外还会额外生成两个组,那就是「栈」和「堆」。
局部变量
),以及函数调用时所用的参数的内存区域。加载到内存的程序由4部分构成
堆和栈的相似之处在于,他们的内存空间都是在程序运行时得到分配。
「编译器」是在「运行前」对所有源代码进行解释处理的。而「解释器」则是在「运行时」对源代码的内容一行一行的进行解释处理
将整个程序分为多个源代码来编写,然后分别进行编译,最后链接成一个EXE
文件。
DLL
文件中的函数可以被「多个程序共用」。因此,「借助该功能可以节约内存和磁盘」。此外,在对函数的内容进行修正时,还不需要重新链接使用这个函数的程序。
「分享是一种态度」。
参考资料:《程序是怎样跑起来的》
「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」