文章目录

  • 前言
  • 工作原理
  • 实现
    • 一些关键的变量
    • main 函数
    • 解析函数
    • 添加汇编指令

前言

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

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

工作原理

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

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

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

assume cs:code,ds:data,ss:stack     ;将cs,ds,ss分别和code,data,stack段相连
data segmentdw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
data endsstack segmentdw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
stack ends
code segmentstart: mov ax,stackmov ss,axmov sp,20h         ; 将设置栈顶ss:sp指向stack:20mov ax, data        ; 将名称为"data"的段的段地址送入axmov ds,ax          ; ds指向data段mov bx,0           ; ds:bx指向data段中的第一个单元mov cx,8s0: push cs:[bx]add bx,2loop s0             ; 以上将代码段0~15单元总的8个字型数据依次入栈mov bx,0mov cx, 8s1:pop cs:[bx]add bx,2loop s1             ; 以上依次出栈8个字型数据到代码段0~15单元中mov ax,4c00hint 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) []bytetype InstructionOps struct {name   string // 指令名称check  checkHandlerencode 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 实现。

实现 8086 汇编编译器(一)——基本框架相关推荐

  1. 实现8086汇编编译器(三)——jmp指令的翻译

    文章目录 前言 jmp 汇编指令的格式 jmp 机器指令的格式 jmp 指令的翻译 jmp 操作数类型 解析操作数 checkJmp 的实现 encodeJmp 的实现 前言 直接看<汇编语言& ...

  2. c++ 模板类实现堆栈实验报告_编译原理——小型类C编译器的设计和实现(生成8086汇编代码)之1:问题定义以及总体功能...

    前面花了两篇文章来介绍词法分析和语法分析,接下来才是比较有意思的部分--一个小型类C编译器的设计和实现(其实是编译原理的课程设计啦!~)我用的是python2.7.13+PyQt来做的...事实上,正 ...

  3. 8086汇编与c++编译器就内存方面的感想

    8086汇编中可以手动分配栈内存,没有堆内存的概念,而c++编译器中栈是系统分配的,堆是手动分配的.

  4. 8086汇编学习小记-1

    8086汇编学习小记-1 View Code assume cs : codesg, ds : datasg, ss : stacksgdatasg SEGMENT... datasg ENDSsta ...

  5. 8086汇编学习之[BX],CX寄存器与loop指令,ES寄存器等

    同类学习笔记总结: (一).8086汇编学习之基础知识.通用寄存器.CS/IP寄存器与Debug的使用 (二).8086汇编学习之DS寄存器.SS/SP寄存器 一.汇编程序的基本格式: 1.基本格式与 ...

  6. linux汇编编译器:GAS和NASM的比较

    GAS即GNU AS汇编编译器,其属于AT&T风格,我们常用的GNU的产品还有GCC/G++ NASM是Linux平台下常用的汇编编译器,是intel风格的汇编编译器 MASM是Windows ...

  7. 计算机要素--第六章 汇编编译器

    计算机系统要素,从零开始构建现代计算机(nand2tetris) 如果完成了本书所有的项目 你将会获得以下成就 构建出一台计算机(在模拟器上运行) 实现一门语言和相应的语言标准库 实现一个简单的编译器 ...

  8. 【8086汇编基础】05--常用函数库文件--emu8086.inc

    8086汇编语言初学者教程(第5部分) 常用函数库 - emu8086.inc 通过引用一些常用函数,可以使你编程更加方便.在你的程序中使用其他文件中的函数的方法是INCLUDE后面接上你要引用的文件 ...

  9. 【汇编语言】8086汇编字符串定义为何使用DB?其他数据类型不可以吗?(20200515复盘)

    目录 0 前言 0.1 先告诉你结论 1 8086汇编语言中的字符串 1.1 字符串的定义与使用 1.2 直接定义的细节 1.2.1 使用DB数据类型 1.2.2 使用其他数据类型 1.3 直接使用的 ...

  10. 在Windows 10上将C语言程序转成16位8086汇编代码

    大多数人在高校里面学的第一门汇编语言是基于16位的Intel 8086处理器(即8086汇编语言),现在的大多数系统都是32或者64位的,为了实验需要我们一般安装DosBox来作为16位DOS系统模拟 ...

最新文章

  1. 2017-2018-1 我爱学Java 第一周 作业
  2. Redhat 停止sendmail的方法
  3. 自己实现简单的AOP(三) 实现增强四项基本功能
  4. fstab各项参数及ls-l 长格式各项信息
  5. Windows SDK编程之一 窗口示例程序
  6. 运行cudasift
  7. 复现Cell附图 |类器官的单细胞分析
  8. c++ 箭头符号怎么打_C++随笔
  9. mac bochs 调试linux,Mac OS X下编译安装带debugger的bochs
  10. 不当IT民工——技术带来质的飞跃
  11. Deciding the Number of Clusterings
  12. ubuntu18安装tim
  13. 服务器系统开启telnet,开启Telnet服务
  14. 自盲化能力 Paillier和EIGamal
  15. 4.4-软件开发中,“UI设计图”的作用与绘制方法说明
  16. 学生证选课系统c语言大作业,学生选课管理系统c语言程序
  17. 如何使用 forestplot 包绘制森林图展示多个效应的大小
  18. 公司章程违反了公司法该怎么办
  19. migo获取header sap_SAP中migo什么意思
  20. 杰理之省电容MIC收敛值【篇】

热门文章

  1. jvm 字节码jclasslib解读
  2. 电解电容串联的均压电阻计算
  3. linux给普通用户添加管理员权限,linux 赋予普通用户管理员权限
  4. CAD2017下载AutoCAD2017下载安装详细教程
  5. Excel学习日记:L9-图表制作-柱状图
  6. 计算机考研复试题(近十万字)
  7. 软件测试师和网络工程师,【软件测试工程师(科技部)网络工程师面试题目|面试经验】-看准网...
  8. 【Word】快速插入参考文献
  9. 米克科技 - 全新开发悬赏任务APP系统源码平台
  10. windows10 企业版 ltsc系统的激活