🖥️ 自制虚拟机 - 概念和汇编器
Conmajia 🇨🇳 © 2012
Alan Bryan 🇺🇸 © 2012
共同完成
Updated on Feb. 19, 2018
1 虚拟机基础
这篇文章是我自制虚拟机系列文章的第一部分. 这个系列将从零开始
虚拟机和模拟器在概念上有重叠
我指的是软件模拟的仿真计算机. ,
虚拟机是一种模拟硬件环境的中间件
- 兼容性
- 隔离
- 封装
- 独立于硬件
2 从一款简单的 CPU 开始
虚拟机来自于对实际硬件的虚拟. 我使用了 SunnyApril 处理器. 这是一款虚构的 16bit 处理器0x0000
~0xffff
.
接下来为 SunnyApril 添加寄存器. 寄存器是具有有限存贮容量
商业微处理器内部往往包含数十至数百个寄存器A
B
D
X
和 Y
. A
B
寄存器是 8 位寄存器0x00
~0xff
的无符号数或是 0x80
~0x7f
的有符号数. X
Y
和 D
寄存器都是 16 位的0x0000
~0xffff
的无符号数或是 0x8000
~0x7fff
的有符号数. 同样是为了设计简便
D
寄存器是一个特殊的 16 位寄存器. 它的值是由 A
B
寄存器的值合并而成A
保存了 D
的高 8 位值B
保存了低 8 位值. 例如 A
寄存器值为 0x3c
B
寄存器值为 0x10
D
寄存器值为 0x3c10
. 反之D
寄存器值为 0x07c0
A
寄存器值变为 0x07
B
寄存器值变为 0xc0
. 图 1 说明了寄存器之间的关系.

为了让虚拟机能在第一时间反馈运行结果0xa000
~0xafa0
接下来的工作就是设计能让虚拟机运行起来的指令集和字节码n
指令 | 字节码 | 操作数 | 功能 | 操作 | 示例 | 运行结果 |
---|---|---|---|---|---|---|
LDA |
0x01 |
Ra, K | 将数据存入 A 寄存器 |
RaK | LDA #41H |
A=0x41 |
LDX |
0x02 |
Rx, K | 将数据存入 X 寄存器 |
RxK | LDX #1000H |
X=0x1000 |
STA |
0x03 |
Ra, Rx | 将 A 寄存器的值存入操作数指定的内存地址 |
(Rx)Ra | STA X |
[0x1000]=0x41 |
END |
0x04 |
结束程序 |
END LABEL |
以 LDA
指令为例A
寄存器. 由于操作数寻址方式太多#
符号起头H
结尾的数字表示十六进制O
B
D
END
指令标记程序结束. 它的操作数称为标签
程序 1 标签
LABEL:
接下来设计编译后的二进制文件格式. 大部分编译器的二进制文件格式都是以一串魔术字字符串开头的. 例如MZ
开头. Java 二进制文件用 4 字节的数字 3405691582 开头0xCAFEBABE
CONMAJIA
作为魔术字
魔术字 | 程序长度 | 执行地址 | 偏移段 | |
---|---|---|---|---|
7 | 2 | 2 | 2 | [] |
文件体紧跟于头部
3 汇编器
汇编器将编写的汇编源程序编译后输出到可以供虚拟机运行的二进制字节码文件中
[标签:]
<指令> <操作数>
方括号中的内容是可选的
下面是一个例子
程序 2 示例源代码
1 START:
2 LDA #65
3 LDX #A000H
4 STA X
5 END START
这个程序的功能是把字符“A”输出到屏幕的左上角.
第一行代码定义了 START
标签. 第二行将立即数 65
A
寄存器. 第三行将立即数 0xa000
X
寄存器. 第四行代码将 A
寄存器中的值存入 X
寄存器中的数值指向的显存地址. 最后用 END
指令结束程序.
3.1 实现汇编器
汇编器界面如图 2 所示

寄存器枚举
程序 3 Registers 枚举
1 enum Registers
2 {
3 Unknown = 0,
4 A = 4,
5 B = 2,
6 D = 1,
7 X = 16,
8 Y = 8
9 }
汇编器的核心代码
程序 4 SunnyApril 汇编器图形界面代码
1 if (textBox1.Text == string.Empty)
2 return;
3
4 labelDict.Clear();
5 binaryLength = (UInt16)numericUpDown1.Value;
6
7 FileInfo fi = new FileInfo(textBox1.Text);
8
9 BinaryWriter output;
10 FileStream fs = new FileStream(
11 Path.Combine(
12 fi.DirectoryName,
13 fi.Name + ".sab"),
14 FileMode.Create
15 );
16 output = new BinaryWriter(fs);
17
18 // magic word
19 output.Write('C');
20 output.Write('O');
21 output.Write('N');
22 output.Write('M');
23 output.Write('A');
24 output.Write('J');
25 output.Write('I');
26 output.Write('A');
27
28 // org
29 output.Write((UInt16)numericUpDown1.Value);
30
31 // scan to ORG and start writing byte-code
32 output.Seek((int)numericUpDown1.Value, SeekOrigin.Begin);
33
34 // parse source code line-by-line
35 TextReader input = File.OpenText(textBox1.Text);
36 string line;
37 while ((line = input.ReadLine()) != null)
38 {
39 parse(line.ToUpper(), output);
40 dealedSize += line.Length;
41 Invoker.Set(progressBar1, "Value", (int)((float)dealedSize / (float)totalSize * 100));
42 }
43 input.Close();
44
45 // binary length & execution address (7 magic-word, 2 org before)
46 output.Seek(10, SeekOrigin.Begin);
47 output.Write(binaryLength);
48 output.Write(executionAddress);
49 output.Close();
50 fs.Close();
源代码解析器
parse()
函数用于对源代码逐行解析
程序 5 parse()
函数代码
1 private void parse(string line, BinaryWriter output)
2 {
3 // eat white spaces and comments
4 line = cleanLine(line);
5 if (line.EndsWith(":"))
6 // label
7 labelDict.Add(line.TrimEnd(new char[] { ':' }), binaryLength);
8 else
9 {
10 // code
11 Match m = Regex.Match(line, @"(\w+)\s(.+)");
12 string opcode = m.Groups[1].Value;
13 string operand = m.Groups[2].Value;
14
15 switch (opcode)
16 {
17 case "LDA":
18 output.Write((byte)0x01);
19 output.Write(getByteValue(operand));
20 binaryLength += 2;
21 break;
22 case "LDX":
23 output.Write((byte)0x02);
24 output.Write(getWordValue(operand));
25 binaryLength += 3;
26 break;
27 case "STA":
28 output.Write((byte)0x03);
29 // NOTE: No error handling.
30 Registers r = (Registers)Enum.Parse(typeof(Registers), operand);
31 output.Write((byte)r);
32 binaryLength += 2;
33 break;
34 case "END":
35 output.Write((byte)0x04);
36 if (labelDict.ContainsKey(operand))
37 {
38 output.Write(labelDict[operand]);
39 binaryLength += 2;
40 }
41 binaryLength += 1;
42 break;
43 default:
44 break;
45 }
46 }
47 }
其中用到了读取字节操作数的内部方法
程序 6 读取字节函数代码
1 private byte getByteValue(string operand)
2 {
3 byte ret = 0;
4 if (operand.StartsWith("#"))
5 {
6 operand = operand.Remove(0, 1);
7 char last = operand[operand.Length - 1];
8 if (char.IsLetter(last))
9 switch (last)
10 {
11 case 'H':
12 // hex
13 ret = Convert.ToByte(operand.Remove(operand.Length - 1, 1), 16);
14 break;
15 case 'O':
16 // oct
17 ret = Convert.ToByte(operand.Remove(operand.Length - 1, 1), 8);
18 break;
19 case 'B':
20 // bin
21 ret = Convert.ToByte(operand.Remove(operand.Length - 1, 1), 2);
22 break;
23 case 'D':
24 // dec
25 ret = Convert.ToByte(operand.Remove(operand.Length - 1, 1), 10);
26 break;
27 }
28 else
29 ret = byte.Parse(operand);
30 }
31
32 return ret;
33 }
3.2 运行结果
运行汇编器demo1.asm
文件进行汇编demo1.sab
二进制字节码文件

demo1.sab
可执行文件内容汇编器正确计算了文件大小0x0200
位置处开始01 00 02 00 00 03 10 04 00 02
.
3.3 验证
下面根据程序 2 的源代码
第一行为 START
标签0x0200
存入缓存
第二行 LDA
指令0x01
A
寄存器是 8 位寄存器0x41
.
第三行 LDX
指令0x02
X
寄存器是 16 位寄存器0xa000
. 由于 SunnyApril 采用低位在前的小端模式00 A0
的形式存储的.
第四行 STA
指令0x03
Registers.X
枚举值
第五行 END
指令0x04
START
标签地址 0x0200
至此
4 接下来的工作
在下一章中
The End.

if(jQuery('#no-reward').text() == 'true') jQuery('.bottom-reward').addClass('hidden');
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2017-02-21 字符型液晶屏模拟控件(En)