0%

《动手写虚拟机》HelloWorld

这篇文章我们来尝试打印出 Hello World

指令设计

虚拟机执行的是指令,所以就要设计有哪些指令,指令需要做什么

就像训狗一样,主人要先搞清楚他想狗子学会并执行哪些指令,比如坐下、躺下、叫等等

下面是我设计的少数几个指令(后面肯定会增加或者修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 所有操作码
// 8位指令后缀是B、16位是S、32位是L、64位是Q
const (
_ OpCode = iota
CONSTQ // CONSTQ 123 局部变量入栈帧
ADDQ // ADDQ pop出两个值,相加,存入栈帧
SUBQ // SUBQ pop出两个值,前-后,存入栈帧
MULQ // MULQ pop出两个值,前*后,存入栈帧
DIVQ // DIVQ pop出两个值,前/后,存入栈帧
JMP // JMP 6 跳到6的位置执行指令
CALL // CALL 6 2 跳到6位置执行,pop出2个参数。返回值入栈,返回值个数入栈
RET // RET 跳转到返回地址执行
// 下面是预设函数
PRINT // PRINT(d interface{}) 打印参数
HALT // HALT() 退出进程
)

虚拟机执行指令

指令设计完了之后,下一步当然是训练狗狗,这里虚拟机跟狗子不一样,狗子是靠后天条件反射,而我们虚拟机是硬编码的,虚拟机的代码一旦写好,指令的执行动作就固化了

这一块代码的编写便是实现并固化虚拟机对各个指令的执行动作

这里先实现了打印Hello World所需要的几个指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (vm *Vm) Run() {
for {
instruction := vm.fetchInstruction()
switch instruction.opCode {
case CONSTQ:
currentStackFrame := vm.stack.GetTopStackFrame()
if len(instruction.args) < 1 {
panic(fmt.Errorf("instruction error - %v", instruction))
}
currentStackFrame.Push(instruction.args[0])
case PRINT:
currentStackFrame := vm.stack.GetTopStackFrame()
fmt.Println(currentStackFrame.Pop().data)
case HALT:
return
}
vm.stepInstruction()
}
}

词法分析器

上面虚拟机完成了几个指令的执行动作。等待所有指令的动作编写完成,虚拟机其实已经完成了

但虚拟机其实只是一门编程语言设计的冰山一角

指令对人来讲并不是友好的,你要给虚拟机传递指令是非常动脑经的事情

所以流程应该是:人跟写作文一样写出一段英语 -> 经过某个东西进行翻译 -> 得到很多指令 -> 交给虚拟机

现代高级语言正是这个流程,其中的翻译的东西就是编译器,编译器才是重点而又复杂的一块

这里我们实现一个非常简单的编译器(仅仅有编译器前端中的词法分析)

可以将下面代码翻译成指令

1
2
3
CONSTQ 'Hello World'  // 定义常量
PRINT
halt

下面是主要逻辑

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
func (lexer *Lexer) NextToken() Token {
//fmt.Println(lexer.currentPosition, fmt.Sprintf("--%c--", rune(lexer.currentChar)))
var token Token
lexer.skipSpace()

if lexer.currentChar == '"' || lexer.currentChar == '\'' {
lexer.readChar()
token.StartPosition = lexer.currentPosition
token.Literal = lexer.readString()
token.Type = TokenType_STRING
token.EndPosition = lexer.currentPosition
token.LineNumber = lexer.currentLine
} else if lexer.currentChar == '/' && lexer.nextChar() == '/' {
lexer.readChar() // 读之前要读两次,把//读掉
lexer.readChar()
lexer.skipSpace()
token.StartPosition = lexer.currentPosition
token.Literal = lexer.readComment()
token.Type = TokenType_COMMENT
token.EndPosition = lexer.currentPosition
token.LineNumber = lexer.currentLine
} else if lexer.currentChar == '\r' || lexer.currentChar == '\n' {
token.StartPosition = lexer.currentPosition
token.Literal = "EOL"
token.Type = TokenType_EOL
token.EndPosition = lexer.currentPosition
token.LineNumber = lexer.currentLine
lexer.readChar()
lexer.currentLine++
} else if isLetter(lexer.currentChar) {
token.StartPosition = lexer.currentPosition
token.Literal = lexer.readInstruction()
tokenType, ok := StringToTokenType[strings.ToUpper(token.Literal)]
if !ok {
panic(fmt.Errorf("illegal keyword - line: %d, position: %d, keyword: %s", lexer.currentLine, lexer.currentPosition, token.Literal))
}
token.Type = tokenType
token.EndPosition = lexer.currentPosition
token.LineNumber = lexer.currentLine
} else if lexer.currentChar == 0 {
token.StartPosition = lexer.currentPosition
token.Literal = "EOF"
token.Type = TokenType_EOF
token.EndPosition = lexer.currentPosition
token.LineNumber = lexer.currentLine
} else {
panic(fmt.Errorf("illegal char - line: %d, position: %d, char: %c", lexer.currentLine, lexer.currentPosition, lexer.currentChar))
}

//fmt.Printf("找到token: %v\n", token)
return token
}

Hello World

简易的编译器完成之后,我就可以写一段代码,然后直接递给虚拟机了,虚拟机会进行编译然后执行

1
make
1
2
3
4
5
./build/bin/darwin/govm "
CONSTQ 'Hello World'
PRINT
halt
"

可以看到打印出 Hello World

开源地址

go-vm

下篇预告

加减乘除




微信关注我,及时接收最新技术文章