C11编写简易16位虚拟机

2023-12-20 21:52
文章标签 16 编写 虚拟机 简易 c11

本文主要是介绍C11编写简易16位虚拟机,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

虚拟机

在计算领域,VM(虚拟机)是一个术语,指的是模拟/虚拟化计算机系统/架构的系统。

一般来说,虚拟机有两类:

  • 系统虚拟机提供真实机器的完整替代品。 它们实现了足够的功能,允许操作系统在它们上运行。 它们可以共享和管理硬件,有时多个环境可以在同一台物理机器上运行而不会互相妨碍。
  • 处理虚拟机更简单,旨在在与平台无关的环境中执行计算机程序。 JVM 是进程虚拟机的一个很好的例子。

在本文中,我们将开发一个简单的进程虚拟机,旨在在独立于平台的环境中执行简单的计算机程序。 我们的虚拟机基于 LC-3 计算机架构,并且能够解释和执行 LC3 汇编代码(的子集)。

Little Computer 3,或 LC-3,是一种计算机教育编程语言,一种汇编语言,一种低级编程语言。 它具有相对简单的指令集,但可用于编写中等复杂的汇编程序,并且是 C 编译器的可行目标。 该语言比 x86 汇编语言简单,但具有许多与更复杂语言类似的功能。 这些功能使其值得入门教学,因此它最常用于向计算机科学和计算机工程学生教授编程和计算机体系结构基础知识

为简单起见,我们特意从以下功能中剥离了 LC-3 实现:中断处理、优先级、进程、状态寄存器 (PSR)、特权模式、管理程序堆栈、用户堆栈。 我们将只虚拟化最基本的硬件,并且我们将通过陷阱与外界(stdin、stdout)进行交互。

我们受 LC-3 启发的 VM 与当今大多数通用计算机一样,基于冯·诺依曼计算机模型,它将具有三个主要组件:CPU、主存储器、输入/输出设备。

实现

我们的虚拟机功能如下:

  • 我们将程序加载到主存中
  • 在RPC寄存器中,我们保存当前需要执行的指令
  • 我们从指令中获取操作码(前 4 位),并据此解码其余参数
  • 我们执行与给定指令相关的方法
  • 我们增加 RPC 并继续下一条指令

内存

我们的机器有 W=UINT16_MAX 个字,每个字 N=16 位。从 C 的角度来看,我们的内存可以定义为:

uint16_t PC_START = 0x3000;
uint16_t mem[UINT16_MAX+1] = {0};

寄存器

我们的 VM 共有 10 个寄存器,每个寄存器 16 位:

从代码的角度来看,我们可以按如下方式实现它们:

enum regist { R0 = 0, R1, R2, R3, R4, R5, R6, R7, RPC, RCND, RCNT };
uint16_t reg[RCNT] = {0};

指令

指令就像我们向虚拟机发出的命令。

为了提取操作码本身,我们可以编写一个实用宏来应用简单的按位技巧:

#define OPC(i) ((i)>>12)

我们可以在 C 中执行的一个好技巧(从数据建模的角度来看)是将所有可能的指令(及其关联的 C 函数)保存在数组中。 索引将代表实际的操作码(毕竟,操作码是从 0 到 15 的数字),并且该值将是指向相应 C 函数的指针。

#define NOPS (16) // number of instructions
typedef void (*op_ex_f)(uint16_t instruction);
//
// ... other operations here
//
static inline void add(uint16_t i)  { /* code here */ }
static inline void and(uint16_t i)  { /* code here */ }
//
// ... other operations here
//
op_ex_f op_ex[NOPS] = { br, add, ld, st, jsr, and, ldr, str, rti, not, ldi, sti, jmp, res, lea, trap 
};

加法

逻辑位“与”

ld - 加载 RPC + 偏移量

ldi - 间接加载

ldr - 加载+偏移量

lea - 加载有效地址

not - 按位求补

st - 存储

sti - 间接存储

str - 存储+偏移量

jump - 跳转

加载和执行程序

我们只缺少两个功能:主循环和加载程序的能力。

我们虚拟机的主循环如下所示:

bool running=true;
uint16_t PC_START = 0x3000;
void start(uint16_t offset) { reg[RPC] = PC_START + offset; // The RPC is setwhile(running) {uint16_t i = mr(reg[RPC]++); // We extract instructions from the memory// location pointed by RPC           // We (auto)increment RPC       op_ex[OPC(i)](i);            // We execute each instruction}
}

现在,唯一缺少的是将程序加载到我们的虚拟机中的能力,在这方面我们将编写一个 ld_img 方法,能够将二进制文件直接加载到我们的主内存中:

void ld_img(char *fname, uint16_t offset) {// Open (binary) file containing the VM programFILE *in = fopen(fname, "rb");  if (NULL==in) {fprintf(stderr, "Cannot open file %s.\n", fname);exit(1);    }// The position from were we start copying the file// to the main memoryuint16_t *p = mem + PC_START + offset;// Load the program in memoryfread(p, sizeof(uint16_t), (UINT16_MAX-PC_START), in);// Close the file streamfclose(in);
}

该方法返回 void 并接受两个输入参数:

  • 包含我们程序的二进制文件的路径
  • 我们开始将第一条程序指令加载到主内存中的偏移量

我们虚拟机的主要方法如下所示:

int main(int argc, char **argv) {ld_img(argv[1], 0x0);start(0x0);return 0;
}

我们的第一个程序,将从键盘读取两个数字并将它们的总和打印到标准输出。

0xF026    //  1111 0000 0010 0110  TRAP tinu16      ;read an uint16_t in R0
0x1220    //  0001 0010 0010 0000  ADD R1,R0,x0     ;add contents of R0 to R1
0xF026    //  1111 0000 0010 0110  TRAP tinu16      ;read an uint16_t in R0
0x1240    //  0001 0010 0010 0000  ADD R1,R1,R0     ;add contents of R0 to R1
0x1060    //  0001 0000 0110 0000  ADD R0,R1,x0     ;add contents of R1 to R0
0xF027    //  1111 0000 0010 0111  TRAP toutu16     ;show the contents of R0 to stdout
0xF025    //  1111 0000 0010 0101  HALT             ;halt

语法对用户不友好,不是吗?我们的程序其实就是这一系列数字:0xF026 0x1220 0xF026 0x1240 0x1060 0xF027 0xF025。但如果我们仔细观察,就会发现在这些数字中我们一直在编码汇编指令。

例如,让我们看一下这个数字0xF026。其二进制表示为1111 0000 0010 0110。很容易看出1111是trap的编码,TRAPVECT是100111,对应tinu16。

或者为了更直观的表示,我们来分析 0x1220:

0x1220 ->0001   001 000 1  00000 
ADD    R1  R0     IMM5=0     

运行我们的第一个程序

#include <stdio.h>
#include <stdlib.h>uint16_t program[] = {/*mem[0x3000]=*/    0xF026,    //  1111 0000 0010 0110             TRAP trp_in_u16  ;read an uint16_t from stdin and put it in R0/*mem[0x3002]=*/    0x1220,    //  0001 0010 0010 0000             ADD R1,R0,x0     ;add contents of R0 to R1/*mem[0x3003]=*/    0xF026,    //  1111 0000 0010 0110             TRAP trp_in_u16  ;read an uint16_t from stdin and put it in R0/*mem[0x3004]=*/    0x1240,    //  0001 0010 0010 0000             ADD R1,R1,R0     ;add contents of R0 to R1/*mem[0x3006]=*/    0x1060,    //  0001 0000 0110 0000             ADD R0,R1,x0     ;add contents of R1 to R0/*mem[0x3007]=*/    0xF027,    //  1111 0000 0010 0111             TRAP trp_out_u16;show the contents of R0 to stdout/*mem[0x3006]=*/    0xF025,    //  1111 0000 0010 0101             HALT             ;halt
};int main(int argc, char** argv) {char *outf = "sum.obj";FILE *f = fopen(outf, "wb");if (NULL==f) {fprintf(stderr, "Cannot write to file %s\n", outf);}size_t writ = fwrite(program, sizeof(uint16_t), sizeof(program), f);fprintf(stdout, "Written size_t=%lu to file %s\n", writ, outf);fclose(f);return 0;
}

源代码

参阅一 - 亚图跨际
参阅二 - 亚图跨际

这篇关于C11编写简易16位虚拟机的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/517635

相关文章

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo

【JavaScript】LeetCode:16-20

文章目录 16 无重复字符的最长字串17 找到字符串中所有字母异位词18 和为K的子数组19 滑动窗口最大值20 最小覆盖字串 16 无重复字符的最长字串 滑动窗口 + 哈希表这里用哈希集合Set()实现。左指针i,右指针j,从头遍历数组,若j指针指向的元素不在set中,则加入该元素,否则更新结果res,删除集合中i指针指向的元素,进入下一轮循环。 /*** @param

HotSpot虚拟机的经典垃圾收集器

读《深入理解Java虚拟机》第三版笔记。 关系 Serial、ParNew、Parallel Scavenge、Parallel Old、Serial Old(MSC)、Concurrent Mark Sweep (CMS)、Garbage First(G1)收集器。 如图: 1、Serial 和 Serial Old 收集器 2、ParNew 收集器 3、Parallel Sc

理解java虚拟机内存收集

学习《深入理解Java虚拟机》时个人的理解笔记 1、为什么要去了解垃圾收集和内存回收技术? 当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。 2、“哲学三问”内存收集 what?when?how? 那些内存需要回收?什么时候回收?如何回收? 这是一个整体的问题,确定了什么状态的内存可以

Wondows dos下怎么编写bat批处理文件

最近搞php,在运行时,以Nginx+php-cgi.exe方式运行Wordpress项目 打开dos,先cd到php-cgi.exe文件当前目录下执行启动命令:php-cgi.exe -b 127.0.0.1:9001再打开一个dos,再cd到nginx.exe文件当前目录下执行启动命令:start nginx 大概过程要经过这些步骤,觉得很麻烦,就学下怎么编写一个bat文件,以双击运行代替

16 子组件和父组件之间传值

划重点 子组件 / 父组件 定义组件中:props 的使用组件中:data 的使用(有 return 返回值) ; 区别:Vue中的data (没有返回值);组件方法中 emit 的使用:emit:英文原意是:触发、发射 的意思components :直接在Vue的方法中声明和绑定要使用的组件 小炒肉:温馨可口 <!DOCTYPE html><html lang="en"><head><

虚拟机ubuntu配置opencv和opencv_contrib

前期准备  1.下载opencv和opencv_contrib源码 opencv-4.6.0:https://opencv.org/releases/ opencv_contrib-4.6.0:https://github.com/opencv/opencv_contrib 在ubuntu直接下载或者在window上下好传到虚拟机里都可以 自己找个地方把他们解压,个人习惯在home下新建一

react笔记 8-16 JSX语法 定义数据 数据绑定

1、jsx语法 和vue一样  只能有一个根标签 一行代码写法 return <div>hello world</div> 多行代码返回必须加括号 return (<div><div>hello world</div><div>aaaaaaa</div></div>) 2、定义数据 数据绑定 constructor(){super()this.state={na

OpenStack创建虚拟机过程

OpenStack创建虚拟机过程 一、在分析OpenStack创建虚拟机的过程之前,先来梳理一下需要用用到哪些组件。 二、每一步都需要去keystone去进行验证,下图有详细的流程。 登录界面或命令行通过RESTful API向keystone获取认证信息。keystone通过用户请求认证信息,并生成auth-token返回给对应的认证请求。界面或命令行通过RESTful API

用Python编写倒计时程序:详细教程

目录 引言 环境准备 基本概念 代码实现 步骤一:导入必要的库 步骤二:获取用户输入 步骤三:实现倒计时逻辑 步骤四:整合代码 运行程序 高级功能 扩展功能示例:支持分钟和小时输入 扩展功能示例:图形用户界面 (GUI) 总结 引言 倒计时程序是一个非常常见的小工具,广泛用于各种应用场景中,例如考试时间提醒、烹饪计时器、会议倒计时等。Python 作为一种