Content

Intel 64 Assembler Sprache

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

unsigned a = 1;
unsigned b = 2;
unsigned c = 3;

void
first()
{
    a = b + c;

    if (a==4) {
        b = 3;
    } else {
        c = a & b;
    }

    while (a>0) {
        a--;
    }
}

Mit

$shell> gcc-4.8 -Wall -S -fno-asynchronous-unwind-tables first.c               

erzeugen wir den Assembler Code

                .globl _a
                .data
                .align 2
_a:
                .long           1
                .globl _b
                .align 2
_b:
                .long           2
                .globl _c
                .align 2
_c:
                .long           3
                .text
                .globl _first
_first:
                pushq           %rbp
                movq            %rsp, %rbp
                movl            _b(%rip), %edx
                movl            _c(%rip), %eax
                addl            %edx, %eax
                movl            %eax_a(%rip)
                movl            _a(%rip), %eax
                cmpl            $4, %eax
                jne             L2
                movl            $3_b(%rip)
                jmp             L4
L2:
                movl            _a(%rip), %edx
                movl            _b(%rip), %eax
                andl            %edx, %eax
                movl            %eax_c(%rip)
                jmp             L4
L5:
                movl            _a(%rip), %eax
                subl            $1, %eax
                movl            %eax_a(%rip)
L4:
                movl            _a(%rip), %eax
                testl           %eax, %eax
                jne             L5
                popq            %rbp
                ret
                .subsections_via_symbols

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.

                .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.

                .data

Diese Direktive sagt, dass alles was ab jetzt kommt später ins Data Segment kommen soll. Jedenfalls solange bis etwas anderes gesagt wird.

                .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.

_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).

                .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.

                .globl _b
                .align 2
_b:
                .long           2

Analog wird die Variable _b definiert:

                .globl _c
                .align 2
_c:
                .long           3

Und nochmals für _c:

                .text

Ab jetzt wird das Text-Segment mit den Intel 64 Instruktionen gefüllt.

                .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.

                pushq           %rbp

Die Intel 64 Spezifikation sagt:

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:

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.

                movq            %rsp, %rbp

Mit dem Move Befehl mov kann man Daten kopieren. Und zwar:

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.

                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:

                movl            _c(%rip), %eax

Analog werden 4-Bytes von der Adresse des Label _c ins Register %eax kopiert.

                addl            %edx, %eax

Der Inhalt von %edx wird zu %eax addiert. Das Ergebnis steht also in %eax.

                movl            %eax_a(%rip)

Hier wird der Inhalt von %eax an die Adresse des Label _a kopiert.

                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.

                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:

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.

                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:

                movl            $3_b(%rip)

Die 4-Bytes mit Symbol-Bezeichnung _b werden mit der Konstanten 3 überschreiben.

                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.

L2:

Das Label L2wird definiert. Nach dem Linken ist damit eine Adresse im Text Segment eindeutig identifizierbar.

                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.

                movl            _b(%rip), %eax

Die 4-Bytes von _b werden ins Register %eax kopiert.

                andl            %edx, %eax

Es wird eine AND-Bitverknüpfung der Register %edx und eax durchgeführt. Das Ergebnis steht dann in %eax.

                movl            %eax_c(%rip)

Wert von %eax zurück in _c kopieren.

                jmp             L4

Weiter geht es bei Marke L4.

L5:
                movl            _a(%rip), %eax
                subl            $1, %eax
                movl            %eax_a(%rip)

Bei der Marke L5 passiert also folgendes:

Insgesamt wurde der Wert von _a also dekrementiert.

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 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.

                jne             L5

Wenn ZF ungleich Null ist, dann wird als nächstes der Befehl bei L5 ausgeführt.

                popq            %rbp

Das Register %rbp bekommt seinen alten Wert.

                ret

Da steckt sehr viel dahinter und wir werden es noch sehr ausführlich behandeln.

                .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> gcc-4.8 -Wall -O3 -S -fno-asynchronous-unwind-tables first.c           

Damit erhält man:

                .text
                .align 4,0x90
                .globl _first
_first:
                movl            _b(%rip), %edx
                movl            _c(%rip), %eax
                addl            %edx, %eax
                cmpl            $4, %eax
                movl            %eax_a(%rip)
                je              L6
                andl            %eax, %edx
                testl           %eax, %eax
                movl            %edx_c(%rip)
                je              L1
L3:
                movl            $0, _a(%rip)
L1:
                rep; ret
                .align 4,0x90
L6:
                movl            $3_b(%rip)
                jmp             L3
                .globl _c
                .data
                .align 4
_c:
                .long           3
                .globl _b
                .align 4
_b:
                .long           2
                .globl _a
                .align 4
_a:
                .long           1
                .subsections_via_symbols

Analysiert den Assembler Code:

Wie groß sind die Datentypen?

Vervollständigt die Tabelle:

C Deklaration

Intel Daten Typ1

GAS2 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.

void
first();

int
main()
{
    first();
    return 0;
}

Wir schauen uns erst den Assembler Code an (nachmachen)

$shell> gcc-4.8 -Wall -S -fno-asynchronous-unwind-tables main4first.c          

der so aussieht

                .text
                .globl _main
_main:
                pushq           %rbp
                movq            %rsp, %rbp
                movl            $0, %eax
                call            _first
                movl            $0, %eax
                popq            %rbp
                ret
                .subsections_via_symbols

und erzeugen dann für first.c und main4first.c jeweils Object Files (nachmachen):

$shell> gcc-4.8 -Wall -c first.c                                               
$shell> gcc-4.8 -Wall -c main4first.c                                          

Deren Inhalt sieht jeweils so aus (nachmachen):

$shell> otool -tdV first.o                                                     
first.o:
(__TEXT,__text) section
_first:
0000000000000000	pushq	%rbp
0000000000000001	movq	%rsp, %rbp
0000000000000004	movl	_b(%rip), %edx
000000000000000a	movl	_c(%rip), %eax
0000000000000010	addl	%edx, %eax
0000000000000012	movl	%eax, _a(%rip)
0000000000000018	movl	_a(%rip), %eax
000000000000001e	cmpl	$0x4, %eax
0000000000000021	jne	0x2f
0000000000000023	movl	$0x3, _b-4(%rip)
000000000000002d	jmp	0x54
000000000000002f	movl	_a(%rip), %edx
0000000000000035	movl	_b(%rip), %eax
000000000000003b	andl	%edx, %eax
000000000000003d	movl	%eax, _c(%rip)
0000000000000043	jmp	0x54
0000000000000045	movl	_a(%rip), %eax
000000000000004b	subl	$0x1, %eax
000000000000004e	movl	%eax, _a(%rip)
0000000000000054	movl	_a(%rip), %eax
000000000000005a	testl	%eax, %eax
000000000000005c	jne	0x45
000000000000005e	popq	%rbp
000000000000005f	ret
(__DATA,__data) section
0000000000000060	01 00 00 00 02 00 00 00 03 00 00 00 
$shell> otool -tdV main4first.o                                                
main4first.o:
(__TEXT,__text) section
_main:
0000000000000000	pushq	%rbp
0000000000000001	movq	%rsp, %rbp
0000000000000004	movl	$_main, %eax
0000000000000009	callq	_first
000000000000000e	movl	$_main, %eax
0000000000000013	popq	%rbp
0000000000000014	ret

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> gcc-4.8 first.o main4first.o                                           

Das mit wurde die ausführbare Datei a.out erzeugt. Deren Inhalt sieht so aus (nachmachen):

$shell> otool -tdV a.out                                                       
a.out:
(__TEXT,__text) section
_first:
0000000100000eeb	pushq	%rbp
0000000100000eec	movq	%rsp, %rbp
0000000100000eef	movl	_b(%rip), %edx
0000000100000ef5	movl	_c(%rip), %eax
0000000100000efb	addl	%edx, %eax
0000000100000efd	movl	%eax, _a(%rip)
0000000100000f03	movl	_a(%rip), %eax
0000000100000f09	cmpl	$0x4, %eax
0000000100000f0c	jne	0x100000f1a
0000000100000f0e	movl	$0x3, _a(%rip)
0000000100000f18	jmp	0x100000f3f
0000000100000f1a	movl	_a(%rip), %edx
0000000100000f20	movl	_b(%rip), %eax
0000000100000f26	andl	%edx, %eax
0000000100000f28	movl	%eax, _c(%rip)
0000000100000f2e	jmp	0x100000f3f
0000000100000f30	movl	_a(%rip), %eax
0000000100000f36	subl	$0x1, %eax
0000000100000f39	movl	%eax, _a(%rip)
0000000100000f3f	movl	_a(%rip), %eax
0000000100000f45	testl	%eax, %eax
0000000100000f47	jne	0x100000f30
0000000100000f49	popq	%rbp
0000000100000f4a	ret
_main:
0000000100000f4b	pushq	%rbp
0000000100000f4c	movq	%rsp, %rbp
0000000100000f4f	movl	$0x0, %eax
0000000100000f54	callq	_first
0000000100000f59	movl	$0x0, %eax
0000000100000f5e	popq	%rbp
0000000100000f5f	ret
(__DATA,__data) section
0000000100001000	01 00 00 00 02 00 00 00 03 00 00 00 

Man beachte die Zeile mit callq _first (6 letzte Zeile). Mit strip können wir den Wert von _first sichtbar machen (nachmachen):

$shell> strip a.out                                                            
$shell> otool -tdV a.out                                                       
a.out:
(__TEXT,__text) section
0000000100000eeb	pushq	%rbp
0000000100000eec	movq	%rsp, %rbp
0000000100000eef	movl	0x10f(%rip), %edx
0000000100000ef5	movl	0x10d(%rip), %eax
0000000100000efb	addl	%edx, %eax
0000000100000efd	movl	%eax, 0xfd(%rip)
0000000100000f03	movl	0xf7(%rip), %eax
0000000100000f09	cmpl	$0x4, %eax
0000000100000f0c	jne	0x100000f1a
0000000100000f0e	movl	$0x3, 0xec(%rip)
0000000100000f18	jmp	0x100000f3f
0000000100000f1a	movl	0xe0(%rip), %edx
0000000100000f20	movl	0xde(%rip), %eax
0000000100000f26	andl	%edx, %eax
0000000100000f28	movl	%eax, 0xda(%rip)
0000000100000f2e	jmp	0x100000f3f
0000000100000f30	movl	0xca(%rip), %eax
0000000100000f36	subl	$0x1, %eax
0000000100000f39	movl	%eax, 0xc1(%rip)
0000000100000f3f	movl	0xbb(%rip), %eax
0000000100000f45	testl	%eax, %eax
0000000100000f47	jne	0x100000f30
0000000100000f49	popq	%rbp
0000000100000f4a	ret
0000000100000f4b	pushq	%rbp
0000000100000f4c	movq	%rsp, %rbp
0000000100000f4f	movl	$0x0, %eax
0000000100000f54	callq	0x100000eeb
0000000100000f59	movl	$0x0, %eax
0000000100000f5e	popq	%rbp
0000000100000f5f	ret
(__DATA,__data) section
0000000100001000	01 00 00 00 02 00 00 00 03 00 00 00 

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