今天通过学习来尝试自己去写一个虚拟机,我们一切先从简单开始。做事总是需要找一个原因,也就是为什么做这个件事,做这件事的意义,发明蒸汽机为了人类从繁重的劳动中解放出来更多思考,发明计算机为了辅助人类从重复计算和繁重计算中解放出来去享受生活吗? 结果带来负效应 996。
virtual-machind.jpgCLR常用简写词语,CLR是公共语言运行库(Common Language Runtime)和Java 虚拟机一样也是一个运行时环境,它负责资源管理(内存管理和垃圾收集等),并保证应用和底层操作系统之间必要的分离。CLR存在两种不同的翻译名称:公共语言运行库和公共语言运行时。
VM 的基本结构
stack vm 结构图虚拟机实际上对物理计算机进行抽象和虚拟化,所以要写一个虚拟机,首先需要对计算机内部简单结构进行了解。我们开发应该是理论先行。
- 栈
- 还要一个主要方法
- 内存
- 数据内存(data memory)
- 指令内存(code memory)
这两个部分都是用于存储数据和源码内存
字节码相比于机器码是更高级以及更有有规律可寻
字节码作为工作的最小单元
- fetch : fetch 不断循环地执行读取 data memory 和 code memory 获取源码
- decode : 负责将字节码进行解码为指令供 execute 执行
- 寄存器(registers)中保存了 sp 栈指针、fp 栈帧指针和 ip 程序指针
通过 ip 指针的自增一行一行读取字节码然后由 decode 将字节码翻译成指令来执行 - 栈虚拟机
- 操作数栈持有操作数
- 操作数可以入栈或出栈进行操作
- 栈的大小是由每一个方法显式确定的
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
定义字节码
vm_instruction.JPG表中列出应该是助记符应该理解为字节码的别名,字节码应该是以字节为单位的 16进制的数值,为了便于阅读这些字节码将字节码对应别名。
- 例如 iadd 为表示对 inter 型进行加法
这张图要说起来还是比较复杂的,应该说相当复杂了,大家需要理解一下什么是栈帧。动画也好电影也好都是由一帧一帧图所组成的。这样按一定循序不断切换的图片最终形成了我们看到的精彩的电影。
栈帧也是我们程序的组成部分,很少有像我这样了解 CG 的程序员吧,所以我们的程序也是🈶一个一个画面来组成的,程序运行也是一个个按一定顺序,顺序不是固定的,根据用户的交互而定。所以栈帧里存放是一个一个的代码块,栈帧入栈时候会记录下调用这个栈帧的栈帧的信息,也就是当这个栈帧结束后他会返回到上一个栈帧。就是这样连续的或者说一层套一层的栈帧组成我们应用。
能够写出自己语言和虚拟机不是每一个程序员的梦想吗?
朝着这个梦想不断努力,哪怕自己仅仅迈出一小步都会感到欣慰,因为今天的一小步就是明天的一大步。
virtual-machind.jpg
无论指针、数据都是 int 类型这里统一使用 int32 进行保存。
#ifndef STACK_VM_H
#define STACK_VM_H
#include <iostream>
#include <stdint.h>
#include <vector>
// type definitions
typedef int32_t i32;
//隐式作为 32位i 进行处理
class StackVM
{
i32 pc = 100; //program counter
i32 sp = 0; //stack pointer 栈的指针
std::vector<i32> memory; //虚拟内存
i32 typ = 0; //类型
i32 dat = 0; //数据
i32 running = 1;
//private function 获取数据类型和值
i32 getType(i32 instruction);
i32 getData(i32 instruction);
//cup 内部不停循环地运行写函数
void fetch();
void decode();
void execute();
void doPrimitive();
public:
//pubic functions
StackVM();
void run(); //运行虚拟机
//加载程序分配内存
void loadProgram(std::vector<i32> prog);
};
#endif
我们先写头文件,其实无论指针还是我们内存都设计成为 32 位 int 类型所有
typedef int32_t i32;
-
定义 pc 也就是程序指针,初始值设置 100 也就是 vector 列表 100 以后的索引分配给用于存放程序,100 作为数据内存,也可以简单理解为栈内存来使用。
-
定义 sp 为栈指针
-
通过列表来虚拟 memory
-
dat 和 typ 分别为数据和类型
-
typ 中 0 表示正整型 1 表示指令 2 表示负整型 3 表示未定义
-
running 用于控制虚拟机运行的标识
-
fetch 模拟遍历读取到内存中的字节码
-
decode 模拟将字节码解码(翻译)为机器指令
-
execute 模拟执行指令
#include "stack-vm.h"
/**
* 指令格式
* header 2 字节
* data 30 字节
*
* header format
* 0 => positive integer
* 1 => primitive instruction
* 2 => negative integer
* 3 => undefined
*
* */
//functions
StackVM::StackVM()
{
//
memory.reserve(1000000);
}
i32 StackVM::getType(i32 instruction)
{
i32 type = 0xc0000000;
type = (type & instruction) >> 30;
return type;
}
i32 StackVM::getData(i32 instruction)
{
i32 data = 0x3fffffff;
data = data & instruction;
return data;
}
void StackVM::fetch()
{
pc++;
}
void StackVM::decode()
{
typ = getType(memory[pc]);
dat = getData(memory[pc]);
}
void StackVM::execute()
{
if (typ == 0 || typ == 2)
{
sp++;
memory[sp] = dat;
}
else
{
doPrimitive();
}
}
void StackVM::doPrimitive()
{
switch (dat)
{
case 0: //halt
std::cout << "halt" << std::endl;
running = 0;
break;
case 1: //add
std::cout << "add " << memory[sp - 1] << " " << memory[sp] << std::endl;
memory[sp - 1] = memory[sp - 1] + memory[sp];
sp--;
break;
}
}
void StackVM::run()
{
pc -= 1;
while (running == 1)
{
fetch();
decode();
execute();
std::cout << "vm: " << memory[sp] << std::endl;
}
}
void StackVM::loadProgram(std::vector<i32> prog)
{
for (i32 i = 0; i < prog.size(); i++)
{
memory[pc + i] = prog[i];
}
}
- 设置内存大小 memory.reserve(1000000);
- getType() 解释
0100_0000_0000_0000_0000_0000_0000_0001 // 16进制 0xc0000000
&
0100_0000_0000_0000_0000_0000_0000_0000 // 表示命令 0x40000001
进行**与**位运算
0100_0000_0000_0000_0000_0000_0000_0000
>> 30 (右移 30 位运算)
0000_0000_0000_0000_0000_0000_0000_0001
得到 1 所以为指令类型
1100_0000_0000_0000_0000_0000_0000_0000
0000_0000_0000_0000_0000_0000_0000_0011
&
0000_0000_0000_0000_0000_0000_0000_0011
0000_0000_0000_0000_0000_0000_0000_0000
>> 30
0000_0000_0000_0000_0000_0000_0000_000
得到类型为 0 所以为正整型
- getData() 方法算法如下
对于数据 3
0011_1111_1111_1111_1111_1111_1111_1111
0000_0000_0000_0000_0000_0000_0000_0011
&
0000_0000_0000_0000_0000_0000_0000_0011
对于指令 0x40000001
0011_1111_1111_1111_1111_1111_1111_1111
0100_0000_0000_0000_0000_0000_0000_0001
&
0000_0000_0000_0000_0000_0000_0000_0001
- fetch 方法遍历code 数据,然后移动 pc 指针
- decode 对内存中数据进行解码
- excute 根据解码数据进行执行
如果 typ 表示值为数据,压入栈内,如果为指令则调用 doPrimitive 进行处理 - doPrimitive 方法中根据不同的指令进行不同处理
在 add 时候会读取栈内数据进行操作。 - run 方法是启动运行虚拟机,通过循环来不断执行读取内存 fetch 方法,解码字节码 decode 和执行命令 execute 这些方法。
- loadProgram 方法接受一个程序,这里所谓的程序暂时就是一个 i32 的集合。然后将程序模拟读入到内存中。
#include "stack-vm.h"
int main()
{
StackVM vm;
std::vector<i32> prog{
3, 4, 0x40000001, 0x40000000};
vm.loadProgram(prog);
vm.run();
return 0;
}
make cpp
CFLAGS= -std=c++11
all: stack-vm
stack-vm: stack-vm.o main.o
$(CXX) $(CFLAGS) stack-vm.o main.o -o stack-vm
main.o: main.cpp
$(CXX) $(CFLAGS) -c main.cpp
stack-vm.o: stack-vm.h stack-vm.cpp
$(CXX) $(CFLAGS) -c stack-vm.cpp
clean:
rm -f *.o stack-vm
网友评论