8086汇编代码_8086汇编语言指令表

8086汇编代码_8086汇编语言指令表如何实现简单的8086汇编语言编译器_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 实现。

今天的文章
8086汇编代码_8086汇编语言指令表分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/80238.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注