文章目录
- 基本框架
- 几点说明:
在 实现8086汇编编译器(四)——生成可执行程序 一文中,我已经实现了一个编译器,可以将汇编语言汇编成二进制程序。
这几篇文章来讲述如何实现虚拟机,也就是执行这个程序的“机器”【它也是一个程序】。这个“机器”的输入是一个二进制程序,以如下汇编程序为例:
assume cs:code,ds:data,ss:stack ;将cs,ds,ss分别和code,data,stack段相连
data segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
data ends
stack segment
dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,20h ; 将设置栈顶ss:sp指向stack:20
mov ax, data ; 将名称为"data"的段的段地址送入ax
mov ds,ax ; ds指向data段
mov bx,0 ; ds:bx指向data段中的第一个单元
mov cx,8
s0: push cs:[bx]
add bx,2
loop s0 ; 以上将代码段0~15单元总的8个字型数据依次入栈
mov bx,0
mov cx, 8
s1:pop cs:[bx]
add bx,2
loop s1 ; 以上依次出栈8个字型数据到代码段0~15单元中
mov ax,4c00h
int 21h
code ends
end start
汇编之后对应的二进制程序如下:
root@ubuntu:~/gogo/demo# hexdump a.exec
0000000 0030 0000 0000 0000 0000 0000 0010 0000
0000010 0002 0000 0031 0000 0210 0039 0000 0110
0000020 0000 0000 0000 0000 0000 0000 0000 0000
*
0000100 0123 0456 0789 0abc 0def 0fed 0cba 0987
0000110 0000 0000 0000 0000 0000 0000 0000 0000
*
0000130 00b8 8e00 bcd0 0020 00b8 8e00 bbd8 0000
0000140 08b9 2e00 37ff c381 0002 f7e2 00bb b900
0000150 0008 8f2e 8107 02c3 e200 b8f7 4c00 21cd
0000160
那么这个机器要执行这个程序的话,主要分为几大步:
- 读取二进制程序,识别出程序头和程序
- 根据程序头中的信息,设定一个程序要加载到内存中的地址,并根据程序头中的重定位信息修改程序
- 将程序写入内存【需要用程序模拟一个内存芯片】中
- 设置 CPU 【需要用程序模拟一个CPU】寄存器 CS 和 IP 的值,让它开始执行程序
基本框架
基本框架代码如下:
// 1. 打开并读取二进制文件
f, err := os.Open("./a.exec")
if err != nil {
log.Fatal("Open error:", err)
}
stat, err := f.Stat()
if err != nil {
log.Fatal("Stat error:", err)
}
b := make([]byte, stat.Size())
n, err := f.Read(b)
if err != nil {
log.Fatal("Read error:", err)
}
// 2. 读取程序头部
buf := bytes.NewBuffer(b[:programHeaderLen])
programHeader := ProgramHeader{}
binary.Read(buf, binary.LittleEndian, &programHeader.codeSegProgOffset)
binary.Read(buf, binary.LittleEndian, &programHeader.codeEntryProgOffset)
binary.Read(buf, binary.LittleEndian, &programHeader.dataSegProgOffset)
binary.Read(buf, binary.LittleEndian, &programHeader.stackSegProgOffset)
binary.Read(buf, binary.LittleEndian, &programHeader.relocationLen)
programHeader.relocationInfos = make([]RelocationInfo, programHeader.relocationLen)
err = binary.Read(buf, binary.LittleEndian, programHeader.relocationInfos)
if err != nil {
log.Fatal("binary.Read error: ", err)
}
// 3. 假设代码段的段地址为0x1000,根据重定位信息修改程序
program := b[programHeaderLen:]
var cs int16 = 0x1000
for _, v := range programHeader.relocationInfos {
switch v.Type {
case CodeSegementRelocation:
program[v.Offset] = byte(cs)
program[v.Offset+1] = byte(cs >> 8)
case DataSegementRelocation:
ds := cs + int16((programHeader.dataSegProgOffset-programHeader.codeSegProgOffset)>>4)
program[v.Offset] = byte(ds)
program[v.Offset+1] = byte(ds >> 8)
case StackSegementRelocation:
ss := cs + int16((programHeader.stackSegProgOffset-programHeader.codeSegProgOffset)>>4)
program[v.Offset] = byte(ss)
program[v.Offset+1] = byte(ss >> 8)
}
}
// 4. 初始化一个CPU和内存芯片
// 初始化一个内存芯片,大小为1M
m := Memory{}
m.Init(1 << 20)
// 初始化一个CPU
c := CPU{}
c.Init()
// 将CPU与内存相连
c.ConnectMemory(&m)
// 5. 将程序写入内存
// 计算出程序在内存中的起始地址
var phyAddr uint32 = uint32(cs)<<4 - programHeader.codeSegProgOffset
// 将程序加载到内存
c.writeMemory(phyAddr, program)
// 6. CPU 开始执行程序
// 第一个参数是CPU开始执行时CS寄存器的值,第二个参数是IP寄存器的值
c.Run(uint16(cs), uint16(programHeader.codeEntryProgOffset))
几点说明:
- 第3条注释。设置代码段的段地址为 0x1000【偏移地址为0】,也就是程序的代码段将被加载到内存偏移量为 64K 的空间【内存地址是0x10000】。这个值是任意定的,但要求不能是CPU保留地址空间。代码段的段地址确定后,就可以根据重定位信息计算出数据段和堆栈段的段地址。
- 第5条注释。由于我们规定代码段的内存地址是0x10000,而代码段在程序中也有偏移量【48】,所以0x10000减去这个偏移量得到程序在内存中的起始地址。
- 6条注释。当我们将程序加载到内存后,此时代码段内存地址是0x10000,对应CS=0x1000,IP=0,但是程序入口地址也有偏移量【在这个示例程序中偏移量为0】,所以要将IP设置为程序入口偏移量。
这个程序在内存中的布局如下图所示:
后文讲述如何实现CPU和内存芯片。