Wie gehen wir von der Baugruppe zum Maschinencode (Codegenerierung)?

16

Gibt es eine einfache Möglichkeit, den Schritt zwischen dem Zusammenstellen von Code und Maschinencode zu visualisieren?

Wenn Sie beispielsweise eine Binärdatei im Editor öffnen, wird eine textlich formatierte Darstellung des Maschinencodes angezeigt. Ich gehe davon aus, dass jedes Byte (Symbol), das Sie sehen, das entsprechende ASCII-Zeichen für den Binärwert ist.

Aber wie kommen wir von der Baugruppe zur Binärdatei? Was passiert hinter den Kulissen?

user12979
quelle

Antworten:

28

Schauen Sie sich die Dokumentation des Anweisungssatzes an, und Sie finden für jede Anweisung Einträge wie diesen von einem pic-Mikrocontroller :

Beispiel Addlw-Anweisung

Die "Kodierungs" -Zeile gibt an, wie diese Anweisung in Binärform aussieht. In diesem Fall beginnt es immer mit 5 Einsen, dann mit einem Egal-Bit (das entweder eins oder null sein kann), dann steht das "k" für das Literal, das Sie hinzufügen.

Die ersten paar Bits werden "Opcode" genannt und sind für jeden Befehl eindeutig. Die CPU prüft im Grunde genommen den Operationscode, um zu sehen, um welchen Befehl es sich handelt, und kann dann die "k" als eine hinzuzufügende Zahl dekodieren.

Es ist mühsam, aber nicht so schwer zu codieren und zu decodieren. Ich hatte eine Grundschulklasse, in der wir es in Prüfungen von Hand machen mussten.

Um eine vollständige ausführbare Datei zu erstellen, müssen Sie beispielsweise Speicher zuweisen, Verzweigungsoffsets berechnen und sie abhängig von Ihrem Betriebssystem in ein Format wie ELF umwandeln .

Karl Bielefeldt
quelle
10

Montage-Operationscodes entsprechen größtenteils eins zu eins den zugrunde liegenden Maschinenanweisungen. Sie müssen also nur jeden Opcode in der Assemblersprache identifizieren, ihn der entsprechenden Maschinenanweisung zuordnen und die Maschinenanweisung zusammen mit den entsprechenden Parametern (falls vorhanden) in eine Datei ausgeben. Anschließend wiederholen Sie den Vorgang für jeden weiteren Opcode in der Quelldatei.

Natürlich ist mehr erforderlich, um eine ausführbare Datei zu erstellen, die ordnungsgemäß geladen und auf einem Betriebssystem ausgeführt werden kann, und die meisten anständigen Assembler verfügen über einige zusätzliche Funktionen, die über die einfache Zuordnung von Opcodes zu Maschinenanweisungen (z. B. Makros) hinausgehen.

Robert Harvey
quelle
7

Das erste, was Sie brauchen, ist so etwas wie diese Datei . Dies ist die Anweisungsdatenbank für x86-Prozessoren, wie sie vom NASM-Assembler verwendet wird (die ich mitgeschrieben habe, obwohl nicht die Teile, die tatsächlich Anweisungen übersetzen). Lass uns eine beliebige Zeile aus der Datenbank auswählen:

ADD   rm32,imm8    [mi:    hle o32 83 /0 ib,s]      386,LOCK

Dies bedeutet, dass es die Anweisung beschreibt ADD. Es gibt mehrere Varianten dieses Befehls, und die hier beschriebene Variante verwendet entweder ein 32-Bit-Register oder eine Speicheradresse und fügt einen unmittelbaren 8-Bit-Wert hinzu (dh eine Konstante, die direkt in dem Befehl enthalten ist). Eine beispielhafte Montageanleitung, die diese Version verwenden würde, lautet wie folgt:

add eax, 42

Nun müssen Sie Ihre Texteingabe in einzelne Anweisungen und Operanden zerlegen. Bei der obigen Anweisung würde dies wahrscheinlich zu einer Struktur führen, die die Anweisung ADDund ein Array von Operanden enthält (eine Referenz auf das Register EAXund den Wert 42). Sobald Sie diese Struktur haben, durchlaufen Sie die Anweisungsdatenbank und suchen die Zeile, die sowohl dem Anweisungsnamen als auch den Typen der Operanden entspricht. Wenn Sie keine Übereinstimmung finden, ist dies ein Fehler, der dem Benutzer angezeigt werden muss ("unzulässige Kombination von Opcode und Operanden" oder ähnliches ist der übliche Text).

Sobald wir die Zeile aus der Datenbank erhalten haben, sehen wir uns die dritte Spalte an, die für diese Anweisung wie folgt lautet:

[mi:    hle o32 83 /0 ib,s] 

Dies ist eine Reihe von Anweisungen, die beschreiben, wie die erforderliche Maschinencodeanweisung generiert wird:

  • Das miist eine Beschreibung der Operanden: ein modr/m(Register- oder Speicher-) Operand (was bedeutet, dass wir ein modr/mByte an das Ende des Befehls anhängen müssen , auf das wir später noch eingehen werden) und ein sofortiger Befehl (der wird) in der Beschreibung der Anweisung verwendet werden).
  • Weiter ist hle. Dies gibt an, wie wir mit dem Präfix "lock" umgehen. Wir haben "lock" nicht verwendet und ignorieren es daher.
  • Weiter ist o32. Dies sagt uns, dass, wenn wir Code für ein 16-Bit-Ausgabeformat zusammenstellen, der Befehl ein Präfix zum Überschreiben der Operandengröße benötigt. Wenn wir eine 16-Bit-Ausgabe erzeugen würden, würden wir jetzt das Präfix ( 0x66) erzeugen , aber ich gehe davon aus, dass wir es nicht sind, und mach weiter.
  • Weiter ist 83. Dies ist ein Literalbyte in hexadezimaler Schreibweise. Wir geben es aus.
  • Weiter ist /0. Dies spezifiziert einige zusätzliche Bits, die wir im modr / m-Byte benötigen, und veranlasst uns, sie zu generieren. Das modr/mByte wird zum Codieren von Registern oder indirekten Speicherreferenzen verwendet. Wir haben einen einzigen solchen Operanden, ein Register. Das Register hat eine Nummer, die in einer anderen Datendatei angegeben ist :

    eax     REG_EAX         reg32           0
  • Wir überprüfen, ob reg32die erforderliche Größe der Anweisung aus der ursprünglichen Datenbank (wie sie ist) übereinstimmt. Das 0ist die Nummer des Registers. Ein modr/mByte ist eine vom Prozessor festgelegte Datenstruktur, die folgendermaßen aussieht:

     (most significant bit)
     2 bits       mod    - 00 => indirect, e.g. [eax]
                           01 => indirect plus byte offset
                           10 => indirect plus word offset
                           11 => register
     3 bits       reg    - identifies register
     3 bits       rm     - identifies second register or additional data
     (least significant bit)
  • Da wir mit einem Register arbeiten, ist das modFeld 0b11.

  • Das regFeld ist die Nummer des Registers, das wir verwenden,0b000
  • Da diese Anweisung nur ein einziges Register enthält, müssen wir das rmFeld mit etwas ausfüllen . Das ist, wofür die zusätzlichen Daten spezifiziert /0wurden, also setzen wir das in das rmFeld 0b000,.
  • Das modr/mByte ist also 0b11000000oder 0xC0. Wir geben dies aus.
  • Weiter ist ib,s. Dies gibt ein vorzeichenbehaftetes Sofortbyte an. Wir sehen uns die Operanden an und stellen fest, dass wir einen sofort verfügbaren Wert haben. Wir konvertieren es in ein vorzeichenbehaftetes Byte und geben es aus ( 42=> 0x2A).

Die komplett montierte Anweisung lautet daher: 0x83 0xC0 0x2A. Senden Sie es zusammen mit dem Hinweis, dass keines der Bytes eine Speicherreferenz darstellt, an Ihr Ausgabemodul (das Ausgabemodul muss möglicherweise wissen, ob dies der Fall ist).

Wiederholen Sie dies für jede Anweisung. Behalten Sie den Überblick über Beschriftungen, damit Sie wissen, was eingefügt werden muss, wenn auf sie verwiesen wird. Fügen Sie Funktionen für Makros und Anweisungen hinzu, die an die Ausgabemodule für Objektdateien übergeben werden. Und so funktioniert im Grunde ein Assembler.

Jules
quelle
1
Vielen Dank. Gute Erklärung, aber sollte es nicht "0x83 0xC0 0x2A" statt "0x83 0xB0 0x2A" sein, weil 0b11000000 = 0xC0
Kamran
@ Kamran - $ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003... ja, du hast ganz recht. :)
Jules
2

In der Praxis erzeugt ein Assembler normalerweise nicht direkt eine ausführbare Binärdatei , sondern eine Objektdatei (die später dem Linker zugeführt wird ). Es gibt jedoch Ausnahmen (Sie können einige Assembler verwenden, um direkt eine ausführbare Binärdatei zu erstellen; diese sind selten).

Beachten Sie zunächst, dass viele Assembler heute freie Softwareprogramme sind. Laden Sie also den Quellcode von GNU as (ein Teil von binutils ) und von nasm herunter und kompilieren Sie ihn auf Ihrem Computer . Dann studieren Sie ihren Quellcode. Übrigens empfehle ich die Verwendung von Linux für diesen Zweck (es ist ein sehr entwickler- und softwarefreundliches Betriebssystem).

Die von einem Assembler erstellte Objektdatei enthält insbesondere ein Codesegment und Anweisungen zum Verschieben . Es ist in einem gut dokumentierten Dateiformat organisiert, das vom Betriebssystem abhängt. Unter Linux ist dieses Format (das für Objektdateien, gemeinsam genutzte Bibliotheken, Core-Dumps und ausführbare Dateien verwendet wird) ELF . Diese Objektdatei wird später in den Linker eingegeben (der schließlich eine ausführbare Datei erzeugt). Umzüge werden vom ABI vorgegeben (zB x86-64 ABI ). Lesen Sie Levines Buch Linkers and Loaders für mehr.

Das Codesegment in einer solchen Objektdatei enthält Maschinencode mit Löchern (die mit Hilfe von Umsiedlungsinformationen vom Linker zu füllen sind). Der von einem Assembler generierte (umsetzbare) Maschinencode ist offensichtlich spezifisch für eine Befehlssatzarchitektur . Die x86- oder x86-64- ISAs (die in den meisten Laptop- oder Desktop-Prozessoren verwendet werden) sind in ihren Details äußerst komplex. Zu Lehrzwecken wurde jedoch eine vereinfachte Teilmenge mit der Bezeichnung y86 oder y86-64 erfunden. Lesen Sie die Folien darauf. Andere Antworten auf diese Frage erklären auch ein bisschen davon. Vielleicht möchten Sie ein gutes Buch über Computerarchitektur lesen .

Die meisten Assembler arbeiten in zwei Durchläufen , wobei der zweite die Verlagerung oder Korrektur eines Teils der Ausgabe des ersten Durchlaufs durchführt. Sie verwenden jetzt üblichen Parsing - Techniken (so lesen vielleicht The Dragon Buch ).

Wie eine ausführbare Datei durch das Betriebssystem gestartet wird Kernel (zB wie der execveSystemaufruf auf Linux arbeitet) eine andere (und komplexe) Frage. Normalerweise wird ein virtueller Adressraum eingerichtet (in dem Prozess , der das ausführt (2) ...) und anschließend der interne Status des Prozesses neu initialisiert (einschließlich Benutzermodusregister ). Ein dynamischer Linker wie ld-linux.so (8) unter Linux ist möglicherweise zur Laufzeit beteiligt. Lesen Sie ein gutes Buch wie Betriebssystem: Drei einfache Teile . Das OSDEV- Wiki bietet auch nützliche Informationen.

PS. Ihre Frage ist so weit gefasst, dass Sie mehrere Bücher darüber lesen müssen. Ich habe einige (sehr unvollständige) Hinweise gegeben. Sie sollten mehr von ihnen finden.

Basile Starynkevitch
quelle
1
In Bezug auf Objektdateiformate würde ich Anfängern empfehlen, sich das von NASM erzeugte RDOFF-Format anzusehen. Dies sollte so einfach wie möglich sein und dennoch in einer Vielzahl von Situationen funktionieren. Die NASM-Quelle enthält einen Linker und einen Loader für das Format. (Vollständige Offenlegung - ich entwarf und schrieb all diese)
Jules