zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

计算机底层知识之运行环境&可执行文件

2023-02-18 16:34:14 时间

❝学习,说到底是一个「学」「练」,以及学以致用的过程 ❞

大家好,我是「柒八九」

今天,我们继续「计算机底层知识」的探索。我们来谈谈关于「运行环境&可执行文件」的相关知识点。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. 计算机底层知识之CPU
  2. 计算机底层知识之二进制
  3. 计算机底层知识之处理小数
  4. 计算机底层知识之内存
  5. 计算机底层知识之内存和磁盘的关系&数据压缩

你能所学到的知识点

  1. 运行环境 = 操作系统 + 硬件 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  2. 不同操作系统的API不同 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  3. Java 虚拟机 「推荐阅读指数」 ⭐️⭐️⭐️
  4. BIOS和引导 「推荐阅读指数」 ⭐️⭐️⭐️
  5. 计算机只能运行本地代码 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  6. 本地代码的内容 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  7. 编译器负责转换源代码 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  8. DLL文件及导入库 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  9. 可执行文件运行时的必要条件 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  10. 程序加载时会生成栈和堆 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️

好了,天不早了,干点正事哇。


运行环境 = 操作系统 + 硬件

「应用软件」能够运行,是需要依赖指定的运行环境的。而运行环境是「操作系统」「计算机硬件」两者的综合。也就是说「操作系统」「硬件」决定了程序的运行环境。

这就是说明了,从应用市场上下载软件是,一般都需要按照你「自身本地」的操作系统而选择对应的软件包。

CPU只能解释其「自身固有」「机器语言」

不同的CPU能解释的机器语言的种类也是不同的。例如,CPU品牌分为IntelAMD两种;,它们各自的机器语言是完全不同的。

「机器语言」的程序称为本地代码Native Code。程序员用C/Java等编写的程序,在「编写阶段」仅仅是「文本文件」

文本文件(排除文字编码问题)在「任何环境」下都能显示和编辑。我们称之为「源代码」

❝通过对源代码进行「编译」,就可以得到本地代码 ❞


不同操作系统的API不同

同样机型的计算机,可安装的操作系统类型也会有多种选择。也就意味着,应用软件则必须根据不同的操作系统类型来专门开发。

CPU的类型不同,所对应的机器语言也不同,同样的道理,「操作系统的类型不同,应用程序向操作系统传递指令的途径也是不同的」

应用程序向操作系统传递指令的途径称为「API」(Application Programming Interface)。WindowsUnix系列操作系统的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加载到内存中运行。


源代码完成后,就可以编译生成「可执行文件」了。负责实现该功能的是「编译器」

计算机只能运行本地代码

假设我们通过「高级语言」(语言类型不限),编写一个把123456的平均值289.5显示出来。

function Main(){
  let ave;
  ave = (123 + 456)/2;
  alert(ave);
}

类似上述的代码,用「某种」编程语言编写的程序被称为「源代码」,保存源代码的文件被称为「源文件」。用JS编写的源文件的扩展名通常是.js

上述的源代码是无法直接运行的。这是因为,CPU能直接解析并运行的不是源代码而是「本地代码」的程序。作为计算机大脑的CPU,也「只能解释已经准换成本地代码的程序内容」

本地这个术语有母语的意思。「对CPU来说,母语就是机器语言,而转换成机器语言的程序就是本地代码」。用任何编程语言编写的源代码,最后都要翻译成本地代码,否则CPU就不能理解。也就是说,「即使是用不同编程语言编写的代码,转换成本地代码后,也都变成用同一种语言(机器语言)来表示」


本地代码的内容

WindowsEXE文件的程序内容,使用的就是本地代码。本地代码的内容是人类无法理解的,也正是因为如此,才有了用人类容易理解的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。


DLL文件及导入库

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部分构成

堆和栈的相似之处在于,他们的内存空间都是在程序运行时得到分配。


Q&A

编译器和解释器有什么不同

「编译器」是在「运行前」对所有源代码进行解释处理的。而「解释器」则是在「运行时」对源代码的内容一行一行的进行解释处理

分割编译

将整个程序分为多个源代码来编写,然后分别进行编译,最后链接成一个EXE文件。

使用DLL文件的好处

DLL文件中的函数可以被「多个程序共用」。因此,「借助该功能可以节约内存和磁盘」。此外,在对函数的内容进行修正时,还不需要重新链接使用这个函数的程序。


后记

「分享是一种态度」

参考资料:《程序是怎样跑起来的》

「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」