# 编写自己的虚拟机

# 什么是虚拟机

p272648921

# 虚拟机的硬件组件的模拟

# 内存空间

虚拟机中模拟内存硬件的作用是为了在一个完全隔离的环境中运行一个完整的计算机系统

# 寄存器

一个寄存器就是 CPU 上一个能够存储单个数据的槽。寄存器就像 CPU 的 “工作台”CPU 要对一段数据进行处理,必须先将数据放到某个寄存器中。但因为寄存器的数量很少,因此在任意时刻只能有很少的数据加载到寄存器。计算机的解决办法是:首先将数据从内存加载到寄存器,然后将计算结果放到其他寄存器,最后将最终结果 再写回内存。

# 指令集

指令是告诉 CPU 执行一些基本任务的命令,例如将两个数字相加。指令既有指示要执行的任务类型的操作码,也有一组为正在执行的任务提供输入的参数。

每个操作码代表 CPU “知道” 如何执行的一项任务。

# 条件标志位

R_COND 寄存器存储条件标记,其中记录了最近一次计算的执行结果。 这使得程序可以完成诸如 if (x > 0) { ... } 之类的逻辑条件。

每个 CPU 都有很多条件标志位来表示不同的情形。

# 一些原理

# 机器码

一种称为 汇编器(assembler)的工具会将这些文本格式的指令转换成 16 比特的二进制指令, 后者是虚拟机可以理解的。这种二进制格式称为机器码(machine code),是虚拟机可以执行的格式,其本质上就是一个 16 比特指令组成的数组。

image.png

# 中断,异常,trap 的了解

这几个我都还没做 233

image.png

# 探索 & 实现 x86 架构的虚拟机 (C 语言)

基于 x86 架构的虚拟机实现可以分为以下几个步骤:

  • 设计虚拟机的结构

  • 编写虚拟机的初始化和运行函数

  • 编写虚拟机的指令集的各指令的实现函数和执行虚拟机的函数

# 虚拟机的基本结构

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
// 定义虚拟机的内存大小
#define MEM_SIZE 512
// 定义虚拟机支持的指令集
enum {
    HLT, // 停止执行
    MOV, // 移动数据
    ADD, // 加法运算
    SUB, // 减法运算
    JMP, // 无条件跳转
    JZ,  // 条件跳转(如果零标志为真)
    JNZ, // 条件跳转(如果零标志为假)
};
// 定义虚拟机支持的寄存器
enum {
    R0 = 0,
    R1,
    R2,
    R3,
    PC, // 程序计数器
    SP, // 栈指针
    FLG,// 标志寄存器(用于保存比较结果)
};
// 定义虚拟机结构体,包含内存、寄存器和运行状态等字段
typedef struct {
    uint8_t mem[MEM_SIZE];   // 内存空间,每个单元 8 位(1 字节)
    uint16_t reg[7];         // 寄存器数组,每个寄存器 16 位(2 字节)
    int running;             // 运行状态,0 表示停止,1 表示运行中
} VM;
// 创建并初始化一个虚拟机实例,并返回指针
VM* vm_create() {
    VM* vm = malloc(sizeof(VM));        // 分配内存空间给虚拟机实例
    vm->reg[PC] = 0;                    // 将程序计数器置为 0(从内存地址 0 开始执行)
    vm->reg[SP] = MEM_SIZE - 2;         // 将栈指针置为内存最高地址 - 2(栈从高地址向低地址增长)
    vm->reg[FLG] = 0;                   // 将标志寄存器置为 0(无比较结果)
    vm->running = 1;                    // 将运行状态置为 1(开始执行)
     // 初始化其他寄存器为 0
    vm->reg[R0] = 0;
    vm->reg[R1] = 0;
    vm->reg[R2] = 0;
    vm->reg[R3] = 0;
    
}
// 加载一段程序到虚拟机的内存中,并返回加载后的程序长度(字节数)
int vm_load(VM* vm, uint8_t* prog) {
    int len = 0;                        // 初始化程序长度为 0
    while (*prog != HLT) {              // 循环读取程序字节,直到遇到 HLT 指令
        vm->mem[vm->reg[PC] + len] = *prog; // 将当前字节写入虚拟机内存中,相对于程序计数器的偏移地址
        prog++;                         // 指向下一个字节
        len++;                          // 程序长度加 1
    }
    vm->mem[vm->reg[PC] + len] = HLT;   // 将 HLT 指令写入虚拟机内存中,作为程序的结束标志
    return len + 1;                     // 返回程序长度(包含 HLT 指令)
}

# 执行函数

本来想在 case 里面定义 dst src 缓存变量的,结果 vscode 一直报错 只好提前定义了

// 定义一个执行函数,接受一个虚拟机实例作为参数
void vm_exec(VM* vm) {
    uint8_t dst;//焯了 本来想在case里面定义的
    uint8_t src;
    while (vm->running) { // 循环执行,直到运行状态为0
        uint8_t op = vm->mem[vm->reg[PC]]; // 从内存中读取当前指令
        switch (op) { // 根据指令类型进行分支处理
            case HLT: // 停止执行
                vm->running = 0; // 将运行状态置为0
                break;
            case MOV: // 移动数据
                dst = vm->mem[vm->reg[PC] + 1]; // 读取目标寄存器编号(第一个操作数)
                src = vm->mem[vm->reg[PC] + 2]; // 读取源寄存器编号(第二个操作数)
                vm->reg[dst] = vm->reg[src]; // 将源寄存器的值赋给目标寄存器
                vm->reg[PC] += 3; // 将程序计数器增加3(跳过指令和操作数)
                break;
            case ADD: // 加法运算
                dst = vm->mem[vm->reg[PC] + 1]; // 读取目标寄存器编号(第一个操作数)
                src = vm->mem[vm->reg[PC] + 2]; // 读取源寄存器编号(第二个操作数)
                uint16_t res = vm->reg[dst] + vm->reg[src]; // 计算两个寄存器的值之和,并保存在16位变量中
                if (res == 0) { // 如果结果为0,则将零标志置为1,否则置为0
                    vm->reg[FLG] = 1;
                } else {
                    vm->reg[FLG] = 0;
                }   
            }
            break;
        }
    }

# 测试用函数

// 一个测试函数,接受一个虚拟机实例作为参数
void vm_test(VM* vm) {
    // 定义一段测试程序,包含以下指令:
    // MOV R0, 10   // 将 10 赋给寄存器 R0
    // MOV R1, 20   // 将 20 赋给寄存器 R1
    // ADD R2, R0   // 将寄存器 R0 和 R1 的值相加,并赋给寄存器 R2
    // HLT          // 停止执行
    uint8_t prog[] = {MOV, R0, 10, MOV, R1, 20, ADD, R2, R0, HLT};
    
    int len = vm_load(vm, prog); // 加载测试程序到虚拟机内存中,并获取程序长度
    printf("Loaded %d bytes of program.\n", len); // 打印程序长度
    
    vm_exec(vm); // 执行虚拟机
    
    
    
    printf("Register values:\n");     // 打印寄存器的值
    for (int i = 0; i < 7; i++) {
        printf("R%d: %d\n", i, vm->reg[i]);
    }
    printf("Execution finished.\n"); // 打印执行结束
}
int main (){
	vm_test( vm_create() );
}

运行结果出错了我焯

mov 指令实现有问题:mov 指令分情况哇,一种是立即数赋给寄存器 一种是寄存器给寄存器

然后虚拟机的初始化有问题 指令读取有问题

# 改变思路解决问题

image.png

看之前做过的 vm 逆向题目 发现多了一个数来判断的 于是改变思路 模仿着这道题写虚拟机

# 完善后的代码

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
// 定义虚拟机的内存大小
#define MEM_SIZE 512
// 定义虚拟机支持的指令集
enum {
    MOV, // 移动数据
    PUSH,
    POP, 
    ALU, 
    CMP,  
    JMP, 
	JNE,
	JE,
    SUB,
    HLT,
 };
// 定义虚拟机支持的寄存器
enum {
    R0,
    R1,
    R2,
    R3,
    R4,
    PC, // 程序计数器
    SP, // 栈指针
    FLG,// 标志寄存器(用于保存比较结果)
};
// 定义虚拟机结构体,包含内存、寄存器和运行状态等字段
typedef struct {
    uint8_t mem[MEM_SIZE];   // 内存空间,每个单元 8 位(1 字节)
    uint16_t reg[8];         // 寄存器数组,每个寄存器 16 位(2 字节)
    int running;             // 运行状态,0 表示停止,1 表示运行中
} VM;
// 创建并初始化一个虚拟机实例,并返回指针
VM* vm_create() {
    VM* vm = malloc(sizeof(VM));        // 分配内存空间给虚拟机实例
    vm->reg[PC] = 0;                    // 将程序计数器置为 0(从内存地址 0 开始执行)
    vm->reg[SP] = MEM_SIZE - 2;         // 将栈指针置为内存最高地址 - 2(栈从高地址向低地址增长)
    vm->reg[FLG] = 0;                   // 将标志寄存器置为 0(无比较结果)
    vm->running = 1;                    // 将运行状态置为 1(开始执行)
    // 初始化其他寄存器为 0
    vm->reg[R0] = 0;
    vm->reg[R1] = 0;
    vm->reg[R2] = 0;
    vm->reg[R3] = 0;
    vm->reg[R4] = 0;
}
// 加载一段程序到虚拟机的内存中,并返回加载后的程序长度(字节数)
int vm_load(VM* vm, uint8_t* prog) {
    int len = 0;                        // 初始化程序长度为 0
    while (*prog != HLT) {              // 循环读取程序字节,直到遇到 HLT 指令
        vm->mem[vm->reg[PC] + len] = *prog; // 将当前字节写入虚拟机内存中,相对于程序计数器的偏移地址
        prog++;                         // 指向下一个字节
        len++;                          // 程序长度加 1
        printf("%d\n", *prog);
    }
     vm->mem[vm->reg[PC] + len] = HLT;   // 将 HLT 指令写入虚拟机内存中,作为程序的结束标志
    return len;                     // 返回程序长度(包含 HLT 指令)
}
// 定义一个执行函数,接受一个虚拟机实例作为参数
void vm_exec(VM* vm) {
    uint8_t dst;
    uint8_t src;
    while (vm->running) { // 循环执行,直到运行状态为 0
        uint8_t op = vm->mem[vm->reg[PC]]; // 从内存中读取当前指令
        switch (op) { // 根据指令类型进行分支处理
            case MOV: // 移动数据 
			if(vm->mem[vm->reg[PC] + 1]==1)
                {dst = vm->mem[vm->reg[PC] + 2]; // 读取目标寄存器编号(第一个操作数)
                src = vm->mem[vm->reg[PC] + 3]; // 读取源寄存器编号(第二个操作数)
                vm->reg[dst] = vm->reg[src]; // 将源寄存器的值赋给目标寄存器
                vm->reg[PC] += 4;}
			else{
				dst = vm->mem[vm->reg[PC] + 2]; // 读取目标寄存器编号(第一个操作数)
                src = vm->mem[vm->reg[PC] + 3];
				vm->reg[dst] = src; // 将目标值赋给目标寄存器
                vm->reg[PC] += 4;
			}; // 将程序计数器增加 4(跳过指令和操作数)
            break;
            case PUSH:
			dst = vm->mem[vm->reg[PC] + 1];
			vm->reg[PC] += 2;
			switch (dst)
			{
			case 1:
				vm->mem[--vm->reg[SP]]=vm->reg[0];
				break;
			case 2:
			    vm->mem[vm->reg[SP]--]=vm->reg[1];
				break;
			default:
			    vm->mem[vm->reg[SP]--]=vm->reg[0];
				break;
			}
            break;
            case POP:
			dst = vm->mem[vm->reg[PC] + 1];
			vm->reg[PC] += 2;
			switch (dst)
			{
			case 1:
				vm->reg[0]=vm->mem[++vm->reg[SP]];
				break;
			case 2:
			    vm->reg[1]=vm->mem[++vm->reg[SP]];
				break;
			default:
			    vm->reg[0]=vm->mem[vm->reg[SP]++];
				break;
			}
            break;
			case ALU:
			dst = vm->mem[vm->reg[PC] + 1];
			switch (dst)
			{
			case 0:
				dst = vm->mem[vm->reg[PC] + 2]; // 读取目标寄存器编号(第一个操作数)
                src = vm->mem[vm->reg[PC] + 3]; // 读取源寄存器编号(第二个操作数)
                vm->reg[dst] += vm->reg[src];// 加法
				break;
			case 1:
			    dst = vm->mem[vm->reg[PC] + 2]; // 读取目标寄存器编号(第一个操作数)
                src = vm->mem[vm->reg[PC] + 3]; // 读取源寄存器编号(第二个操作数)
                vm->reg[dst] -= vm->reg[src];// 减法
                break;
			case 2:
			    dst = vm->mem[vm->reg[PC] + 2]; // 读取目标寄存器编号(第一个操作数)
                src = vm->mem[vm->reg[PC] + 3]; // 读取源寄存器编号(第二个操作数)
                vm->reg[dst] *= vm->reg[src];// 乘法
                break;
			case 3:
			    dst = vm->mem[vm->reg[PC] + 2]; // 读取目标寄存器编号(第一个操作数)
                src = vm->mem[vm->reg[PC] + 3]; // 读取源寄存器编号(第二个操作数)
                vm->reg[dst] ^= vm->reg[src];// 异或
                break;
			default:
				break;
			}
			vm->reg[PC] += 4;
            break;
			case CMP:// 比较的是后面 2 个操作数代表的寄存器的值
			if(vm->reg[(vm->mem[vm->reg[PC] + 1])]==vm->reg[(vm->mem[vm->reg[PC] + 2])])
			{vm->reg[FLG]=0;
            vm->reg[PC] += 3;
            break;}
            if(vm->reg[vm->mem[vm->reg[PC] + 1]]!=vm->reg[vm->mem[vm->reg[PC] + 2]])
			{vm->reg[FLG]=1;
			vm->reg[PC] += 3;
            break;}
			case JMP:
			vm->reg[PC]=vm->mem[vm->reg[PC] + 1];
            break;
			case JNE:
			if(vm->reg[FLG]){
			vm->reg[PC]=vm->mem[vm->reg[PC] + 1];}
			else
			vm->reg[PC]+=2;
            break;
			case JE:
			if(vm->reg[FLG])
			vm->reg[PC]+=2;
			else
			vm->reg[PC]=vm->mem[vm->reg[PC] + 1];
            break;
            case HLT: // 停止执行
            vm->running = 0; // 将运行状态置为 0s
            break;
            }
			
        }
    }
	// 定义一个测试函数,接受一个虚拟机实例作为参数
void vm_test(VM* vm) {
    // 定义一段测试程序 实现一个异或
    int data[16]= {12,78,45,68,44,78,45,79,99,67,34,67,45,78,32,45};// 瞎输的一串数据
    /*
    for (int i=2; i<16; i+=2){
        data[i+1] = data[i]^data[i+1];
    }
    */
    for(int i=0;i<16;i++){
    vm->mem[vm->reg[SP]--]=data[i];// 读取数据到内存
    }
    uint8_t prog[] = {MOV,0,R3,8,MOV,0,R4,1,POP,1,POP,2,ALU,3, R0, R1,PUSH,1,POP,1,ALU,1,R3,R4,CMP,R3,R4,JNE,8,HLT};
    
    int len = vm_load(vm, prog); // 加载测试程序到虚拟机内存中,并获取程序长度
    printf("Loaded %d bytes of program.\n", len); // 打印程序长度
    
    vm_exec(vm); // 执行虚拟机
    
    
    printf("Register values:\n");     // 打印寄存器的值
    for (int i = 0; i < 8; i++) {
        printf("R%d: %d\n", i, vm->reg[i]);
    }
    vm->reg[SP]+=2;
    for(int i=0;i<16;i++){
    printf("%d ", vm->mem[vm->reg[SP]--]);// 打印内存中的数据
    }
    printf("Execution finished.\n"); // 打印执行结束
}
int main (){
	vm_test( vm_create() );
}

# 实现了一个简单的加密:

/*
    for (int i=2; i<16; i+=2){
        data[i+1] = data[i]^data[i+1];
    }
    */

呜呜呜 本来想用虚拟机实现一个 TEA 加密的 但是这几天得甲流了等我写出来了 再贴上来

# LC-3 虚拟机

https://www.jmeiners.com/lc3-vm/