Implementierung des 8086-Assembly-Compilers (1) – Grundgerüst

Vorwort

Ich habe vor mehr als einem halben Jahr „Assemblersprache“ von Lehrer Wang Shuang rezensiert, lerne die GO-Sprache und möchte sie üben. Deshalb habe ich mit GO einen 8086-Assembler-Compiler implementiert, der Assembler-Programme in ausführbare Programme kompilieren kann. Dann wurde eine virtuelle 8086-Maschine implementiert, um dieses Programm zu laden und auszuführen.

Jetzt werde ich einige Artikel schreiben, um vorzustellen, wie ich diesen einfachen 8086-Compiler und die virtuelle Maschine implementiert habe.

Arbeitsprinzip

Die Eingabe des Compilers ist eine Assemblydatei und die Ausgabe ist ein ausführbares Programm [kann von der virtuellen 8086-Maschine ausgeführt werden]. Es macht hauptsächlich zwei Dinge:

  1. Übersetzen Sie Montageanweisungen in Maschinenanweisungen
  2. Fügen Sie einige notwendige Programm-Header-Informationen hinzu, damit diese Maschinenanweisungen ausgeführt werden können [und es zu einem ausführbaren Programm machen]

Das folgende Assemblerprogramm im Buch dient als Beispiel zur Veranschaulichung der Implementierung eines Compilers:

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. Dieser Assembler definiert das Datensegment, das Stapelsegment und das Codesegment. Diese Abschnitte werden mithilfe der Anweisungen assume, XXX segment, XXX ends definiert. Der Compiler muss diese Anweisungen erkennen und verarbeiten können.

  2. Im Datensegment und Stapelsegment werden Pseudoanweisungendw verwendet, um einige Daten zu definieren. Der Compiler muss in der Lage sein, diese Direktive zu erkennen und die von ihr definierten Daten in Binärdateien umzuwandeln.

  3. Das Codesegment enthält drei Beschriftungen: start, s0 und s1. Der Wert der Beschriftung ist ein Offset relativ zum Codesegment. Das spezielle Label start stellt den Programmeintrag dar und hat in diesem Programm den Wert 0. Die Beschriftung erscheint am Anfang einer Zeile [im Folgenden als externe Beschriftung bezeichnet] oder im Operanden einer Anweisung [z. B. der Zeile „Schleife s0“. " , im Folgenden internes Label genannt]. Der Compiler muss diese Labels erkennen und verarbeiten können.

  4. Das Codesegment enthält mov, push, add, loop usw . Montageanleitungen. Der Compiler muss in der Lage sein, diese Montageanweisungen zu erkennen und in Maschinenanweisungen zu übersetzen.

  5. Sonderanweisungend gibt das Ende des Assemblerprogramms an.

Daher besteht das grundlegende Arbeitsprinzip des Compilers darin, die Assembler-Quelldatei Zeile für Zeile zu scannen und die darin enthaltenen Pseudoanweisungen, Beschriftungen und Assembleranweisungen zu verarbeiten.

erreichen

einige Schlüsselvariablen

  1. Um die Direktive zu verarbeiten assume müssen wir eine Karte definieren:
var segMap = map[string]string{
    
    }

Zeichnen Sie den Segmentnamen und das entsprechende Segment auf. Beispielsweise entspricht „Code“ in diesem Programm „cs“. Als nächstes wird beim Scannen der XXX segment-Direktive XXX verwendet, um die Karte zu durchsuchen, um zu bestimmen, welches Segment durch die Direktive definiert wird.

  1. Um Beschriftungen zu verarbeiten, müssen wir eine Karte definieren:
var labelMap = map[string]uint16{
    
    }

Notieren Sie den Labelnamen und seinen Offset relativ zum Codesegment.

Wir müssen auch eine Variable definieren, um den Offset relativ zum Codesegment darzustellen:

var codeOffset uint16 // 代码段偏移量

Diese Variable wird bei der Verarbeitung der „Codesegment“-Direktive auf 0 initialisiert. Nach Abarbeitung einer Montageanweisung erhöht sich ihr Wert um die Länge der Maschinenanweisung, die der Montageanweisung entspricht. Wenn beispielsweise die Assembleranweisung „mov ax, stack“ in Maschinenanweisungen übersetzt wird und 3 Byte groß ist, erhöht sich der Wert von codeOffset um 3.

  1. Als nächstes müssen Sie die folgenden Variablen definieren:
var progOffset uint32 // 程序偏移量
var codeEntryOffset uint32 // 代码段中程序入口的偏移量
var codeSegProgOffset  uint32 // 代码段在程序中的偏移量
var dataSegProgOffset  uint32 // 数据段在程序中的偏移量
var stackSegProgOffset uint32 // 堆栈段在程序中的偏移量

Die Variable progOffset ist zunächst 0.

  1. Nach der Verarbeitung von Anweisungen wie dw wird sein Wert um die definierte Datenlänge erhöht. Nach der Verarbeitung der dw-Direktive in der dritten Zeile [die 16 Datenbytes definiert] wird der progOffset-Wert beispielsweise zu 16.
  2. Nach Abarbeitung einer Montageanweisung erhöht sich ihr Wert um die Länge der Maschinenanweisung, die der Montageanweisung entspricht. Wenn beispielsweise die Assembleranweisung „mov ax,stack“ in Maschinenanweisungen übersetzt wird und 3 Bytes groß ist, erhöht sich der Wert von progOffset um 3.

Der Variable codeSegProgOffset wird bei der Verarbeitung der Direktive „Codesegment“ der Wert der Variable progOffset zugewiesen.

Der Variable dataSegProgOffset wird bei der Verarbeitung der Direktive „data segment“ der Wert der Variable progOffset zugewiesen.

Der Variablen „stackSegProgOffset“ wird bei der Verarbeitung der „Stack-Segment“-Direktive der Wert der Variablen „progOffset“ zugewiesen.

Der Variable codeEntryOffset wird bei der Verarbeitung des „Start“-Labels der Wert der Variable codeOffset zugewiesen.

  1. Die vom Compiler ausgegebene ausführbare Programmausgabe wird durch ein Byte-Slice dargestellt:
var program []byte

Außerdem müssen einige wichtige Datenstrukturen und Variablen definiert werden, die im Folgenden erwähnt werden.

Hauptfunktion

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)
	}
}

Analytische Funktionen

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. Schauen Sie sich die Verarbeitung von db, dw, dd und anderen Pseudoanweisungen an:

Die Funktion parseDB gibt die durch die Direktive dw definierten Daten zurück. Geben Sie einfach ein []Byte zurück. Die spezifische Implementierung besteht darin, die Zeichenfolge zu analysieren und die durch die Zeichenfolge definierte Zahl in eine Ganzzahl umzuwandeln. Es gibt nichts zu sagen.

Diese Daten werden dann dem Programm-Slice hinzugefügt, bei dem es sich um das im Programm definierte Datensegment handelt.

Aktualisieren Sie dann den Programmoffset progOffset.

  1. Die Abwicklung von Montageanleitungen ist ähnlich:

Hängen Sie die übersetzten Maschinenanweisungen an den Programmabschnitt an. Zusätzlich zur Aktualisierung der Variablen progOffset müssen Sie jedoch auch die Variable codeOffset aktualisieren, da im Codesegment im Allgemeinen nur Assembleranweisungen enthalten sind.

Die Funktion der parseLabelField-Funktion besteht darin, die externen Beschriftungen, Assembleranweisungen und Assembleranweisungsoperanden in der Assemblyanweisung zu trennen und die externen Beschriftungen zu verarbeiten. Es gibt einen []String zurück. Das erste Element ist der Anweisungsname und die folgenden Elemente sind die Operanden der Anweisung.

Beispielsweise gibt die Assembly-Anweisung „start: mov ax, stack“ [„mov“, „ax“, „stack“] zurück.

Die Assembly-Anweisung „push cs:[bx]“ gibt [„push“, „cs:[bx]“] zurück.

Die Implementierung erfolgt in Form einer Zeichenfolgenverarbeitung, die hier nicht erläutert wird.

Montageanleitung hinzufügen

Definieren Sie die InstructionOps-Struktur zur Darstellung einer Montageanweisung und rufen Sie deren Do-Methode auf, um die Montageanweisung in Maschinenanweisungen zu übersetzen:

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
}

Das heißt, eine Assembleranweisung muss zwei Handler implementieren:

  1. Überprüfen Sie den Handler und prüfen Sie, ob das Format der Montageanweisung korrekt ist. Wenn der Operand korrekt ist, speichern Sie ihn in ctx.
  2. Encode-Handler, ruft den Operanden von ctx ab und übersetzt die Anweisung in Maschinencode. Gibt []Byte zurück.

Zum Speichern der hinzugefügten Anweisungen wird eine Variable vom Typ „instruktionTable“ vom Kartentyp definiert. Rufen Sie die AddInstruction-Funktion auf, um eine Anweisung hinzuzufügen. Rufen Sie die FindInstruction-Funktion auf, um Anweisungen zu finden:

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
}

Wenn Sie beispielsweise den Befehlmov hinzufügen, können Sie eine neue Datei encode_jmp.go erstellen und dann:

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

Ab dem nächsten Artikel werden wir die Implementierungen des Check-Handlers und des Encode-Handlers mehrerer gängiger Anweisungen wie mov, jmp und anderer Anweisungen vorstellen.

Guess you like

Origin blog.csdn.net/woay2008/article/details/126535448