ChangeLog

[update-2018-11-25]:改正关于 java 执行方式的相关描述

(一)从一段代码到 cpu 运行

1.引

先有这样一个概念——计算机只能识别 0/1 序列。当然,这样说并不准确,在硬件层面计算机其实只能识别电压变动之类的物理信号。为了方便下面的描述,我们先规定以这样的方式来看待计算机,它只能理解 0/1 序列,即只能理解二进制。

所有的我们通过计算机交互的东西,用户程序,系统程序…最终都只能以一串 0/1 序列的形式被计算机识别。

这串 0/1 序列,叫做机器语言。

机器语言就是用二进制编码的机器指令,机器指令是 CPU 能直接识别并执行的指令。由于机器语言可读性太差(一串 0/1),人们通过用简短的英文符号和二进制代码建立对应关系,搞出了汇编语言。

现在来看,汇编代码可读性也不是很好了,人们又弄出了种种高级语言。不论如何,高级语言最终还是得转换为机器语言才能被计算机识别。

2.代码转换为可执行程序

我们使用的程序如何来的?一堆代码经过某些步骤后转化来的。

代码说起来只是一些文本(给人读的嘛),而如上所说,计算机只能识别 0/1 序列。将这些文本转化为计算机可识别的的信息的工具被成为编译器,解释器。

C 语言举例,想要生成可执行文件,需要进行这几个步骤:(虽然在 IDE 上只是一个点击动作)

预处理

编译

汇编

链接

详见(二)C 语言的编译过程。

先不深究。总之经过这几个步骤,C 代码就转换成了可执行文件。这个可执行文件是通过编译器生成的与计算机系统相关的机器指令,即这个可执行文件是和平台相关的。这就带来一些后果,比如在 windows 上生成的可执行程序并不能在 linux 上运行。

相关讨论见(三)代码与平台。

我们把 JAVA 与 C 比较。可以把 C 当做编译型语言,把 JAVA 当做解释型语言。

2018-11-25 更新

只有确定了谈论对象是具体的某种 java 实现版本和执行引擎运行模式,谈解释执行还是编译执行才会比较确切。class 文件中的代码到底如何执行,是由虚拟机判断操作的。

编译型语言:在程序运行之前,有一个单独的编译过程,将程序翻译成机器语言,以后执行这个程序时,就不用再进行翻译了。

解释型语言:是在运行的时候将程序翻译成机器语言,所以运行速度相对于编译型语言要慢。

PS:这里拿编译型,解释型来区分,感觉并不大准确。(java 不是编译,解释在混用吗…)不过这里主要是做个对比,那就这样理解吧…

Java 首先被编译为 .class 文件,也就是字节码,然后由 JVM(Java Virtual Machine,java 虚拟机)来解释执行。

这里其实就是在程序与平台之间做了个桥梁,由 jvm 来处理平台间的差异(帮助开发者做了兼容,如果开发者用 c 写一个跨平台的应用,就需要生成多个可执行文件,而 java 则可以一份代码,多处运行)。

因为中间多了一个解释的过程,也就很好理解为什么普遍认为 java 慢了。

好啦,现在有一个可执行程序了,接下来看看计算机是怎么运行程序的。

3.计算机如何运行可执行程序

当进行启动应用程序操作后(双击快捷方式或 shell 启动),将调用操作系统内核中相应的服务例程,由内核来加载磁盘上的可执行程序到存储器。(就是将应用程序加载到运行内存里去

内核加载完可执行文件中的代码及要处理的数据后,就是重头戏了!

这个运行过程涉及到 cpu 的运算,简单提一些点。

首先内核将程序的第一条指令的地址送到 程序计数器 中,然后执行这条指令,cpu 又计算下一条指令的地址,把它再次存到 程序计数器 中。也就是说,cpu 永远将程序计数器的内容作为将要执行的指令的地址。(这也就是程序计数器的作用)

程序的执行过程就是数据在 cpu ,主储存器和 I/O 模块之间流动的过程,所有数据的流动都是通过总线,I/O 桥接器进行的。

4.现在我们回头引入计算机系统的层次结构。

总结一下上面的内容,用某种程序设计语言编写源程序,代码翻译为机器语言程序,操作系统调用内核把指令和数据装入内存,cpu 执行,从第一条指令开始,直到程序所有指令执行完。这就是代码到运行环境的一个大致流程了。

我们把这个过程再补充一下。

机器语言是如何被计算机执行的呢?

机器语言程序与所运行的计算机硬件与软件之间有一个“桥梁”,这个在软件与硬件之间的东西叫做 指令集体系结构 (ISA)。ISA 定义了一台计算机可以执行的所有指令的集合。关于 ISA,不多说了。

机器语言程序就是一个 ISA 规定的指令的序列。

再往下的实现就涉及到硬件层面了。

ISA 由微体系结构实现,微体系结构由逻辑电路实现,逻辑电路就是和半导体打交道了。

留一个疑问,既然代码转换成的机器语言是和 cpu 交互,为什么不同操作系统生成的这串二进制代码不通用呢?

(二)C 语言的编译过程

1. 过程

预处理

编译

汇编

链接

2.编译与汇编

2.1.预处理

主要用于 C 语言编译器对各种预处理命令进行处理,如对头文件的包含等等。

#include 的处理,就是把 .h 文件的内容插入到源程序文件中。

在刚开始的时候只知道要按

#include<stdio.h>
...

这样的格式来写,后来知道了第一行代码是引入一堆被包装好的代码。那么怎么达到引入的效果呢?就是在预处理的时候把引入文件的内容插入到源程序文件中。

2.2.编译

进行了词法分析,语法分析,语义分析,根据分析的结果进行代码优化和储存分配。

那些从命令行开始的 C 语言教程里会有这么一步 gcc mian.c(类似于该命令),就是使用 gcc 编译器对 C 文件(这里文件名为 mian.c)编译 。大多数编译器里直接 F5 就行了,这样子要稍微底层一些。

具体是怎么做的?先不深究。

gcc 可以直接产生机器语言代码,也可以先产生汇编语言代码,再通过汇编程序将汇编语言代码转换成机器语言代码。

总之,代码要转换成计算机能够识别的形式。

2.3.汇编

如上所说,汇编的功能就是将编译生成的汇编语言代码转换为机器语言代码。

这里有个问题。

通常最终的可执行目标文件是由多个不同模块对应的机器语言目标代码合并而形成的。

比方说写 C 的时候,一大堆函数混合着逻辑写在一个文件里,很不科学(为什么?)。为了代码的可读性以及代码复用之类的原因,经常会把程序分成好几个模块。

在生成单个模块的机器语言目标代码时,不可能确定每条指令或每个数据最终的地址。

举个例子。

//func.c
int add(int x, int y){return x + y;}
//mian.c
...
#include"func.c"
void mian(){
    add(1, 2);
}

在 func.c 里定义了一个函数,然后在 main.c 里引入该文件,再在 main 方法里调用这个函数。要是我们单独对 func.c 进行汇编,此时编译器发现 func.c 还没使用,当然不能分配地址了。

对于汇编生成的文件,.o 格式。在 linux 用 objdump 命令查看(查看反编译的汇编代码),会发现汇编代码的地址是从 0000 开始的。(是这个意思吗?)

通常把汇编生成的机器语言目标代码文件成为可重定位目标文件。

2.3.1符号表是啥?

在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。

2.4可执行目标文件的生成

链接的功能就是将所有关联的可重定位目标文件组合起来,以生成一个可执行文件。

可重定位目标文件和可执行目标文件都是机器语言的目标文件,所不同的是前者是单个模块生成的,而后者是由多个模块组合而成的。因而,在前者中,代码总是从 0 开始(地址),而后者,代码在操作系统规定的虚拟地址空间中产生。

2.4.1虚拟地址空间是啥?

我的理解就是这是对物理地址的抽象。使用的是虚拟地址空间,实际上还是在与物理上的储存器打交道(废话…)。但是使用这种方式更安全,更方便。

为什么更安全,更方便呢?

它的出现一开始是为了解决什么问题呢?

cpu 为什么要使用虚拟地址空间与物理地址空间映射?解决了什么样的问题?

(三)代码与平台

PS:啊!我之前一直在纠结“机器语言程序与 cpu 的指令集相关,如果指令集不同的话,那即便操作系统一样,程序应该也不能运行。难道说操作系统还做了一些解释工作?不然的话操作系统又为什么能安装在不同的 cpu 上。”

相关讨论

首先,明确一点,不同的cpu不是牌子不同,是指不同的架构,如x86,arm,mips,power等,好,接下去,对于同一种架构,对应的指令集的规范是一样的,这就意味着同一个操作系统,只要是装在同一个架构的cpu上,就不用重新编译,因为他们与硬件沟通的语言一样的,即指令集规范。那么,对于不同的架构,意味着不同的指令集,操作系统必须在该指令集的编译器下重新编译,才能安装。

举个最简单的例子,为什么有这么多国人说龙芯不是自己的cpu,就是因为指令集的问题,用了mips的指令集,这就相同于cpu的灵魂一样,外壳再怎么变,还是一个人。

PC上都是X86体系的,所以通用,多媒体部分不同,会有额外的优化,对于其他嵌入式设备,有不同的CSP,这样OS就可以不感知CPU差异了(MIPS还需要额外的编译器)

这两个说法好像都在说“不同的 cpu 用了一样的指令集”

这可能是新手最容易入门的JVM讲解(不过是一场恋爱)

理解Java的跨平台特性,是对JVM最直观的认识。所谓的“一次编译,到处运行”,为什么C/C++ 却不能实现呢?这一类语言直接使用物理硬件(或者说操作系统的内存模型),那么不同系统之间的内存模型是不同的,比如说Linux和Window,这就意味,在Window编译好的代码,却不能在Linux上运行。《深入理解Java虚拟机》记录说,Java虚拟机规范中试图定义一种Java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致性的并发效果。举个现实的例子,一个只会听说中文的人,要如何和一个只会听说英文的人交流,在Java的世界里,采用的方式即是给两边的人各配一名翻译官(JVM),所以,这就是为什么JVM要有window版本,也要有Linux版本。

众所周知,Java的程序编译的最终样子是.class文件,不同虚拟机的对每一个.class文件的翻译结果都是一致的。而对于C/C++而言,编译生成的是纯二进制的机器指令,是直接面对计算机系统的内存,但是,java程序的编译结果是面向JVM,是要交付给JVM,让他再做进一步处理从而让计算机识别运行,这就是所谓的“屏蔽掉各种硬件和操作系统的内存访问差异”。这里的特点又和面向对象推崇的面向接口有着不可描述的关系,我只需要有这么个规范,不需要去知道接触你的底层原理实现。

(四)资料

我所理解的Java到底是解释型语言还是编译型语言

Java 是编译型语言还是解释型语言?

解释器和编译器区别和联系

程序是怎么从代码到执行的

单片机、CPU、指令集和操作系统的关系

为什么一个操作系统可以安装到不同的CPU上?

这可能是新手最容易入门的JVM讲解(不过是一场恋爱)