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 ADD
und ein Array von Operanden enthält (eine Referenz auf das Register EAX
und 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
mi
ist eine Beschreibung der Operanden: ein modr/m
(Register- oder Speicher-) Operand (was bedeutet, dass wir ein modr/m
Byte 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/m
Byte 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 reg32
die erforderliche Größe der Anweisung aus der ursprünglichen Datenbank (wie sie ist) übereinstimmt. Das 0
ist die Nummer des Registers. Ein modr/m
Byte 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 mod
Feld 0b11
.
- Das
reg
Feld ist die Nummer des Registers, das wir verwenden,0b000
- Da diese Anweisung nur ein einziges Register enthält, müssen wir das
rm
Feld mit etwas ausfüllen . Das ist, wofür die zusätzlichen Daten spezifiziert /0
wurden, also setzen wir das in das rm
Feld 0b000
,.
- Das
modr/m
Byte ist also 0b11000000
oder 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.
$ 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. :)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
execve
Systemaufruf 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.
quelle