实现 8086 汇编编译器(一)——基本框架

前言

大半年前重温了王爽老师的《汇编语言》,加之正在学习 GO 语言,想练练手。于是用 GO 实现了一个8086 汇编编译器,用这个可以将汇编语言程序编译成可执行程序。然后又实现了一个 8086 虚拟机加载并执行这个程序。

现在就写一些文章来介绍我是如何实现这个简单的 8086 编译器和虚拟机的。

工作原理

编译器的输入是一个汇编文件,输出是一个可执行程序【能够被 8086 虚拟机执行】,它主要做两件事:

  1. 将汇编指令翻译【编码】成机器指令
  2. 加入如一些必要的程序头信息,使得这些机器指令能够被执行【使它成为一个可执行程序】

以书中如下的汇编程序作为示例说明如何实现一个编译器:

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
  1. 这个汇编程序定义了数据段,堆栈段,代码段。这些段是使用伪指令assumeXXX segmentXXX ends 定义的。编译器要能识别并处理这些伪指令。

  2. 在数据段和堆栈段,使用了伪指令dw定义了一些数据。编译器要能识别这个伪指令,将它定义的数据转换成二进制。

  3. 代码段中包含了 start,s0,s1 三个标号。标号的值是相对代码段的偏移量。特殊的标号start表示的是程序入口,在这个程序中它的值是 0。标号出现在一行的开头【下文中称为外部标号】或者在某个指令的操作数中【比如 "loop s0"这行,下文中称为内部标号】。编译器要能识别并处理这些标号。

  4. 代码段里包含了movpushaddloop 等汇编指令。编译器要能识别这些汇编指令并将它们翻译成机器指令。

  5. 特殊的伪指令end,表示汇编程序的结束。

所以编译器的基本工作原理是一行一行扫描汇编程序源文件,处理其中的伪指令,标号,汇编指令。

实现

一些关键的变量

  1. 处理伪指令 assume,我们需要定义一个map:
var segMap = map[string]string{}

记录段名称和对应的段。比如这个程序中"code" 对应"cs"。接下来在扫描到XXX segment伪指令时,就用 XXX 查找这个 map,来确定伪指令定义的到底是哪个段。

  1. 处理标号,我们需要定义一个map:
var labelMap = map[string]uint16{}

记录标号名称和它相对代码段的偏移量。

我们还要定义一个变量来表示相对代码段的偏移量:

var codeOffset uint16 // 代码段偏移量

这个变量在处理“code segment”伪指令时被初始化为 0。在处理一条汇编指令后,它的值增加这条汇编指令对应的机器指令的长度。比如“mov ax,stack”这条汇编指令翻译成机器指令后是 3 个字节,那么 codeOffset 的值增加 3。

  1. 接下来还需要定义如下几个变量:
var progOffset uint32 // 程序偏移量
var codeEntryOffset uint32 // 代码段中程序入口的偏移量
var codeSegProgOffset  uint32 // 代码段在程序中的偏移量
var dataSegProgOffset  uint32 // 数据段在程序中的偏移量
var stackSegProgOffset uint32 // 堆栈段在程序中的偏移量

progOffset 变量初始为 0。

  1. 在处理dw等伪指令后,它的值增加定义的数据长度。比如在处理第三行的 dw 伪指令后【它定义了16个字节的数据】,progOffset 值变为 16。
  2. 在处理一条汇编指令后,它的值增加这条汇编指令对应的机器指令的长度。比如“mov ax,stack”这条汇编指令翻译成机器指令后是 3 个字节,那么 progOffset 的值增加 3。

codeSegProgOffset 变量在处理“code segment”伪指令时被赋值为 progOffset 变量的值。

dataSegProgOffset 变量在处理“data segment”伪指令时被赋值为 progOffset 变量的值。

stackSegProgOffset 变量在处理“stack segment”伪指令时被赋值为 progOffset 变量的值。

codeEntryOffset 变量在处理“start”标号时被赋值为 codeOffset 变量的值。

  1. 编译器输出的可执行程序,用一个字节切片表示:
var program []byte

还需要定义一些关键的数据结构和变量,下文会提到。

main 函数

func main() {
    // 打开汇编程序源文件
	file, err := os.Open("program.S")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
    // 遍历每一行
	for scanner.Scan() {
		s := scanner.Text()
		// 去掉两边空白字符
		s = strings.TrimSpace(s)
        // 统一转成小写格式
		s = strings.ToLower(s)
		// 忽略注释
		if idx := strings.IndexRune(s, ';'); idx >= 0 {
			s = strings.TrimSpace(s[:idx])
		}

        // 解析每一行
		if len(s) > 0 {
			parse(s)
		}
	}

	if err := scanner.Err(); err != nil {
		log.Fatal(err)
	}
}

解析函数

func parse(s string) {
	if strings.HasPrefix(s, "assume") { // 处理 assume 伪指令
		//
	} else if strings.HasSuffix(s, " segment") ||
		strings.HasSuffix(s, " ends") { // 处理定义段的伪指令
		//
	} else if strings.HasPrefix(s, "end") { // 处理定义程序结束的伪指令
		//
	} else if strings.HasPrefix(s, "db ") ||
		strings.HasPrefix(s, "dw ") ||
		strings.HasPrefix(s, "dd ") { // 处理定义数据的伪指令
		data := parseDB(s)
		program = append(program, data...)
		progOffset += uint32(len(data))
	} else { // 处理汇编指令
		stat := parseLabelField(s)
		ops := FindInstruction(stat[0])
		if ops == nil {
			fmt.Printf("unsuppored instruction: \"%s\"", stat[0])
			return
		}
        // 将汇编指令翻译成机器指令
        instruction := ops.Do(stat)
		program = append(program, instruction...)
		progOffset += uint32(len(instruction))
		codeOffset += uint16(len(instruction))
	}
}
  1. 看下对db、dw、dd等伪指令的处理:

parseDB 函数返回dw伪指令定义的数据。就是返回一个 []byte。具体实现就是解析字符串,将字符串定义的数转换为整数,没啥好说的。

然后将这些数据加到 program 切片中,这即是程序中定义的数据段。

再更新程序偏移量 progOffset。

  1. 对汇编指令的处理也类似:

将翻译后的机器指令追加到 program 切片中,但是除了更新 progOffset 变量外,还要更新 codeOffset 变量,因为一般只有代码段中才包含汇编指令。

parseLabelField 函数的功能就是分离出汇编语句中的外部标号,汇编指令,汇编指令操作数,并对外部标号做处理。它返回一个[]string。第一个元素是指令名称,后面的元素指令的操作数。

比如“start: mov ax,stack”这条汇编语句,它返回[“mov”, “ax“,”stack”]。

“push cs:[bx]”这条汇编语句,它返回[“push”,“cs:[bx]”]。

它的实现就是一些字符串的处理,这里就不说了。

添加汇编指令

定义 InstructionOps 结构体表示一个汇编指令,调用它的 Do 方法将汇编指令翻译成机器指令:

type checkHandler func([]string) (bool, context.Context)

type encodeHandler func(context.Context) []byte

type InstructionOps struct {
	name   string // 指令名称
	check  checkHandler
	encode encodeHandler
}

// 将汇编指令翻译成机器指令
func (ops *InstructionOps) Do(stat []string) []byte {
	t, ctx := ops.check(stat)
	if t {
		instruction := ops.encode(ctx)
		return instruction
	}
	return nil
}

也就是说一个汇编指令需要实现两个 handler:

  1. check handler,检查汇编指令格式是否正确。如果正确,将操作数保存到 ctx 中。
  2. encode handler,从 ctx 获取操作数,将指令翻译成机器码。返回 []byte。

定义了一个 map 类型的 instructionTable 变量保存添加的指令。调用 AddInstruction 函数添加指令。调用 FindInstruction 函数查找指令:

var instructionTable = make(map[string]*InstructionOps)

func AddInstruction(Name string, CheckHandler checkHandler, EncodeHandler encodeHandler) {
	if _, ok := instructionTable[Name]; !ok {
		instructionTable[Name] = &InstructionOps{Name, CheckHandler, EncodeHandler}
	} else {
		log.Fatalf("duplicate instruction \"%s\"\n", Name)
	}
}

func FindInstruction(Name string) *InstructionOps {
	if v, ok := instructionTable[Name]; ok {
		return v
	}
	return nil
}

比如添加mov指令,可以新建一个文件 encode_jmp.go,然后:

func init() {
	AddInstruction("mov", checkMov, encodeMov)
}

从下一篇文章开始介绍几个常见指令比如movjmp等指令的 check handler 和 encode handler 实现。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值