实现 8086 汇编编译器(一)——基本框架
文章目录
- 前言
- 工作原理
- 实现
- 一些关键的变量
- main 函数
- 解析函数
- 添加汇编指令
前言
大半年前重温了王爽老师的《汇编语言》,加之正在学习 GO 语言,想练练手。于是用 GO 实现了一个8086 汇编编译器,用这个可以将汇编语言程序编译成可执行程序。然后又实现了一个 8086 虚拟机加载并执行这个程序。
现在就写一些文章来介绍我是如何实现这个简单的 8086 编译器和虚拟机的。
工作原理
编译器的输入是一个汇编文件,输出是一个可执行程序【能够被 8086 虚拟机执行】,它主要做两件事:
- 将汇编指令翻译【编码】成机器指令
- 加入如一些必要的程序头信息,使得这些机器指令能够被执行【使它成为一个可执行程序】
以书中如下的汇编程序作为示例说明如何实现一个编译器:
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
这个汇编程序定义了数据段,堆栈段,代码段。这些段是使用伪指令
assume
,XXX segment
,XXX ends
定义的。编译器要能识别并处理这些伪指令。在数据段和堆栈段,使用了伪指令
dw
定义了一些数据。编译器要能识别这个伪指令,将它定义的数据转换成二进制。代码段中包含了 start,s0,s1 三个标号。标号的值是相对代码段的偏移量。特殊的标号
start
表示的是程序入口,在这个程序中它的值是 0。标号出现在一行的开头【下文中称为外部标号】或者在某个指令的操作数中【比如 "loop s0"这行,下文中称为内部标号】。编译器要能识别并处理这些标号。代码段里包含了
mov
,push
,add
,loop
等汇编指令。编译器要能识别这些汇编指令并将它们翻译成机器指令。特殊的伪指令
end
,表示汇编程序的结束。
所以编译器的基本工作原理是一行一行扫描汇编程序源文件,处理其中的伪指令,标号,汇编指令。
实现
一些关键的变量
- 处理伪指令
assume
,我们需要定义一个map:
var segMap = map[string]string{}
记录段名称和对应的段。比如这个程序中"code" 对应"cs"。接下来在扫描到XXX segment
伪指令时,就用 XXX 查找这个 map,来确定伪指令定义的到底是哪个段。
- 处理标号,我们需要定义一个map:
var labelMap = map[string]uint16{}
记录标号名称和它相对代码段的偏移量。
我们还要定义一个变量来表示相对代码段的偏移量:
var codeOffset uint16 // 代码段偏移量
这个变量在处理“code segment”伪指令时被初始化为 0。在处理一条汇编指令后,它的值增加这条汇编指令对应的机器指令的长度。比如“mov ax,stack”这条汇编指令翻译成机器指令后是 3 个字节,那么 codeOffset 的值增加 3。
- 接下来还需要定义如下几个变量:
var progOffset uint32 // 程序偏移量
var codeEntryOffset uint32 // 代码段中程序入口的偏移量
var codeSegProgOffset uint32 // 代码段在程序中的偏移量
var dataSegProgOffset uint32 // 数据段在程序中的偏移量
var stackSegProgOffset uint32 // 堆栈段在程序中的偏移量
progOffset 变量初始为 0。
- 在处理
dw
等伪指令后,它的值增加定义的数据长度。比如在处理第三行的 dw 伪指令后【它定义了16个字节的数据】,progOffset 值变为 16。 - 在处理一条汇编指令后,它的值增加这条汇编指令对应的机器指令的长度。比如“mov ax,stack”这条汇编指令翻译成机器指令后是 3 个字节,那么 progOffset 的值增加 3。
codeSegProgOffset 变量在处理“code segment”伪指令时被赋值为 progOffset 变量的值。
dataSegProgOffset 变量在处理“data segment”伪指令时被赋值为 progOffset 变量的值。
stackSegProgOffset 变量在处理“stack segment”伪指令时被赋值为 progOffset 变量的值。
codeEntryOffset 变量在处理“start”标号时被赋值为 codeOffset 变量的值。
- 编译器输出的可执行程序,用一个字节切片表示:
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))}
}
- 看下对db、dw、dd等伪指令的处理:
parseDB 函数返回dw
伪指令定义的数据。就是返回一个 []byte。具体实现就是解析字符串,将字符串定义的数转换为整数,没啥好说的。
然后将这些数据加到 program 切片中,这即是程序中定义的数据段。
再更新程序偏移量 progOffset。
- 对汇编指令的处理也类似:
将翻译后的机器指令追加到 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:
- check handler,检查汇编指令格式是否正确。如果正确,将操作数保存到 ctx 中。
- 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)
}
从下一篇文章开始介绍几个常见指令比如mov
,jmp
等指令的 check handler 和 encode handler 实现。
实现 8086 汇编编译器(一)——基本框架相关推荐
- 实现8086汇编编译器(三)——jmp指令的翻译
文章目录 前言 jmp 汇编指令的格式 jmp 机器指令的格式 jmp 指令的翻译 jmp 操作数类型 解析操作数 checkJmp 的实现 encodeJmp 的实现 前言 直接看<汇编语言& ...
- c++ 模板类实现堆栈实验报告_编译原理——小型类C编译器的设计和实现(生成8086汇编代码)之1:问题定义以及总体功能...
前面花了两篇文章来介绍词法分析和语法分析,接下来才是比较有意思的部分--一个小型类C编译器的设计和实现(其实是编译原理的课程设计啦!~)我用的是python2.7.13+PyQt来做的...事实上,正 ...
- 8086汇编与c++编译器就内存方面的感想
8086汇编中可以手动分配栈内存,没有堆内存的概念,而c++编译器中栈是系统分配的,堆是手动分配的.
- 8086汇编学习小记-1
8086汇编学习小记-1 View Code assume cs : codesg, ds : datasg, ss : stacksgdatasg SEGMENT... datasg ENDSsta ...
- 8086汇编学习之[BX],CX寄存器与loop指令,ES寄存器等
同类学习笔记总结: (一).8086汇编学习之基础知识.通用寄存器.CS/IP寄存器与Debug的使用 (二).8086汇编学习之DS寄存器.SS/SP寄存器 一.汇编程序的基本格式: 1.基本格式与 ...
- linux汇编编译器:GAS和NASM的比较
GAS即GNU AS汇编编译器,其属于AT&T风格,我们常用的GNU的产品还有GCC/G++ NASM是Linux平台下常用的汇编编译器,是intel风格的汇编编译器 MASM是Windows ...
- 计算机要素--第六章 汇编编译器
计算机系统要素,从零开始构建现代计算机(nand2tetris) 如果完成了本书所有的项目 你将会获得以下成就 构建出一台计算机(在模拟器上运行) 实现一门语言和相应的语言标准库 实现一个简单的编译器 ...
- 【8086汇编基础】05--常用函数库文件--emu8086.inc
8086汇编语言初学者教程(第5部分) 常用函数库 - emu8086.inc 通过引用一些常用函数,可以使你编程更加方便.在你的程序中使用其他文件中的函数的方法是INCLUDE后面接上你要引用的文件 ...
- 【汇编语言】8086汇编字符串定义为何使用DB?其他数据类型不可以吗?(20200515复盘)
目录 0 前言 0.1 先告诉你结论 1 8086汇编语言中的字符串 1.1 字符串的定义与使用 1.2 直接定义的细节 1.2.1 使用DB数据类型 1.2.2 使用其他数据类型 1.3 直接使用的 ...
- 在Windows 10上将C语言程序转成16位8086汇编代码
大多数人在高校里面学的第一门汇编语言是基于16位的Intel 8086处理器(即8086汇编语言),现在的大多数系统都是32或者64位的,为了实验需要我们一般安装DosBox来作为16位DOS系统模拟 ...
最新文章
- 2017-2018-1 我爱学Java 第一周 作业
- Redhat 停止sendmail的方法
- 自己实现简单的AOP(三) 实现增强四项基本功能
- fstab各项参数及ls-l 长格式各项信息
- Windows SDK编程之一 窗口示例程序
- 运行cudasift
- 复现Cell附图 |类器官的单细胞分析
- c++ 箭头符号怎么打_C++随笔
- mac bochs 调试linux,Mac OS X下编译安装带debugger的bochs
- 不当IT民工——技术带来质的飞跃
- Deciding the Number of Clusterings
- ubuntu18安装tim
- 服务器系统开启telnet,开启Telnet服务
- 自盲化能力 Paillier和EIGamal
- 4.4-软件开发中,“UI设计图”的作用与绘制方法说明
- 学生证选课系统c语言大作业,学生选课管理系统c语言程序
- 如何使用 forestplot 包绘制森林图展示多个效应的大小
- 公司章程违反了公司法该怎么办
- migo获取header sap_SAP中migo什么意思
- 杰理之省电容MIC收敛值【篇】