前端小魔女

V1

2022/11/03阅读:12主题:蔷薇紫

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

学习,说到底是一个,以及学以致用的过程

大家好,我是柒八九

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

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

文章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文件的构造

程序加载时会生成栈和堆

EXE文件的内容分为再配置信息变量组函数组。不过,当程序加载到内存后,除此之外还会额外生成两个组,那就是

  • 是用来存储函数内部临时使用的变量局部变量),以及函数调用时所用的参数的内存区域。
  • 是用来存储程序运行时的任意数据及对象的内存领域
加载到内存的程序由4部分构成
加载到内存的程序由4部分构成

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


Q&A

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

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

分割编译

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

使用DLL文件的好处

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


后记

分享是一种态度

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

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

分类:

前端

标签:

JavaScript

作者介绍

前端小魔女
V1

微信公众号:前端柒八九