您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件开发专栏 > 系统/运维 > 正文

编译器、虚拟机、操作系统,到底哪个更难?

发表于:2023-01-09 作者:底层技术栈 来源:今日头条

编译器、虚拟机、操作系统,到底哪个更难?

实际上,除了MATLAB这样的数学软件之外,肯定是编译器更难!

虚拟机和操作系统更多的是麻烦,工作量大,而不是难。

1、虚拟机

什么是虚拟机?

能够运行字节码的程序,就是虚拟机。

CPU的机器码就是一种字节码,它是直接在硬件上跑的,由硬件的数字电路来保证它的运行。

但是虚拟机是由软件程序来保证字节码的运行的。

软件程序是高级语言写的,可以写非常上层的逻辑,实现起来比数字电路简单得多。

到了字节码(机器码)这个层面,逻辑已经非常简单了,远不如高级语言的源代码复杂。

让字节码运行起来,实际上比编译器生成字节码更简单:

因为生成字节码是编码,而让字节码运行是解码,任何时候都是编码比解码更复杂。

编码,需要把杂乱的信息整理成有序的。

解码,只需要把有序的信息顺序读出来就行。

所以,H264编码的CPU消耗,远比H264解码更大!

如果字节码类似RISC架构的机器码(例如ARM),那么每4字节就是一条指令,指令里的每一位做什么都是固定的。

所以,虚拟机的代码就是这样的:

uint32_t codes[N]; // 程序的字节码数组
for (i = 0; i < N; i++) {
uint8_t opcode = codes[i] >> 25; // 最高7位是操作码,可以支持128个指令
uint8_t src = (codes[i] >> 19) & 0x3f; // 接着6位是源操作数的寄存器编号,
uint8_t dst = (codes[i] >> 13) & 0x3f; // 接着6位是目的操作数的寄存器编号,
// 寄存器的编号占6位,可以支持64个寄存器
uint8_t flag = (codes[i] >> 12) & 0x1; // 是否设置跳转标志,
uint16_t imm = codes[i] & 0xfff; // 可以携带12位的立即数,
run(ctx, opcode, src, dst, flag, imm); // 运行字节码,ctx为进程的上下文
}
 

这种程序很难吗?

不难。

机器码的逻辑是特别简单的,比高级语言的代码简单得多!

尤其是RISC架构的,更是比x64的机器码还简单。

x64的机器码因为长度不固定,解释起来要一个字节一个字节的分析,稍微复杂一点,但复杂度也远不如高级语言的源代码!

qemu复杂,是因为它要模拟多个型号的CPU。

如果只是给字节码实现一个跨平台的虚拟机,并不难。

把java源代码变成字节码的过程,远比让java字节码运行起来,要难得多:

前者是编译器,后者是虚拟机。

2、操作系统

如果只是让OS内核在CPU上跑起来,大概只需要5000-8000行的C代码!

Linux 0.01版(即第一个Linux版本)的代码量也就在8000行左右。

Linux 0.11版,大约不到2万行。

与编译器比起来,操作系统只是更麻烦!

因为要支持的驱动模块很多、要支持的文件系统很多、要支持的网络协议很多,这些模块的代码都是工作量​

但是,麻烦不等于难!

8000行代码的OS内核(例如Linux 0.01),只需要实现进程管理、内存管理、控制台管理、键盘驱动、硬盘驱动,另外支持一种简单的文件系统,就可以跑得起来。

这样的OS内核实际上已经很完善了​

剩下的都是在文件系统的底下添加驱动模块、网络协议模块。

按照unix一切皆是文件的设计哲学,外设的驱动模块和TCP/IP协议,都是隶属于文件系统的子模块。

shell(命令解释器)不属于OS内核,而是一个用来解释命令的用户态程序。

当然,shell对系统的使用来说是必需的。

在文件系统的API基础上,实现列目录、创建目录、创建文件之类的功能并不难。

当然,这些命令实现起来的工作量,比让一个8000行的OS内核运行起来还大。

3、编译器

光一个语法分析就可能超过1万行!

如果语法像C++那么复杂,那语法分析的代码量更大。

(如果用第三方的正则表达式库的话,第三方库的代码也没有低于1万行的)

而且编译器的实现中有一些非常别扭的地方,例如C++的如下代码:

vector<vector<A> > vecA;
vector<vector<A>> vecA;
 

两个> >之间必须有一个空格,否则g++是会报错的。

之所以会这样是键盘上的符号太少了,而C++的语法太复杂,在编码上实在应付不过来了。

另外,编译器的后端也有一些非常复杂的模块,例如:指针分析、自动内存管理、循环分析、寄存器分配、goto的处理,等等。

还有一种是并行分析:

for (i = 0; i < N; i++)
a[2*i+1] = a[2*i];
 

显然,N个源位置与N个目的位置是不可能相同的,所以它可以并行复制数据。

人一眼就可以看出来源位置与目的位置的数组读写是不相关的,但用代码怎么判断?

要用整数线性规划。

运筹学上有这一章,龙书(编译原理)里也有提到,我也曾经学过但都忘了​

虚拟机和操作系统真只是工作量大,论难度还是编译器和MATLAB!

我的gitee上也有一个bochs上的内核demo,有兴趣的可以看看,实现起来比scf简单多了。

scf编译器的后端只加了必需的模块,都写了4万行代码。