0%

VM虚拟机技术简析

基础知识

虚拟机是一种类似于计算机的程序。它模拟CPU和其他一些硬件组件,允许它执行算术,读写内存,并与I/O设备交互,就像物理计算机一样

虚拟机的核心功能是:理解一种可以用来编程的机器语言

  • 编译器可以通过将标准高级语言编译为多个 CPU 体系结构来解决类似的问题
  • VM 创建一个标准 CPU 体系结构,该体系结构在各种硬件设备上进行模拟
  • 编译器的一个优点是它没有运行时开销,而 VM 有
  • 尽管编译器做得很好,但编写面向多个平台的新编译器非常困难,因此 VM 在这里仍然很有帮助(实际上,VM 和编译器在不同级别混合使用)

虚拟机的其他运用:

  • 垃圾收集:在 C 或 C++ 之上实现自动垃圾回收没有简单的方法,因为程序看不到自己的堆栈或变量,但是虚拟机位于其正在运行的程序的“外部”,可以观察堆栈上的所有内存引用
  • 安全隔离:智能合约是由区块链网络中的每个验证节点执行的小程序,这要求节点操作员在他们的机器上运行由完全陌生的程序,为了防止合约执行恶意操作,所有合约都在无法访问文件系统、网络、磁盘等的VM中运行

在真实机器中,二进制代码是由具体的硬件来执行的,而虚拟机则是用其他语言模拟了这个过程,使该二进制代码可以在虚拟机的控制下执行

LC-3架构

LC-3 与 x86 相比,它具有简化的指令集,但包含现代 CPU 中使用的所有主要思想

  • LC-3 有 65536 个内存位置(可由16位无符号整数寻址的最大位置),每个位置存储一个16位值,这意味着它总共只能存储128KB
  • LC-3 总共有10个寄存器,每个寄存器为16位:
    • 8个通用寄存器 - R0 - R7
    • 1个程序计数器 - PC
    • 1个条件标志寄存器 - COND
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
};
1
uint16_t reg[R_COUNT];
  • LC-3 中只有16个操作码,计算机可以计算的所有内容都是这些简单指令的某个序列,每条指令的长度为16位,剩下的4位存储操作码,其余位用于存储参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 */
};
  • LC-3 仅使用3个条件标志,用于指示先前计算的符号
1
2
3
4
5
6
enum
{
FL_POS = 1 << 0, /* P */
FL_ZRO = 1 << 1, /* Z */
FL_NEG = 1 << 2, /* N */
};

Assembly examples 装配示例

现在,让我们看一个 LC-3 汇编程序,以了解 VM 实际运行的内容,您不需要知道如何对装配进行编程或了解正在发生的一切,只是试着大致了解正在发生的事情:

1
2
3
4
5
6
.ORIG x3000                        ; 这是内存中将加载程序的地址
LEA R0, HELLO_STR ; 将HELLO_STR字符串的地址加载到R0中
PUTs ; 将R0指向的字符串输出到控制台
HALT ; 停止程序
HELLO_STR .STRINGZ "Hello World!" ; 将此字符串存储在程序中
.END ; 标记文件末尾
  • 程序从顶部开始,一次执行一个语句

请注意,某些语句的名称与我们之前定义的操作码匹配,之前,我们了解到每条指令都是16位,但每行看起来都是不同数量的字符

  • 这是因为我们正在阅读的代码是用汇编编写的,汇编是人类可读和可写的形式,以纯文本编码
  • 称为汇编器的工具用于将每行文本转换为 VM 可以理解的16位二进制指令
  • 这种二进制形式本质上是一个16位指令数组,称为机器代码,是 VM 实际运行的内容

Executing programs 程序执行

虚拟机执行的流程为:

  1. 从 PC 寄存器地址的内存中加载一条指令
  2. 递增 PC 寄存器
  3. 查看操作码以确定它应该执行哪种类型的指令
  4. 使用指令中的参数执行指令
  5. 返回到第一步

main 函数整体的构建为:

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
int main(int argc, const char* argv[])
{
@{Load Arguments}
@{Setup}

reg[R_COND] = FL_ZRO;

enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;

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

switch (op)
{
case OP_ADD:
@{ADD}
break;
case OP_AND:
@{AND}
break;
case OP_NOT:
@{NOT}
break;
case OP_BR:
@{BR}
break;
case OP_JMP:
@{JMP}
break;
case OP_JSR:
@{JSR}
break;
case OP_LD:
@{LD}
break;
case OP_LDI:
@{LDI}
break;
case OP_LDR:
@{LDR}
break;
case OP_LEA:
@{LEA}
break;
case OP_ST:
@{ST}
break;
case OP_STI:
@{STI}
break;
case OP_STR:
@{STR}
break;
case OP_TRAP:
@{TRAP}
break;
case OP_RES:
case OP_RTI:
default:
@{BAD OPCODE}
break;
}
}
@{Shutdown}
}
  • 先读取传入 VM 的参数(一个文件的路径)
  • 打开这个文件,并把里面的指定数据传输到 VM 私有内存
  • 设置好 PC 寄存器,然后在循环中分析并执行二进制代码
  • 执行完毕后退出

指令 ADD:

  • 指令 ADD 需要两个数字,将它们相加,并将结果存储在寄存器 DR 中
1
2
[0001][DR][SR1][0][00][SR2]
[0001][DR][SR1][1][imm5]
  • 前4位为指令编码,接下来的3位表示目标寄存器 DR,接下来的3位表示存储有目标数据的寄存器 SR1,接下来1位用于指明“模式”类型(即时模式 / 寄存器模式),最后5位可以代表 SR2 寄存器也可以代表 imm5 立即数
  • 编码显示两行,因为 ADD 指令有两种不同的“模式”:
    • 寄存器模式:把 SR1 和 SR2 中的数据相加然后存储到 DR 中
    • 即时模式:把 SR1 中的数据与 imm5 相加然后存储到 DR 中
  • 符号扩展:ADD 即时模式 imm5 值只有5位,但需要将其添加到16位数字中
    • 对于正数,用“0”填充额外的位(强制类型转换即可)
    • 对于负数,用“1”填充额外的位
1
2
3
4
5
6
7
uint16_t sign_extend(uint16_t x, int bit_count)
{
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}
  • 每当将值写入寄存器时,我们都需要更新标志以指示其符号:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
14
15
16
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);

指令 LDI:

  • LDI 代表“间接加载”,此指令用于将值从内存中的某个位置加载到寄存器中
1
[1010][DR][PCoffset9]
  • 前4位为指令编码,接下来的3位表示目标寄存器 DR,最后9位代表偏移地址,它告诉我们从何处加载
    • 目标地址 = 偏移地址 + PC寄存器的值
  • 指令 LDI 的代码如下:
1
2
3
4
5
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);

接下来的大部分指令都是基于以上这两种格式,具体实现过程就省略了

Trap routines 陷阱例程

LC-3 提供了一些预定义的例程用于执行常见任务和与 I/O 设备交互,这些被称为陷阱例程,可以将其视为 LC-3 的操作系统或API

每个陷阱例程都被分配一个陷阱代码 trap code 来标识它(类似于操作码),如果想要执行一个陷阱,则需要使用所需例程的陷阱代码来调用 TRAP 指令

1
[1111][0000][trapvect8]
  • 前4位为指令编码 [1111],接下来的4位固定为 [0000],最后8位代表陷阱代码 trap code
1
2
3
4
5
6
7
8
9
enum
{
TRAP_GETC = 0x20, /* 从键盘获取字符,不回显到终端上 */
TRAP_OUT = 0x21, /* 输出字符 */
TRAP_PUTS = 0x22, /* 输出一字大小的字符串 */
TRAP_IN = 0x23, /* 从键盘获取字符,回显到终端上 */
TRAP_PUTSP = 0x24, /* 输出一字节大小的字符串 */
TRAP_HALT = 0x25 /* 停止程序 */
};

在官方的 LC-3 模拟器中,陷阱例程是用汇编编写的,调用陷阱代码时,会将 PC 寄存器移动到该代码的地址,CPU 执行陷阱例程的指令,完成后,PC 将重置到初始调用后的位置

  • 程序起始地址为 0x3000,就是为了给陷阱例程腾出空间

尽管陷阱例程可以用汇编形式编写,并且这是物理 LC-3 计算机可以执行的操作,但它并不是最适合 VM 的

与其编写我们自己的原始 I/O 例程,我们可以利用操作系统上可用的例程,这将使 VM 在我们的计算机上更好地运行,简化代码,并为可移植性提供更高级别的抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
reg[R_R7] = reg[R_PC];

switch (instr & 0xFF) /* 获取trap code */
{
case TRAP_GETC:
@{TRAP GETC}
break;
case TRAP_OUT:
@{TRAP OUT}
break;
case TRAP_PUTS:
@{TRAP PUTS}
break;
case TRAP_IN:
@{TRAP IN}
break;
case TRAP_PUTSP:
@{TRAP PUTSP}
break;
case TRAP_HALT:
@{TRAP HALT}
break;
}

例程 PUTS:

  • 陷阱代码用于输出以 NULL 结尾的字符串(类似于 C 中的 puts
  • 要显示字符串,我们必须给陷阱例程一个要显示的字符串,这是通过在开始陷阱之前存储第一个字符的地址 memory 来完成的
1
2
3
4
5
6
7
8
9
10
{
uint16_t* c = memory + reg[R_R0];

while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
}
  • LC-3 中的内存位置为16位,因此字符串中的每个字符的宽度为16位,想要使用C函数显示它,我们需要将每个值转换为 char 并单独输出它们

其他的例程都与之类似,用C代码很好实现

Loading programs 加载程序

当汇编程序转换为机器代码时,结果是一个包含指令和数据数组的文件,这可以通过将内容直接复制到内存中的地址来加载

程序文件的前16位指定程序应启动的内存地址(此地址称为 origin),必须首先读取它,然后从源地址开始将其余数据从文件读取到内存中

以下是将 LC-3 程序读入内存的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void read_image_file(FILE* file)
{
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);

uint16_t max_read = MEMORY_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);

while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}
  • LC-3 程序是大端序的,但大多数现代计算机都是小端序的
  • 因此,我们需要调用 swap16 交换每个加载的内容
1
2
3
4
uint16_t swap16(uint16_t x)
{
return (x << 8) | (x >> 8);
}

Memory mapped registers 内存映射寄存器

某些特殊寄存器无法从普通寄存器表访问,相反,在内存中为他们保留了一个特殊地址,要读取和写入这些寄存器,只需读取和写入它们的内存位置

  • 它们被称为内存映射寄存器 Memory mapped registers,它们通常用于与特殊硬件设备进行交互

LC-3有两个需要实现的 Memory mapped registers:

  • 键盘状态寄存器 KBSR:标识键盘是“按下”还是“弹起”
  • 键盘数据寄存器 KBDR:记录按下的键盘数据
1
2
3
4
5
enum
{
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
};
  • Memory mapped registers 使内存访问稍微复杂一些:
    • 不能直接读取和写入内存数组,而是必须调用 settergetter 函数
    • 当从 KBSR 中读取内存时,getter 将检查键盘并更新两个内存位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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];
}

Platform specifics 平台细节

本节包含访问键盘和表现良好的一些繁琐细节,这些与了解 VM 没有见地或相关(随意复制粘贴)

这些函数应该在主函数上方声明

Linux/macOS/UNIX:

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
struct termios original_tio;

void disable_input_buffering()
{
tcgetattr(STDIN_FILENO, &original_tio);
struct termios new_tio = original_tio;
new_tio.c_lflag &= ~ICANON & ~ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
}

void restore_input_buffering()
{
tcsetattr(STDIN_FILENO, TCSANOW, &original_tio);
}

uint16_t check_key()
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);

struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
return select(1, &readfds, NULL, NULL, &timeout) != 0;
}
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
/* unix only */
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/termios.h>
#include <sys/mman.h>

Windows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();
}
1
2
3
4
5
6
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
/* windows only */
#include <Windows.h>
#include <conio.h> // _kbhit

All platforms:

为了正确处理终端的输入,我们需要调整一些缓冲设置,这些平台的实现因每个平台而异,应该已在上面定义

  • 在 main 中循环开始时写入如下代码:
1
2
signal(SIGINT, handle_interrupt); /* 设置信号处理程序 */
disable_input_buffering(); /* 禁用输入缓冲区 */
  • 如果我们收到结束程序的信号,也应该恢复设置
1
2
3
4
5
6
7
void handle_interrupt(int signal)
{
restore_input_buffering(); /* 将终端设置恢复正常 */
printf("\n");
exit(-2);
}

  • 在 main 中循环结束时写入如下代码:
1
restore_input_buffering();

到目前为止,我们编写的所有内容都应按以下顺序添加到 C 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@{Includes}

@{Registers}
@{Condition Flags}
@{Opcodes}

@{Memory Mapped Registers}
@{TRAP Codes}

@{Memory Storage}
@{Register Storage}

@{Input Buffering}
@{Handle Interrupt}
@{Sign Extend}
@{Swap}
@{Update Flags}
@{Read Image File}
@{Read Image}
@{Memory Access}

@{Main Loop}

Running the VM 启动虚拟机

现在可以构建并运行 LC-3 虚拟机了:

  • 编译虚拟机
1
gcc lc3.c -o lc3-vm
  • 下载 2048 或 Rogue 的组装版本
  • 使用 .obj 文件作为参数运行 VM
1
lc3-vm path/to/2048.obj

调试 Debugging:

如果程序无法正常工作,则可能是因为您错误地编写了指令,调试起来可能很棘手

我建议通读 LC-3 程序的汇编源代码,同时使用调试器逐个执行 VM 指令,读取程序集时,请确保 VM 转到预期指令,如果出现差异,您将知道是哪个指令导致了问题,重新阅读其规范并仔细检查您的代码

Full VM code 完整虚拟机代码

本人只是把如下文章中的代码拼接到了一起

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
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
/* Includes */
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/termios.h>
#include <sys/mman.h>

/* Registers */
enum
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC,
R_COND,
R_COUNT
};

/* Condition Flags */
enum
{
FL_POS = 1 << 0,
FL_ZRO = 1 << 1,
FL_NEG = 1 << 2,
};

/* Opcodes */
enum
{
OP_BR = 0,
OP_ADD,
OP_LD,
OP_ST,
OP_JSR,
OP_AND,
OP_LDR,
OP_STR,
OP_RTI,
OP_NOT,
OP_LDI,
OP_STI,
OP_JMP,
OP_RES,
OP_LEA,
OP_TRAP
};

/* Memory Mapped Registers */
enum
{
MR_KBSR = 0xFE00,
MR_KBDR = 0xFE02
};

/* TRAP Codes */
enum
{
TRAP_GETC = 0x20,
TRAP_OUT = 0x21,
TRAP_PUTS = 0x22,
TRAP_IN = 0x23,
TRAP_PUTSP = 0x24,
TRAP_HALT = 0x25
};

/* Memory Storage */
#define MEMORY_MAX (1 << 16)
uint16_t memory[MEMORY_MAX];

/* Register Storage */
uint16_t reg[R_COUNT];

/* Input Buffering */
struct termios original_tio;

void disable_input_buffering()
{
tcgetattr(STDIN_FILENO, &original_tio);
struct termios new_tio = original_tio;
new_tio.c_lflag &= ~ICANON & ~ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
}

void restore_input_buffering()
{
tcsetattr(STDIN_FILENO, TCSANOW, &original_tio);
}

uint16_t check_key()
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);

struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
return select(1, &readfds, NULL, NULL, &timeout) != 0;
}

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

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

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

/* Update Flags */
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;
}
}

/* Read Image File */
void read_image_file(FILE* file)
{
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);

uint16_t max_read = MEMORY_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);

while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}

/* Read Image */
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;
}

/* Memory Access */
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];
}

/* Key instructions */
void ADD(uint16_t instr)
{
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);
}

void LDI(uint16_t instr)
{
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);
}

void RTI(uint16_t instr)
{
abort();
}

void RES(uint16_t instr)
{
abort();
}

void AND(uint16_t instr)
{
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);
}

void NOT(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;

reg[r0] = ~reg[r1];
update_flags(r0);
}

void BR(uint16_t instr)
{
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;
}
}

void JMP(uint16_t instr)
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1];
}

void JSR(uint16_t instr)
{
uint16_t long_flag = (instr >> 11) & 1;
reg[R_R7] = reg[R_PC];
if (long_flag)
{
uint16_t long_pc_offset = sign_extend(instr & 0x7FF, 11);
reg[R_PC] += long_pc_offset; /* JSR */
}
else
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1]; /* JSRR */
}
}

void LD(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = mem_read(reg[R_PC] + pc_offset);
update_flags(r0);
}

void LDR(uint16_t instr)
{
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);
}

void LEA(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = reg[R_PC] + pc_offset;
update_flags(r0);
}

void ST(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
}

void STI(uint16_t instr)
{
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]);
}

void STR(uint16_t instr)
{
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]);
}

void TRAP(uint16_t instr,int *running)
{
uint16_t* c;
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:
{
c = memory + reg[R_R0];
while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
break;
}
case TRAP_IN:
{
printf("Enter a character: ");
char ch = getchar();
putc(ch, stdout);
fflush(stdout);
reg[R_R0] = (uint16_t)ch;
update_flags(R_R0);
break;
}
case TRAP_PUTSP:
{
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;
}
}
}

/* Main Loop */
int main(int argc, const char* argv[])
{
if (argc < 2)
{
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();

reg[R_COND] = FL_ZRO;

enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;

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

switch (op)
{
case OP_ADD:
ADD(instr);
break;
case OP_AND:
AND(instr);
break;
case OP_NOT:
NOT(instr);
break;
case OP_BR:
BR(instr);
break;
case OP_JMP:
JMP(instr);
break;
case OP_JSR:
JSR(instr);
break;
case OP_LD:
LD(instr);
break;
case OP_LDI:
LDI(instr);
break;
case OP_LDR:
LDR(instr);
break;
case OP_LEA:
LEA(instr);
break;
case OP_ST:
ST(instr);
break;
case OP_STI:
STI(instr);
break;
case OP_STR:
STR(instr);
break;
case OP_TRAP:
TRAP(instr,&running);
break;
case OP_RES:
case OP_RTI:
default:
abort();
break;
}
}
restore_input_buffering();
}

尝试编译该代码并运行目标程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  vm gcc lc3.c -o lc3-vm
➜ vm ./lc3-vm ./2048.obj
Control the game using WASD keys.
Are you on an ANSI terminal (y/n)? n
+--------------------------+
| |
| 2 |
| |
| |
| |
| |
| |
| 2 |
| |
+--------------------------+

Reverse the VM 逆向虚拟机

此虚拟机被编译为了3个版本:

  • 正常版本
  • debug 版本
  • 去符号版本

正常版本和 debug 版本只有一些全局变量的差别:

去符号版本的逆向难度要大一些:

总体的逆向思路就是:

  • 先找到 Switch-Case 循环,并确定每条指令的长度
  • 找到存放寄存器的全局变量,判断各个寄存器个数/位置/功能
  • 分析 Switch-Case 的各个分支,弄懂大概的功能,并了解指令中各个位的用途

在 VM pwn 中要多多注意带有 Read/Write 的指令(重点看有没有溢出)