龙空技术网

总结一下用C语言写虚拟机的要点

底层技术栈 2897

前言:

此刻小伙伴们对“c语言有关的书籍”可能比较关怀,大家都想要知道一些“c语言有关的书籍”的相关文章。那么小编也在网络上搜集了一些关于“c语言有关的书籍””的相关内容,希望大家能喜欢,你们一起来了解一下吧!

之前发过3个视频,详细讲了下怎么用C语言写虚拟机,我再写一篇文章总结一下,毕竟文章看得快[捂脸]

1,总纲

虚拟机是个用软件实现的“CPU”,CPU是个用硬件实现的“虚拟机”。

这句话是虚拟机和CPU的哲学定义,它们在信息处理的意义上是等价的。

虚拟机可以用来处理字节码,CPU也可以用来处理指令码。

不管是字节码还是指令码,都是二进制的数字序列,一般长度为32位(4字节)。

虚拟机的字节码和CPU的指令码,在信息编码上是完全一样的。

2,字节码的设计

字节码的设计和CPU指令集的设计是一样的,也是要考虑指令码、扩展位、寄存器编码各占据多少位。

因为使用64位的字节码太过浪费,而32位的字节码虽然有点局促,但现在常用的还是32位。

不管是字节码还是CPU指令集,常用的指令大类都有50多类,所以指令码要占6个二进制位(2^6 = 64)才够用。

去掉6位的指令码之后,还剩下26位可用。

1)如果是跳转指令(或函数调用),那么最远可以跳转的目标范围是-128M 到 128M之间。

2^20 = 1M,2^5 = 32,还有1个符号位,直接这么算是 -32M 到 32M之间。

但是因为每条指令的大小都是4字节,所以指令地址的末尾两位都是0,不需要编码到字节码里,所以实际的跳转范围还要乘以4(即-128M到128M)。

2)如果是携带寄存器的三地址码,要去掉3个寄存器编号所占的位数。

在16个寄存器的情况下占12位,在32个寄存器的情况下占15位:2^4 = 16,2^5 = 32。

在3个寄存器的情况下,至少还有26 - 15 = 11位可用。这11位一般用来编码移位信息。

字节码里的移位信息,10位足够了

因为64 = 2^6,所以64位机的移位位数只需要6位即可编码。

再加上左移、逻辑右移、算术右移这3种运算方式(只需两位,2^2 = 4 > 3),所以只需8位就可以编码移位信息了。

大多数RISC指令集的指令码,如上图所示,跟我为scf编译器设计的Naja字节码类似。

3)指令里的常数,

指令里的常数在汇编语言里叫立即数。

立即数是没什么编码技巧的,16位的就需要16位,32位的就需要32位,所以RISC指令集在加载立即数时都比较耗费时间。

一般来说,在基地址+偏移量(base+disp)的寻址方式下,基地址寄存器和目标寄存器各占5位(2x5 = 10),再去掉6位的指令码,还剩下16位可以编码偏移量。

再考虑到符号扩展或零扩展也要占据4位,所以实际的偏移量只有12位(有符号数),即偏移量范围是-2048到2047。

如果不是做为偏移量的立即数,而是赋值语句(mov指令)的立即数,则可以达到16位。

所以,在RISC字节码层面要加载一个64位的常数需要4条指令,代价非常大。

3,虚拟机的写法

编译之后的字节码文件与一般的可执行文件没有区别,只是前者的内容是脚本语言的字节码,后者的内容是CPU的机器码。

对于按RISC理念设计的字节码来说,解码非常的容易。

6位的指令码,5-15位的寄存器,再加上移位、立即数等扩展信息,一个简单的移位加与运算就可以实现解码。

所以虚拟机的主函数就是一个for循环[呲牙]

6位的指令码最多有64种,每种定义一个处理函数,把这些函数的指针组成一个数组,就可以实现解码了。

4,动态库函数的处理

虚拟机的字节码一样要调用C库的API去实现输入输出,所以动态库函数的处理是虚拟机的一个关键方面。

完全可以让字节码文件也使用ELF格式,这样对动态库的处理就与C语言一样了。

C库的dlopen(), dlsym(), dlclose()函数,可以帮助我们简单地加载动态库和查找库函数。

只需要在某个库函数第一次被调用时做动态连接,然后就可以正常使用它了,这种方式在Linux上被叫做Lazy模式。

scf编译器的Naja虚拟机也是对动态库使用Lazy模式。

我的代码风格大多是跟Linux之父学的,他既然用Lazy模式,那我萧规曹随就行了。

我是Linux的铁粉[笑哭]

本文的细节内容都已经做成视频了,但考虑到程序员更喜欢看文档,所以把它写成文章放到编译器的合集里。

C语言的入门就看这本书,不过我的读者里应该没有C语言不入门的[捂脸]

标签: #c语言有关的书籍