========================== Intel 64 Assembler Sprache [TOC] ========================== Nachdem wir uns einigermaßen mit der Intel 64 Architektur auskennen wollen wir auch verstehen wie man diese mittels Assembler programmieren kann. Wir fragen dazu jemanden der sich auskennt: Nicht mich sondern den `gcc`. Wenn man ein bisschen C versteht und einfache Programme in Assembler übersetzen lässt ist der Einstieg gar nicht so schwer. Erstes Beispiel =============== :import: day02/first.c [stripped] Mit *--[SHELL(path=day02)]----------------------------------------------------* | | | gcc-4.8 -Wall -S -fno-asynchronous-unwind-tables first.c | | | *-------------------------------------------------------------------------* erzeugen wir den Assembler Code :import: day02/first.s Zeile für Zeile =============== Zeilen die mit einem Punkt (`.`) beginnen sind sogenannte Assembler Direktiven. Diese werden nicht in Maschinenbefehle umgewandelt sondern stellen eine Art Verwaltungsinformation für den Assembler oder Linker dar. --- CODE(type=s) --------------------------------------------------------------- .globl _a -------------------------------------------------------------------------------- Die *.globl* Direktive besagt hier, dass das Symbol *_a* von anderen Dateien aus referenziert werden kann. Das ist später für den Linker wichtig. Zu beachten ist, dass nicht gesagt wird um was für ein Symbol es sich handelt. Es könnte sich theoretisch um eine Funktion oder eine Variable handeln. Aus historischen Gründen wird vom C Compiler jedem Symbol übrigens ein Unterstrich vorangesetzt. Damit sollten angeblich Namenskonflikten mit vordefinierten Symbolen vermieden werden. Wenn man Assembler Code von Hand schreibt, dann muss man sich daran nicht halten. Der Fortran Compiler hält sich an ähnliche Konventionen. Möchte man C von Fortran oder umgekehrt aufrufen dann wird das relevant. --- CODE(type=s) --------------------------------------------------------------- .data -------------------------------------------------------------------------------- Diese Direktive sagt, dass alles was ab jetzt kommt später ins *Data Segment* kommen soll. Jedenfalls solange bis etwas anderes gesagt wird. --- CODE(type=s) --------------------------------------------------------------- .align 2 -------------------------------------------------------------------------------- Im Prinzip können bei der Intel Architektur die Daten an einer beliebigen Adresse gespeichert werden. Ein `double word` (wie zum Beispiel `unsigned`) könnte an der Adresse 1, 2, 3 oder 4 abgespeichert werden. Hier wird aber verlangt, dass die Adresse des nächsten Datensatzes, der im Data-Segment angelegt wird, durch $2^2=4$ teilbar sein soll. Dazu wird der *location counter* auf die nächste durch 4-teilbare Zahl aufgerundet. Wieso mit so einer Direktive die Effizienz gesteigert werden kann behandeln wir später. --- CODE(type=s) --------------------------------------------------------------- _a: -------------------------------------------------------------------------------- Dies ist ein sogenannter *Label*. Damit wird gesagt, dass ab jetzt die Definition des Symbols `_a` erfolgt (von dem schon vorher gesagt wurde, dass es von Außen referenziert werden kann). --- CODE(type=s) --------------------------------------------------------------- .long 1 -------------------------------------------------------------------------------- Jetzt wird gesagt, dass an der Stelle des *location counter* 4 Bytes (long, double word) mit `1` initialisiert werden sollen. Anschliessend wird der *location counter* um 4 erhöht. --- CODE(type=s) --------------------------------------------------------------- .globl _b .align 2 _b: .long 2 -------------------------------------------------------------------------------- Analog wird die Variable `_b` definiert: - von Außen referenzierbar, - an einer durch 4 teilbaren Adresse und - mit `2` initialisiert. --- CODE(type=s) --------------------------------------------------------------- .globl _c .align 2 _c: .long 3 -------------------------------------------------------------------------------- Und nochmals für `_c`: - von Außen referenzierbar, - an einer durch 4 teilbaren Adresse und - mit `3` initialisiert. --- CODE(type=s) --------------------------------------------------------------- .text -------------------------------------------------------------------------------- Ab jetzt wird das *Text-Segment* mit den Intel 64 Instruktionen gefüllt. --- CODE(type=s) --------------------------------------------------------------- .globl _first _first: -------------------------------------------------------------------------------- Auch hier wird ein öffentlich zugängliches Symbol (`_first`) deklariert. Da wir jetzt im Text Segment sind, ist klar, dass das Symbol eine Funktion darstellt. Der Begriff "Funktion" hört sich im Kontext von Assembler etwas hoch gegriffen an. Wenn der Maschinencode erzeugt wird muss klar sein an welcher Speicherstelle der nächste Befehl steht. --- CODE(type=s) --------------------------------------------------------------- pushq %rbp -------------------------------------------------------------------------------- Die Intel 64 Spezifikation sagt: *--[NOTE]---------------------------------------------------------* | | | Registers %rbp, %rbx and %r12 through %r15 “belong” to the | | calling function and the called function is required to | | preserve their values. | | | *-----------------------------------------------------------------* Da im Folgenden der Wert keiner dieser Register verändert wird, könnte man dies eigentlich ignorieren. Schaltet man beim Compiler die Optimierung ein, dann wird dies ignoriert. Ohne Optimierung geht der Compiler aber auf Nummer Sicher. Der Befehl `push` legt sein Argument auf den Stack. Das geht so: - Der Wert von `%rsp` enthält die Adresse des obersten Datensatzes auf dem Stack. Also die Position der Spitze. - Soll ein 4-Byte Datensatz auf den Stack gelegt werden, dann wird `%rsp` um 4 dekrementiert. Denn der Stack wächst nach unten. - Das 4-Byte Argument wird an die in `%rsp` enthaltene Adresse kopiert. Woher soll `push` wissen wie groß sein Argument ist? Das Suffix `q` sagt, dass das Argument als `quad word` interpretiert wird. Der Befehl kommt also in verschiedenen Varianten: +--------+-------------------------------------+ | pushb | Argument ist 1 Byte | +--------+-------------------------------------+ | pushw | Argument ist ein Word (2 Byte) | +--------+-------------------------------------+ | pushl | Argument ist ein Long (4 Byte) | +--------+-------------------------------------+ | pushq | Argument ist ein Quad-Word (8 Byte) | +--------+-------------------------------------+ Was immer also in `%rbp` war ist jetzt also auf dem Stack. --- CODE(type=s) --------------------------------------------------------------- movq %rsp, %rbp -------------------------------------------------------------------------------- Mit dem *Move Befehl* `mov` kann man Daten kopieren. Und zwar: - Vom Speicher in ein Register, - von Register zu Register, - von Register in den Speicher. *Nicht erlaubt* ist das Kopieren von "Speicheradresse zu Speicheradresse*. Die Daten müssen also einmal durch die Register. Hier wird also der Inhalt von `%rsp` in `%rbp` gespeichert. Es ist also eine Kopie von Register nach Register. Wie gesagt, das vorige Ablegen von `%rbp` auf den Stack und anschliessende Überschreiben mit `%rsp` ist hier eigentlich unnötig. Wieso dies im Allgemeinen notwendig ist sehen wir später. --- CODE(type=s) --------------------------------------------------------------- movl _b(%rip), %edx -------------------------------------------------------------------------------- Hier werden 4 Bytes von *_b(%rip)* ins Register `%edx` kopiert. Klar ist, dass mit `%edx` die ersten 4 Bytes des `%rdx` Register gemeint sind. Aber was soll *_b(%rip)* sein? Natürlich soll das die Adresse im Data-Segment darstellen an der `_b` steht. Das Problem ist, dass man erst nach dem Linken weiss wo `_b` letztlich im Data-Segement steht. Denn man muss daran denken, dass eventuell viele Object Files gelinkt werden und jedes etwas zum Data Segment beiträgt. Nun werden in *_b(%rip)* mehrere Informationen verknüpft: - `%rip` enthält die Adresse des Maschinenbefehls der als nächstes ausgeführt wird. - Mit der Notation `(%rip)` wird aus dem Wert in `%rip` eine Adresse (mit diesem Wert) erzeugt. - Sei `D` eine Konstante, dann wird mit `D(%rip)` eine von `(%rip)` um `D` verschobene (*displaced*) Adresse erzeugt. - Der Linker weiss wo die mit Label `_b` gekennzeichnete Stelle im Data-Segment letztlich gelandet ist. Daraus wird beim Linken die Adresse vom Datensatz mit Label `_b` relativ zur Adresse des Befehls erzeugt. --- CODE(type=s) --------------------------------------------------------------- movl _c(%rip), %eax -------------------------------------------------------------------------------- Analog werden 4-Bytes von der Adresse des Label `_c` ins Register `%eax` kopiert. --- CODE(type=s) --------------------------------------------------------------- addl %edx, %eax -------------------------------------------------------------------------------- Der Inhalt von `%edx` wird zu `%eax` addiert. Das Ergebnis steht also in `%eax`. --- CODE(type=s) --------------------------------------------------------------- movl %eax, _a(%rip) -------------------------------------------------------------------------------- Hier wird der Inhalt von `%eax` an die Adresse des Label `_a` kopiert. --- CODE(type=s) --------------------------------------------------------------- movl _a(%rip), %eax -------------------------------------------------------------------------------- Jetzt wird der Wert von `_a` wieder in `%eax` kopiert. Hallo?!?!? Okay, man bedenke, dass der Compiler noch nicht wirklich optimiert hat. --- CODE(type=s) --------------------------------------------------------------- cmpl $4, %eax -------------------------------------------------------------------------------- `cmpl` ist die `cmp` Variante um zwei Word Daten zu vergleichen. Analog gibt es `cmpb`, `cmpw` und `cmpq`. Außerdem muss man wissen, dass im Assembler Code Konstanten mit einem Dollarzeichen codiert werden. Hier wird also der Inhalt von Register `%eax` mit `4` Verglichen. Das geschieht indem die beiden Werte subtrahiert werden und das Ergebnis weggeworfen wird. Allerdings werden auch die Flags `CF` (*Carry Flag*) und `ZF` (*Zero Flag*) gesetzt! Und zwar so: - Wenn die Differenz $0$ ist, dann wird das `ZF` gesetzt. - Wenn der rechte Operand größer als der linke ist (also "links-rechts<0") dann wird das `CF`gesetzt. Man kann also folgende Fälle unterscheiden: +-----------+------+------+ | CMP A B | ZF | CF | +-----------+------+------+ | A > B | 0 | 0 | +-----------+------+------+ | A < B | 0 | 1 | +-----------+------+------+ | A = B | 1 | 0 | +-----------+------+------+ Das heisst mit den Flags `ZF` und `CF` kann man alle drei Fälle unterscheiden. --- CODE(type=s) --------------------------------------------------------------- jne L2 -------------------------------------------------------------------------------- `jne` steht für *jump if not equal*. Falls das `ZF` Flag *nicht* gesetzt ist, dann wird das `%rip` Register überschrieben mit der Adresse des Label `L2`. Das heisst: Wenn das `ZF` Flag gesetzt ist, dann wird als nächstes der Befehl ausgeführt, der beim Label `L2` steht. Alte Hasen kennen das vom berüchtigten `go to` Befehl. Das Label `L2` wird übrigens erst weiter unten definiert. Wenn das `ZF` Flags gesetzt ist (also `_a` gleich `3` ist), dann geht es dort weiter. Sonst wird der näachte Befehl ausgeführt: --- CODE(type=s) --------------------------------------------------------------- movl $3, _b(%rip) -------------------------------------------------------------------------------- Die 4-Bytes mit Symbol-Bezeichnung `_b` werden mit der Konstanten `3` überschreiben. --- CODE(type=s) --------------------------------------------------------------- jmp L4 -------------------------------------------------------------------------------- Das `%rip` register wird auf die Adresse von `L4` gesetzt. Das heisst der Befehl der an dieser Adresse steht wird als nächstes ausgeführt. --- CODE(type=s) --------------------------------------------------------------- L2: -------------------------------------------------------------------------------- Das Label `L2`wird definiert. Nach dem Linken ist damit eine Adresse im *Text Segment* eindeutig identifizierbar. --- CODE(type=s) --------------------------------------------------------------- movl _a(%rip), %edx -------------------------------------------------------------------------------- Dieser Befehl steht an der Adresse, die mit `L2` identifizierbar ist. Der Wert der 4-Bytes die mit dem Symbol `_a` assoziert sind werden ins Register `%edx` kopiert. --- CODE(type=s) --------------------------------------------------------------- movl _b(%rip), %eax -------------------------------------------------------------------------------- Die 4-Bytes von `_b` werden ins Register `%eax` kopiert. --- CODE(type=s) --------------------------------------------------------------- andl %edx, %eax -------------------------------------------------------------------------------- Es wird eine AND-Bitverknüpfung der Register `%edx` und `eax` durchgeführt. Das Ergebnis steht dann in `%eax`. --- CODE(type=s) --------------------------------------------------------------- movl %eax, _c(%rip) -------------------------------------------------------------------------------- Wert von `%eax` zurück in `_c` kopieren. --- CODE(type=s) --------------------------------------------------------------- jmp L4 -------------------------------------------------------------------------------- Weiter geht es bei Marke `L4`. --- CODE(type=s) --------------------------------------------------------------- L5: movl _a(%rip), %eax subl $1, %eax movl %eax, _a(%rip) -------------------------------------------------------------------------------- Bei der Marke `L5` passiert also folgendes: - Wert von `_a` in Register `%eax` kopieren, - davon die Konstante $1$ (als 4 Byte Zahl aufgefasst) abziehen und - das Ergebnis in `_a` zurück kopieren. Insgesamt wurde der Wert von `_a` also dekrementiert. --- CODE(type=s) --------------------------------------------------------------- L4: movl _a(%rip), %eax testl %eax, %eax -------------------------------------------------------------------------------- Bei Marke `L4` wird zuerst der Inhalt von `_a` in `%eax` kopiert. Dann wird der Befehl `test` (bzw. seine 4-Byte Variante) benutzt um zu prüfen, ob `%eax` den Wert `0` enthält. Intern wird bei `test` eine bitweise AND-Verknüpfung der Argumente durchgeführt und das Ergebnis verworfen. Abhängig vom Ergebnis werden aber die `SF`, `ZF`, `PF`, `CF`, `OF` und `AF` Flags gesetzt. In diesem Fall ist relevant, dass `ZF` auf 1 gesetzt wird, wenn das Ergebnis 0 ist. Ansonsten wird `ZF` auf 0 gesetzt. --- CODE(type=s) --------------------------------------------------------------- jne L5 -------------------------------------------------------------------------------- Wenn `ZF` ungleich Null ist, dann wird als nächstes der Befehl bei `L5` ausgeführt. --- CODE(type=s) --------------------------------------------------------------- popq %rbp -------------------------------------------------------------------------------- Das Register `%rbp` bekommt seinen alten Wert. --- CODE(type=s) --------------------------------------------------------------- ret -------------------------------------------------------------------------------- Da steckt sehr viel dahinter und wir werden es noch sehr ausführlich behandeln. --- CODE(type=s) --------------------------------------------------------------- .subsections_via_symbols -------------------------------------------------------------------------------- Fragt Google. Aufgaben ======== Was macht der Linker? --------------------- Im vorigen Beispiel wurde mit `otool` disassembliert. Mit `strip` wurden die Symbole mit Zahlen ersetzt. Rechnet bei der Ausgabe von `otool` alle Adressen der Form `D(%rip)` nach. Was macht der Optimierer? ------------------------- Wir schalten nun die Optimierung ein: *--[SHELL(path=day02)]----------------------------------------------------* | | | gcc-4.8 -Wall -O3 -S -fno-asynchronous-unwind-tables first.c | | | *-------------------------------------------------------------------------* Damit erhält man: :import: day02/first.s Analysiert den Assembler Code: - Was tun die einzelnen Zeilen? - Was wird insgesamt getan? Wenn die Funktion `first` z.B. gleich bei Programm Start aufgerufen wird (also `a` und `b` noch nicht verändert ist) kann man das Programm von Hand abarbeiten (spielt "Virtuelle Maschine"). - Mindestens einen Befehl muss man googlen. Bei einem anderen muss man googlen was das zweite Argument tut. - Vergleicht den Assembler Code, den verschiedene gcc Versionen produzieren. Die Versionen sind z.B. `gcc-4.7`, `gcc-4.8`, `gcc-4.9`. Wie groß sind die Datentypen? ----------------------------- Vervollständigt die Tabelle: - nicht mit Google sondern mit dem GCC - aber auch nicht mit `sizeof` sondern aus dem Assemble Code +-------------------+--------------------+------------------+-------------+ | C Deklaration | Intel Daten Typ^[1]| GAS^[2] Suffix | x86-64 Größe| | | | | in Bytes | +-------------------+--------------------+------------------+-------------+ | char | | | | +-------------------+--------------------+------------------+-------------+ | short | | | | +-------------------+--------------------+------------------+-------------+ | int | | | | +-------------------+--------------------+------------------+-------------+ | unsigned | | | | +-------------------+--------------------+------------------+-------------+ | long int | | | | +-------------------+--------------------+------------------+-------------+ | unsigned long | | | | +-------------------+--------------------+------------------+-------------+ | char * | | | | +-------------------+--------------------+------------------+-------------+ | float | | | | +-------------------+--------------------+------------------+-------------+ | double | | | | +-------------------+--------------------+------------------+-------------+ | long double | | | | +-------------------+--------------------+------------------+-------------+ Von Hand Linken --------------- Aus `first.c` alleine kann man keine Ausführbare Datei machen. Den die muss laut Konvention ein Label `_main` enthalten, das nach dem Programm laden als erstes angesprungen wird. Wir ändern `first.c` aber nicht ab sondern schreiben ein zweites C Source File das ein `_main` Label enthält und die Funktion `first()` aufruft. :import: day02/main4first.c [stripped] Wir schauen uns erst den Assembler Code an (*nachmachen*) *--[SHELL(path=day02)]----------------------------------------------------* | | | gcc-4.8 -Wall -S -fno-asynchronous-unwind-tables main4first.c | | | *-------------------------------------------------------------------------* der so aussieht :import: day02/main4first.s und erzeugen dann für `first.c` und `main4first.c` jeweils Object Files (*nachmachen*): *--[SHELL(path=day02)]----------------------------------------------------* | | | gcc-4.8 -Wall -c first.c | | gcc-4.8 -Wall -c main4first.c | | | *-------------------------------------------------------------------------* Deren Inhalt sieht jeweils so aus (*nachmachen*): *--[SHELL(path=day02)]----------------------------------------------------* | | | otool -tdV first.o | | otool -tdV main4first.o | | | *-------------------------------------------------------------------------* Man sieht, dass bei `main4first` gar kein Data Segment vorkommt. Ist auch klar, denn es wurde dort keine globale Variable definiert. Außerdem beginnen die Adressen in beiden Object Files bei `0000000000000000` (*Wieso?*). Diese Object Files Linken wir nun (*nachmachen*): *--[SHELL(path=day02)]----------------------------------------------------* | | | gcc-4.8 first.o main4first.o | | | *-------------------------------------------------------------------------* Das mit wurde die ausführbare Datei `a.out` erzeugt. Deren Inhalt sieht so aus (*nachmachen*): *--[SHELL(path=day02)]----------------------------------------------------* | | | otool -tdV a.out | | | *-------------------------------------------------------------------------* Man beachte die Zeile mit `callq _first` (6 letzte Zeile). Mit `strip` können wir den Wert von `_first` sichtbar machen (*nachmachen*): *--[SHELL(path=day02)]----------------------------------------------------* | | | strip a.out | | otool -tdV a.out | | | *-------------------------------------------------------------------------* Es sollt klar sein, dass der Wert von `_first` erst *nach* dem Linken fest steht. Denn erst dann weiss man wo der Maschinen-Code der Funktion `first()` im Text-Segment steht. Fussnoten ~~~~~~~~~ :footnotes: [1] Also `Byte`, `Word`, `Double Word`, `Quad Word` [2] GAS = __GNU Assembler__ :links: __GNU Assembler__ -> http://de.wikipedia.org/wiki/GNU_Assembler :navigate: __up__ -> doc:index __back__ -> doc:day02/page01