学习,说到底是一个学和练,以及学以致用的过程
大家好,我是柒八九。
今天,我们继续计算机底层知识的探索。我们来谈谈关于运行环境&可执行文件的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
你能所学到的知识点
- 运行环境 = 操作系统 + 硬件 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 不同操作系统的API不同 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- Java 虚拟机 推荐阅读指数 ⭐️⭐️⭐️
- BIOS和引导 推荐阅读指数 ⭐️⭐️⭐️
- 计算机只能运行本地代码 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 本地代码的内容 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 编译器负责转换源代码 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- DLL文件及导入库 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 可执行文件运行时的必要条件 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 程序加载时会生成栈和堆 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
好了,天不早了,干点正事哇。
运行环境 = 操作系统 + 硬件
应用软件能够运行,是需要依赖指定的运行环境的。而运行环境是操作系统和计算机硬件两者的综合。也就是说操作系统和硬件决定了程序的运行环境。
这就是说明了,从应用市场上下载软件是,一般都需要按照你自身本地的操作系统而选择对应的软件包。
CPU
只能解释其自身固有的机器语言
不同的CPU
能解释的机器语言的种类也是不同的。例如,CPU
品牌分为Intel
和AMD
两种;,它们各自的机器语言是完全不同的。
机器语言的程序称为{本地代码| Native Code}。 程序员用C/Java
等编写的程序,在编写阶段仅仅是文本文件。
文本文件(排除文字编码问题)在任何环境下都能显示和编辑。 我们称之为源代码
通过对源代码进行编译,就可以得到本地代码
不同操作系统的API不同
同样机型的计算机,可安装的操作系统类型也会有多种选择。也就意味着,应用软件则必须根据不同的操作系统类型来专门开发。
CPU
的类型不同,所对应的机器语言也不同,同样的道理,操作系统的类型不同,应用程序向操作系统传递指令的途径也是不同的。
应用程序向操作系统传递指令的途径称为API(Application Programming Interface
)。 Windows
及Unix
系列操作系统的API
,提供了任何应用程序都可以利用的函数组合。
因为不同操作系统的API
是有差异的,因此,将同样的应用程序移植到其他操作系统时,就必须重写应用中利用到API
的部分。
在同类型操作系统下,不管硬件如何,API
基本上没有差别。因此,针对某特定操作系统的API
所编写的程序,在任何硬件上都可以运行。
当然,由于CPU
种类不同,机器语言也不相同,因此本地代码也不同。这种情况下,就需要利用能够生成各CPU
专用的本地代码的编译器,来对源代码进行重新编译。
程序(
本地代码
)的运行环境是由操作系统和硬件来决定的
Java 虚拟机
Java
能够提供不依赖于特定硬件及操作系统的程序运行环境。
针对Java
有两个层面。一是作为编程语言,另一个是作为程序运行环境。
同其他编程语言相同,Java
也是将Java
语法记述的源代码编译后运行。不过,编译后生成的并不是特定CPU
使用的本地代码,而是名为字节代码的程序。
字节代码的运行环境被称为Java虚拟机(Java Virtual Machine
)。
Java
虚拟机是一边把Java
字节代码逐一准换成本地代码,一边运行的。
编译器将程序员编写的源代码(xx.java
)转换成字节代码(xx.class
)。而Java
虚拟机(java.exe
)则会把字节代码变成本地CPU
适用的本地代码,然后由本地CPU
负责实际的处理。
从操作系统方面来看,Java
虚拟机是一个应用,而从Java
应用来看,Java
虚拟机就是运行环境。
BIOS和引导
程序的运行环境中,存在着名为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位十六进制数来表示的方式。本地代码的内容就是各种数值的罗列,而这些数值就是本地代码的真面目。每一个数值都表示某一个命令或数据。
编译器负责转换源代码
能够把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}。
DLL文件及导入库
Windows
以函数的形式为应用提供了各种功能。这些形式的函数称为API(Application Programming Interface
,应用程序接口)。
Windows
中,API
的目标文件,并不是存储在通常的库文件中,而是存储在名为DLL(Dynamic Link Library
)文件的特殊库文件中。DLL文件是程序运行时动态结合的文件。
与此相反,存储着目标文件的实体,并直接和EXE
文件结合的库文件形式称为静态链接库
可执行文件运行时的必要条件
EXE
文件是作为单独的文件存储在硬盘中的。通过资源管理器找到并双击EXE
文件,就会把EXE
文件的内容加载到内存中运行。
这里有一个疑问? 本地代码在对程序中记述的变量进行读写时,是参照数据存储的内存地址来运行命令的。在调用函数时,程序的处理流程就会跳转到存储着函数处理内容的内存地址上。EXE
文件作为本地代码的程序,并没有指定变量及函数的实际内存地址。在类似于Windows
操作系统这样的可以加载多个可执行程序的运行环境中,每次运行时,程序内的变量及函数被分配到的内存地址都是不同的。
那么,在EXE
文件中,变量和函数的内存地址的值,是如何来表示的呢?
那就是EXE
文件中给变量和函数分配了虚拟的内存地址。在程序运行时,虚拟的内存地址会转换成实际的内存地址。
链接器会在
EXE
文件的开头,追加转换内存地址所需的必要信息。这个信息称为再配置信息。
EXE
文件的再配置信息,就成了变量和函数的相对地址。相对地址表示的是相对于基点地址的偏移量,也就是相对距离。
程序加载时会生成栈和堆
EXE
文件的内容分为再配置信息、变量组和函数组。不过,当程序加载到内存后,除此之外还会额外生成两个组,那就是栈和堆。
- 栈是用来存储函数内部临时使用的变量(
局部变量
),以及函数调用时所用的参数的内存区域。 - 堆是用来存储程序运行时的任意数据及对象的内存领域
堆和栈的相似之处在于,他们的内存空间都是在程序运行时得到分配。
Q&A
编译器和解释器有什么不同
编译器是在运行前对所有源代码进行解释处理的。而解释器则是在运行时对源代码的内容一行一行的进行解释处理
分割编译
将整个程序分为多个源代码来编写,然后分别进行编译,最后链接成一个EXE
文件。
使用DLL文件的好处
DLL
文件中的函数可以被多个程序共用。因此,借助该功能可以节约内存和磁盘。此外,在对函数的内容进行修正时,还不需要重新链接使用这个函数的程序。
后记
分享是一种态度。
参考资料:《程序是怎样跑起来的》
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。