0%

编写虚拟机

注意,接下来要实现的虚拟机不是VMware那种有完整操作的虚拟机,而是JVM这种(解释器),更像一个翻译,可以将汇编语言转成高级语言。我们将用代码模拟计算机的硬件组件。

虚拟机概念

简单来说虚拟机可以定义为一个软件程序,用来模拟一些其他物理机的功能。

LC-3 架构

下面我们将实现LC-3虚拟机而不是x86计算机。与x56相比,LC-3 的指令集更 加简化,但现代 CPU 的主要思想其中都包括了。

内存

该计算机有65536(16bit)个内存空间,每个位置可以存储一个16bit的值。我们用数组来模拟内存。

1
uint16_t  memory[UINT16_MAX];     //UINT16_MAX为16bit所能表示的最大值

寄存器

就像cpu的工作台,cpu要对一段数据进行处理则必须要将这段数据放到寄存器中。lc-3有十个寄存器,其中八个通用寄存器,一个程序计数器寄存器,一个条件标志位寄存器。

通用寄存器可以执行任何程序计算。计数器寄存器存放下一条指令的地址,条件寄存器记录上一次计算的正负符号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum //10个寄存器,八个通用,一个程序计数器寄存器,一个条件寄存器
{
R_R0=0,
R_R1,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC,//程序计数器寄存器,存储要执行的指令的地址
R_COND,
R_COUNT//这个值为10,是下面reg数组的长度
};
//用数组来表示这些寄存器
uint16_t reg[R_COUNT];

指令集

指令集是一组计算机处理器可以理解和执行的指令或操作码,机器语言则是指令的二进制形式。一条指令就是一条cpu命令,他告诉cpu执行什么任务。一条指令包括两个部分:1.操作码 2.参数

每个操作码(0000—1111)代表一种任务,每条指令16bit,左侧的4bit存储操作码,其余存储参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum//定义操作码   
{
OP_BR = 0,//分支指令
OP_ADD,
OP_LD,//LOAD 加载指令
OP_ST,//STORE 存储指令
OP_JSR,//JUMP REGISTER 寄存器跳转指令
OP_AND,//BITWISE AND 按位 与
OP_LDR,//LOAD REGISTER 寄存器装载指令
OP_STR,//STORE REGISTER 寄存器存储指令
OP_RTI,//UNUSED 未使用
OP_NOT,//BITWISE NOT 按位非
OP_LDI,//LOAD INDIRRCT 间接装载指令
OP_STI,//STORE INDIRECT 间接存储指令
OP_JUMP,//JUMP
OP_RES,//RESERVED(UNUSED)
OP_LEA,//LOAD EFFECTIVE ADDRESS 加载有效地址
OP_TRAP//EXECUTE TRAP 执行陷阱指令
};

条件标志位

1
2
3
4
5
enum {
FL_POS = 1 << 0, /* P */ //正数 0001
FL_ZRO = 1 << 1, /* Z */ //零 0010
FL_NEG = 1 << 2, /* N */ //负数 0100
};

硬件的模拟完成。

执行程序

过程

  1. 从PC寄存器指向的内存地址中加载一条指令
  2. 递增PC寄存器(指向下一条命令)
  3. 查看指令的操作码,判断指令类型
  4. 根据指令类型和指令中的参数执行该指令
  5. 跳转到第一步

这个过程一点也不陌生和x86机器一摸一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int main(int argc, const char* argv[]) {
{Load Arguments, 12}
{Setup, 12}

/* set the PC to starting position */
enum { PC_START = 0x3000 }; /* 0x3000 is the default */
reg[R_PC] = PC_START;

int running = 1;
while (running) {
uint16_t instr = mem_read(reg[R_PC]++); /* FETCH */ //步骤一、步骤二
uint16_t op = instr >> 12;

switch (op) { //步骤三、四
case OP_ADD: {ADD, 6} break;
case OP_AND: {AND, 7} break;
case OP_NOT: {NOT, 7} break;
case OP_BR: {BR, 7} break;
case OP_JMP: {JMP, 7} break;
case OP_JSR: {JSR, 7} break;
case OP_LD: {LD, 7} break;
case OP_LDI: {LDI, 6} break;
case OP_LDR: {LDR, 7} break;
case OP_LEA: {LEA, 7} break;
case OP_ST: {ST, 7} break;
case OP_STI: {STI, 7} break;
case OP_STR: {STR, 7} break;
case OP_TRAP: {TRAP, 8} break;
case OP_RES:
case OP_RTI:
default:
{BAD OPCODE, 7}
break;
}
}
{Shutdown, 12}
}

这就是主要框架,接下来的任务就是具体的写出每一个指令实现了什么功能。详单与我们去完善一些函数。

指令实现

在这里我们描述指令如何运行

ADD指令

ADD 指令将两个数相加,然后将结果存到一个寄存器中。

有两种格式:ADD R2 R0 R1 ;ADD R0 R0 1 第一种是将R0+R2存储到R2,第二种是将R0+1存到R0。

第二种方式我们可以看出立即数只有五位,我们得把它加载到一个16bit的空间上,这就涉及到有符号扩展,这部分内容CSAPP第二章讲的很详细,对于正数我们直接给高位填充0即可,对于负数,由于是用补码表示,所以我们将高位填充1,举个例子:

1
2
 5-->0101-->0000 0101
-5-->1011-->1111 1011
1
2
3
4
5
6
uint16_t sign_extend(uint16_t x, int bit_count) {  //实现有符号扩展
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}

每次使用ADD都会涉及到寄存器值的改变,每当改变发生我们都要更新标志寄存器

1
2
3
4
5
6
7
8
9
10
void update_flags(uint16_t r){
if(reg[r]==0){
reg[R_COND]=FL_ZRO;
}
else if (reg[r] >> 15) { //查看符号位
reg[R_COND] = FL_NEG;
} else {
reg[R_COND] = FL_POS;
}
}

实现ADD的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
{
uint16_t r0=(instr>>9)&0x7;//只看9到11位,即目的寄存器
uint16_t r1=(instr>>6)&0x7;//第一个参数 寄存器编号
uint16_t imm_flag=(instr>>5)&1;//查看第五位从而确定最后一个参数是寄存器还是立即数
if(imm_flag){//立即数模式
uint16_t imm5=sign_extend(instr&0x1f,5);//短于16bit的值进行符号扩展
reg[r0]=reg[r1]+imm5;
}else{ //寄存器模式
uint16_t r2=instr&0x7;
reg[r0]=reg[r1]+reg[r2];
}
update_flags(r0);//查看计算的结果并更新标志寄存器
}

这就是完整的ADD啦,小巧而精悍。

LDI指令

LDI 是 load indirect 的缩写,用于从内存加载一个值到寄存器。感觉和x86里面的mov与lea(load effective address)相似。 LDI 的二进制格式如下:

实现逻辑是这样的:0-8是内存地址,将程序计数器寄存器的值加上此值,pc自加1,自加1之后的PC就是加载地址,取出该内存单元的值,将其放入9-11位所表示的寄存器中,这个0-8,也就是能够表示一个9bit的地址,我们的内存是16位的,所以显然LDI指令只能对附近的一些内存单元进行操作,这在x86中也有体现,当时只是知道一些指令只能进行段内跳转,这还是相对跳转hhh,现在清楚了为什么是远眺转为什么是近跳转。“可以将它想想成 C 中有一个局部变 量,这变量是指向某些数据的指针”,C语言中的局部变量也是因为距离的原因不能作用于全局嘛??

1
2
3
4
5
6
{
uint16_t r0=(instr>>9)&0x7;//取目的寄存器
uint16_t pc_offset=sign_extend(instr&0x1ff,9);//符号扩展,第一个参数是该数的位,第二个参数描述长度
reg[r0]=men_read(mem_read(reg[R_PC]+pc_offset));//读取指定内存地址处的值加载入目的寄存器
update_flags(r0);//更新标志寄存器
}

Bitwise and (按位与)

AND和上面的ADD一样,也有两种模式即立即模式和寄存器模式。照着葫芦画瓢就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
uint16_t r0=(instr>>9)&0x7;//目的寄存器
uint16_t r1=(instr>>6)&0x7;//寄存器编号
uint16_t imm_flag=(instr>>5)&0x1;//确定模式
if(imm_flag){
uint16_t imm5=sign_extend(instr&0x1f,5);
reg[r0]=reg[r1]&imm5;
}
else{
uint16_t r2=instr&0x7;
reg[r0]=reg[r1]&reg[r2];
}
update_flags(r0);
}

其实就是把ADD代码里的+换成了&这么简单。

Bitwise not(按位非)

位上的0变1,1变0;

1
2
3
4
5
6
{
uint16_t r0=(instr>>9)&0x7;//目的寄存器
uint16_t r1=(instr>>6)&0x7;//寄存器编号
reg[r0]=~reg[r1];
update_flags(r0);
}

Branch(条件分支)

11-9所示的三位分别代表负数、零、正数,当n被设置为1的时候,就会检测条件码寄存器的值是否为FL_NEG,如果相等则跳转,其余两位原理相同。可以进行一些组合,比如设置为011则代表如果最近一次寄存器中的值更改为0或正数都会进行跳转。特殊的,如果设置为000则不进行检测,无条件跳转。(这里为什么不设置成111,这样无论正数负数还是0都会进行跳转啊),至于如何跳转,就是进行pc=pc+pccoffset9.那这样进行排列组合的话就有八种可能。

我的狗屎代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
uint16_t n=(instr>>11)&0x1;
uint16_t z=(instr>>10)&0x1;
uint16_t p=(instr>>9)&0x1;
uint16_t pc_offset=sign_extend(instr&0x1ff,9);
uint16_t nzp=(instr>>9)&0x7;//000 001 010 011 100 101 110 111
if(nzp==0)
{
reg[R_PC]=reg[R_PC]+pc_offset;
}
else if(nzp==1)
{//判断是否为正数
if(reg[R_COND]==FL_POS)
{
reg[R_PC]=reg[R_PC]+pc_offset;
}
}
else if(nzp==2)
{

}
}////////////这样一个个的判断当然不是不行,可就是这八种情况少说也得有70行代码,我觉得应该不会如此臃肿,果然,作者的实现只用了5行代码woc

作者代码:

1
2
3
4
5
6
7
{
uint16_t pc_offset=sign_extend(instr&0x1ff,9);
uint16_t cond_flag=(instr>>9)&0x7;
if(cond_flag&reg[R_COND]){
reg[R_PC]+=pc_offset;
}
}

最精华的部分是在 if(cond_flag&reg[R_COND])这里,太精华了,这样一看代码果然前面的那个无条件跳转是将nzp三位设置成111而不是000.从这一步也可以解释当时条件码为什么要用移位符号那样设置。

Jump(跳转)

RET 在规范中作为一个单独的指令列出,因为在汇编中它是一个独立的关键字。但是,RET 本质上是 JMP 的一个特殊情况。当 R1 为 7 时会执行 RET。

无条件的跳转到寄存器指示的位置。

1
2
3
4
{
uint16_t r0=(instr>>6)&0x7;
reg[R_PC]=reg[r0];
}

Jump Register(跳转寄存器)

也是两种情况。根据文章的解释将它理解为x86里的call指令应该没什么不妥,毕竟他首先将下一条指令存到了寄存器rR7中,这对应了push ip操作吧。JSR模式:将pc加上pccoffset11;JSSR模式:将pc设置为寄存器的值。一个是远眺转一个是进跳转吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
uint16_t imm_flag=(instr>>11)&0x1;
uint16_t long_pc_offset = sign_extend(instr & 0x7ff, 11);
uint16_t long_flag = (instr >> 11) & 1;

reg[R_R7] = reg[R_PC];
if (long_flag) {
reg[R_PC] += long_pc_offset; /* JSR */
} else {
reg[R_PC] = reg[r1]; /* JSRR */
}
break;//终止包含他的循环,并执行下一阶段。结束这次的switch
}

在这个最后为什么要加上个break呀,在模板里break不是统一加在了最后吗,去掉应该没啥问题。

Load(加载)

相对地址在后面的pc段,用相对寻址的方法取得目的地址存放的数据,载入目的寄存器中。

1
2
3
4
5
6
{
uint16_t r0=(instr>>9)&0x7;
uint16_t pc_coffset=sign_extend(instr&0x1ff,9);
reg[r0]=mem_read(reg[R_PC]+pc_coffset);
update_flags(r0);
}

Load Register(加载寄存器)

1
2
3
4
5
6
7
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
reg[r0] = mem_read(reg[r1] + offset);
update_flags(r0);
}

Load Effective Address(加载有效地址)

和load小小不同

1
2
3
4
5
6
{
uint16_t r0=(instr>>9)&0x7;
uint16_t pc_coffset=sign_extend(instr&0x1ff,9);
reg[r0]=reg[R_PC]+pc_coffset;
update_flags(r0);
}

Store(存储)

将寄存器中的值存储到我们指定的内存单元

1
2
3
4
5
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1ff, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
}

Store Indirect(间接存储)

1
2
3
4
5
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1ff, 9);
mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]);
}

Store Register(存储寄存器)

1
2
3
4
5
6
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
mem_write(reg[r1] + offset, reg[r0]);
}

涉及到的一些函数

mem_read()

1
2
3
4
5
6
7
8
9
10
11
12
uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR) {
if (check_key()) {
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
} else {
memory[MR_KBSR] = 0;
}
}
return memory[address];
}

mem_write()

1
2
3
void mem_write(uint16_t address, uint16_t val) {
memory[address] = val;
}

中断陷入例程

这是什么,和之前接触的内中断、外中断一回事吗?

LC-3 提供了几个预定于的函数(过程),用于执行常规任务以及与 I/O 设备交换, 例如,用于从键盘接收输入的函数,在控制台上显示字符串的函数。这些都称为 trap routines,你可以将它们当做操作系统或者是 LC-3 的 API。 每个 trap routine 都有一个对应的 trap code(中断号)。要执行一次捕获, 需要用相应的 trap code 执行 TRAP 指令。

1
2
3
4
5
6
7
8
enum {
TRAP_GETC = 0x20, /* 从键盘获取字符,不在终端回显 */
TRAP_OUT = 0x21, /* 输出一个字符 */
TRAP_PUTS = 0x22, /* 输出一个字符串 */
TRAP_IN = 0x23, /* 从键盘获取字符,并在终端回显 */
TRAP_PUTSP = 0x24, /* 输出一个字节字符串*/
TRAP_HALT = 0x25 /* 停止程序运行 */
};

“字节字符串”(Byte String)是由多个字节组成的序列,每个字节都有8位。字节字符串不像字符串那样直接包含可读的字符,而是用于表示各种类型的信息,如图像、声音、视频、压缩文件等。在字节字符串中,单独的字节没有特定的意义,而是需要根据上下文来解释。

逻辑:

1
2
3
4
5
6
7
8
switch (instr & 0xFF) {
case TRAP_GETC: {TRAP GETC, 9} break;
case TRAP_OUT: {TRAP OUT, 9} break;
case TRAP_PUTS: {TRAP PUTS, 8} break;
case TRAP_IN: {TRAP IN, 9} break;
case TRAP_PUTSP: {TRAP PUTSP, 9} break;
case TRAP_HALT: {TRAP HALT, 9} break;
}

PUTS

PUT trap code 用于输出一个以空字符结尾的字符串(和 C 中的 printf 类似)显示一个字符串需要将这个字符串的地址放到 R0 寄存器,然后触发 trap。

将字符串存储到一个连续的内存区域。由于LC-3内存寻址是16位的,所以打印之前需要先将内存中的值转换为char类型再输出。

1
2
3
4
5
6
7
8
{
uint16_t* c=memory+reg[R_R0];//memory内存首地址
while(*c){//c语言的字符串结尾会放一个0,ASCII的0来代表结束
putc((char)*c,stdout);//stdout是标准输出流,这是一个地址,类似于之前汇编接触的演示区域
++c;//自增,加载下一个字符
}
fflush(stdout);//该函数会将缓冲区中尚未输出的数据立即写入到文件中,并清空缓冲区。
}

输入单个字符(Input Character)

1
2
3
{
reg[R_R0] = (uint16_t)getchar();
}

输出单个字符(Output Character)

1
2
3
4
{
putc((char)reg[R_R0], stdout);
fflush(stdout);
}

打印输入单个字符提示(Prompt for Input Character)

1
2
3
4
5
6
{
printf("Enter a character: ");
char c = getchar();
putc(c, stdout);
reg[R_R0] = (uint16_t)c;
}

输出字符串(Output String)

1
2
3
4
5
6
7
8
9
10
11
{
uint16_t* c = memory + reg[R_R0];
while (*c) {
char char1 = (*c) & 0xFF;
putc(char1, stdout);
char char2 = (*c) >> 8;//这里细节注意 字符mzy在内存中是 zm ’0‘y因为寻址是16位的两个字节为一组,要想正常打印我们就要将顺序颠倒一下。
if (char2) putc(char2, stdout);
++c;
}
fflush(stdout);
}

暂停程序执行(Halt Program)

1
2
3
4
5
{
puts("HALT");//在终端上显示HALT
fflush(stdout);//立即显示
running = 0;//置零,用于程序的判断
}

加载程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void read_image_file(FILE* file) {
uint16_t origin; /* the origin tells us where in memory to place the image */
fread(&origin, sizeof(origin), 1, file);//用于打开和读取文件
origin = swap16(origin);//变换字节序,将我们物理机产生的小端数据转换为LC-3的大端

/* we know the maximum file size so we only need one fread */
uint16_t max_read = UINT16_MAX - origin;//计算出可以读取的最大字节数
uint16_t* p = memory + origin;//指针p指向程序入口
size_t read = fread(p, sizeof(uint16_t), max_read, file);
//从输入流file中读取最多max_read个元素,并将他们存储在以p为起始地址的内存位置中,返回值是读取的元素数存储在read中。
/* swap to little endian */
while (read-- > 0) {
*p = swap16(*p);
++p;
}
}
uint16_t swap16(uint16_t x) {//变换大小端
return (x << 8) | (x >> 8);
}

对加载程序进行封装,经过封装之后,接受一个文件路径字符串作为参数,这样更加方便。搜嘎,原来这就是封装的奥妙。

1
2
3
4
5
6
7
int read_image(const char* image_path) {
FILE* file = fopen(image_path, "rb");
if (!file) { return 0; };
read_image_file(file);
fclose(file);
return 1;
}

内存映射寄存器

一些寄存器不能通过常规的寄存器表进行访问,而内存中储存了特殊的地址,可以通过读写相应的内存地址而达到读写这些寄存器的目的。

是不是和端口有点关系。

  • KBSR:键盘状态寄存器(keyboard status register),表示是否有键按下
  • KBDR:键盘数据寄存器(keyboard data register),表示哪个键按下了
1
2
3
4
enum {
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
};

访问方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void mem_write(uint16_t address, uint16_t val) {
memory[address] = val;
}

uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR) {
if (check_key()) {
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
} else {
memory[MR_KBSR] = 0;
}
}
return memory[address];
}

完整的虚拟机

运行时需要传参 ./LC-3\ VM.exe 2048.obj 就可以玩2048了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
/* windows only */
#include <Windows.h>
#include <conio.h> // _kbhit
uint16_t memory[UINT16_MAX];//每个存储单元存储一个16位的值
//{Memory Mapped Registers}
uint16_t check_key();
uint16_t mem_read(uint16_t address);
void mem_write(uint16_t address, uint16_t val);
enum
{
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
};
void mem_write(uint16_t address, uint16_t val)
{
memory[address] = val;
}

uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR)
{
if (check_key())
{
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
}
else
{
memory[MR_KBSR] = 0;
}
}
return memory[address];
}
//陷阱代码
enum
{
TRAP_GETC = 0x20, /* get character from keyboard, not echoed onto the terminal */
TRAP_OUT = 0x21, /* output a character */
TRAP_PUTS = 0x22, /* output a word string */
TRAP_IN = 0x23, /* get character from keyboard, echoed onto the terminal */
TRAP_PUTSP = 0x24, /* output a byte string */
TRAP_HALT = 0x25 /* halt the program */
};

enum
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC, /* program counter */
R_COND,
R_COUNT
};
uint16_t reg[R_COUNT];

enum
{
OP_BR = 0, /* branch */
OP_ADD, /* add */
OP_LD, /* load */
OP_ST, /* store */
OP_JSR, /* jump register */
OP_AND, /* bitwise and */
OP_LDR, /* load register */
OP_STR, /* store register */
OP_RTI, /* unused */
OP_NOT, /* bitwise not */
OP_LDI, /* load indirect */
OP_STI, /* store indirect */
OP_JMP, /* jump */
OP_RES, /* reserved (unused) */
OP_LEA, /* load effective address */
OP_TRAP /* execute trap */
};

HANDLE hStdin = INVALID_HANDLE_VALUE;
DWORD fdwMode, fdwOldMode;

void disable_input_buffering()
{
hStdin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hStdin, &fdwOldMode); /* save old mode */
fdwMode = fdwOldMode
^ ENABLE_ECHO_INPUT /* no input echo */
^ ENABLE_LINE_INPUT; /* return when one or
more characters are available */
SetConsoleMode(hStdin, fdwMode); /* set new mode */
FlushConsoleInputBuffer(hStdin); /* clear buffer */
}

void restore_input_buffering()
{
SetConsoleMode(hStdin, fdwOldMode);
}

uint16_t check_key()
{
return WaitForSingleObject(hStdin, 1000) == WAIT_OBJECT_0 && _kbhit();
}

void handle_interrupt(int signal)
{
restore_input_buffering();
printf("\n");
exit(-2);
}

uint16_t sign_extend(uint16_t x, int bit_count)
{
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}

uint16_t swap16(uint16_t x)
{
return (x << 8) | (x >> 8);
}

enum
{
FL_POS = 1 << 0, /* P */
FL_ZRO = 1 << 1, /* Z */
FL_NEG = 1 << 2, /* N */
};
void update_flags(uint16_t r)
{
if (reg[r] == 0)
{
reg[R_COND] = FL_ZRO;
}
else if (reg[r] >> 15) /* a 1 in the left-most bit indicates negative */
{
reg[R_COND] = FL_NEG;
}
else
{
reg[R_COND] = FL_POS;
}
}

void read_image_file(FILE* file)
{
/* the origin tells us where in memory to place the image */
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);

/* we know the maximum file size so we only need one fread */
uint16_t max_read = UINT16_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);

/* swap to little endian */
while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}
int read_image(const char* image_path)
{
FILE* file = fopen(image_path, "rb");
if (!file) { return 0; };
read_image_file(file);
fclose(file);
return 1;
}




int main(int argc, const char* argv[])
{
if (argc < 2)
{
/* show usage string */
printf("lc3 [image-file1] ...\n");
exit(2);
}

for (int j = 1; j < argc; ++j)
{
if (!read_image(argv[j]))
{
printf("failed to load image: %s\n", argv[j]);
exit(1);
}
}
signal(SIGINT, handle_interrupt);
disable_input_buffering();

/* since exactly one condition flag should be set at any given time, set the Z flag */
reg[R_COND] = FL_ZRO;

/* set the PC to starting position */
/* 0x3000 is the default */
enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;

int running = 1;
while (running)
{
/* FETCH */
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;

switch (op) { //步骤三、四
case OP_ADD: {
uint16_t r0 = (instr >> 9) & 0x7;//只看9到11位,即目的寄存器
uint16_t r1 = (instr >> 6) & 0x7;//第一个参数 寄存器编号
uint16_t imm_flag = (instr >> 5) & 1;//查看第五位从而确定最后一个参数是寄存器还是立即数
if (imm_flag) {//立即数模式
uint16_t imm5 = sign_extend(instr & 0x1f, 5);//短于16bit的值进行符号扩展
reg[r0] = reg[r1] + imm5;
}
else { //寄存器模式
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] + reg[r2];
}
update_flags(r0);//查看计算的结果并更新标志寄存器
} break;
case OP_AND: {
uint16_t r0 = (instr >> 9) & 0x7;//目的寄存器
uint16_t r1 = (instr >> 6) & 0x7;//寄存器编号
uint16_t imm_flag = (instr >> 5) & 0x1;//确定模式
if (imm_flag) {
uint16_t imm5 = sign_extend(instr & 0x1f, 5);
reg[r0] = reg[r1] & imm5;
}
else {
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] & reg[r2];
}
update_flags(r0);
} break;
case OP_NOT: {
uint16_t r0 = (instr >> 9) & 0x7;//目的寄存器
uint16_t r1 = (instr >> 6) & 0x7;//寄存器编号
reg[r0] = ~reg[r1];
update_flags(r0);
} break;
case OP_BR: {
uint16_t pc_offset = sign_extend(instr & 0x1ff, 9);
uint16_t cond_flag = (instr >> 9) & 0x7;
if (cond_flag & reg[R_COND]) {
reg[R_PC] += pc_offset;
}
} break;
case OP_JMP: {
uint16_t r0 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r0];
} break;
case OP_JSR: {
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t long_pc_offset = sign_extend(instr & 0x7ff, 11);
uint16_t long_flag = (instr >> 11) & 1;

reg[R_R7] = reg[R_PC];
if (long_flag) {
reg[R_PC] += long_pc_offset; /* JSR */
}
else {
reg[R_PC] = reg[r1]; /* JSRR */
}
break;//终止包含他的循环,并执行下一阶段。结束这次的switch
} break;
case OP_LD: {
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_coffset = sign_extend(instr & 0x1ff, 9);
reg[r0] = mem_read(reg[R_PC] + pc_coffset);
update_flags(r0);
} break;
case OP_LDI: {
uint16_t r0 = (instr >> 9) & 0x7;//取目的寄存器
uint16_t pc_offset = sign_extend(instr & 0x1ff, 9);//符号扩展,第一个参数是该数的位,第二个参数描述长度
reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset));//读取指定内存地址处的值加载入目的寄存器
update_flags(r0);//更新标志寄存器
} break;
case OP_LDR: {
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
reg[r0] = mem_read(reg[r1] + offset);
update_flags(r0);
} break;
case OP_LEA: {
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_coffset = sign_extend(instr & 0x1ff, 9);
reg[r0] = reg[R_PC] + pc_coffset;
update_flags(r0);
} break;
case OP_ST: {
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1ff, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
} break;
case OP_STI: {
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1ff, 9);
mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]);
} break;
case OP_STR: {
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
mem_write(reg[r1] + offset, reg[r0]);
} break;
case OP_TRAP:
reg[R_R7] = reg[R_PC];

switch (instr & 0xFF)
{
case TRAP_GETC:
{
reg[R_R0] = (uint16_t)getchar();
update_flags(R_R0);
}
break;
case TRAP_OUT:
{
putc((char)reg[R_R0], stdout);
fflush(stdout);
}
break;
case TRAP_PUTS:
{
/* one char per word */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
}
break;
case TRAP_IN:
{
printf("Enter a character: ");
char c = getchar();
putc(c, stdout);
fflush(stdout);
reg[R_R0] = (uint16_t)c;
update_flags(R_R0);
}
break;
case TRAP_PUTSP:
{
/* one char per byte (two bytes per word)
here we need to swap back to
big endian format */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
char char1 = (*c) & 0xFF;
putc(char1, stdout);
char char2 = (*c) >> 8;
if (char2) putc(char2, stdout);
++c;
}
fflush(stdout);
}
break;
case TRAP_HALT:
puts("HALT");
fflush(stdout);
running = 0;
break;
}break;
case OP_RES:
case OP_RTI:
default:
abort();
break;
}
}
restore_input_buffering();
}

其实整个虚拟机跟着做完人还处于一种比较懵逼的状态,我写了个虚拟机是干啥的????实操了一下,这个虚拟机和我一开始接触的VMware不一样,这更像是一个翻译机器,我们可以用lc-3的汇编语言编写程序,然后用汇编器生成二进制文件,我们编写的这个程序就可以执行这个二进制文件。比如我们编写

1
2
3
4
5
6
.ORIG X3000 ;.ORIG是伪操作符
LEA R0,string
PUTS
HALT
string .STRINGZ "Hello,word!"
.END

这个汇编程序可以实现将hello word打印在屏幕上。具体的实现步骤

1.首先在模拟器中用汇编语言编写

2.将其保存为.asm文件

3.点击asm创建.obj文件

然后我们就可以通过 ./LC-3\ VM.exe hello.obj来实现

工作流程:

  1. 读取镜像文件,加载到内存
  2. PC寄存器设置为0x3000,指向第一条指令
  3. 重复取指-译码-执行
  4. 遇到HALT指令后,退出模拟,程序结束。

将二进制(机器码)翻译成高级语言,我们可以再套一层虚拟机,将我们的c语言转变成这个汇编。通过这次虚拟机的实现过程,对计算机行为有了更进一步的了解。

Write your Own Virtual Machine (jmeiners.com)