Content

Intel 64 Architektur

Im Folgenden betrachten wir die Rechner Architektur x86-64 (auch Intel64 oder AMD64 genannt) genauer. Genauer gesagt wird hier ein Befehlssatz spezifiziert den ein Prozessor unterstützen muss. Implementiert wird diese Architektur zum Beispiel von den AMD Prozessoren Opteron, Athlon 64, Fusion, etc. und Intel Prozessoren Core 2, Core i5, i7, etc. Ein Maschinen Programm das diesen Befehlssatz benutzt kann also unmodifiziert auf jedem dieser Prozessor ausgeführt werden.

Register

Ein Prozessor Register ist ein Speicherplatz innerhalb der CPU. Soll die CPU zwei Zahlen (a und b), die im RAM liegen addieren (z.B. b = a + b), dann muss

Allgemein nutzbare Register

Die x86-64 Architectur bietet

Ein 64-Bit Register kann auch als 32-Bit, 16-Bit Register benutzt werden. So kann man kann zum Beispiel die ersten 32 (bzw. 16) Bits direkt ansprechen. Teilweise kann man auch die ersten zwei Bytes (lower und higher 8-Bit) jeweils direkt ansprechen.

Im Assembler Code kann man dies über den Register Namen bzw. dem Anfangsbuchstaben steuern welcher Teil des Registers angesprochen wird. Zum Beispiel kann das %rax Register

Name

64-Bit

Lower 32-Bit

Lower 16-Bit

Lower 8-Bits (erstes Byte)

High 8-Bits (zweites Byte)

Accumulator

%rax

%eax

%ax

%al

%ah

Base

%rbx

%ebx

%bx

%bl

%bh

Count

%rcx

%ecx

%cx

%cl

%ch

Data

%rdx

%edx

%dx

%dl

%dh

(Frame) base pointer

%rbp

%ebp

%bp

Stack pointer

%rsp

%esp

%sp

Source index

%rsi

%esi

%si

Destination index

%rdi

%edi

%di

Integer register

%r8 - %r15

%r8d - %r15d

%r8w - %r15w

%r8b - %r15b

Spezielle Register und Flags

Ein weiteres sehr spezielles Register ist %rip. Dabei steht ip für Instruction Pointer (synonym zu Program Counter (PC)). Dieses Register wird nie direkt verändert. Es wird aber von jeder Anweisung indirekt verändert! Der Wert in %rip ist die 64-Bit Speicher Adresse mit der als nächstes auszuführenden Anweisung. Wenn eine Anweisung ausgeführt wird, dann wird normalerweise der Wert von %rip so inkrementiert, dass %rip auf die folgende Anweisung verweist. Damit werden die Anweisungen also sequentiell abgearbeitet.

Gewisse “Jump” Anweisungen können %rip aber auch so ändern, dass Anweisung nicht in sequentieller Reihenfolge ausgeführt werden.

Weitere Register, die auch Flags genannt werden (da sie nur die Werte 0 oder 1 annehmen können) sind

Carry Flag

CF

Overflow Flag

OF

Sign Flag

SF

Zero Flag

ZF

Parity Flag

PF

Adjust Flag

AF

Die Flags werden ebenfalls implizit von gewissen Anweisungen geändert bzw. gesetzt. Von anderen Anweisungen kann deren Status wiederum abgefragt werden.

Speicher Layout

Den Speicher kann man Byte weise adressieren, d.h. jedes Byte hat eine eigene Adresse. Mit 64-Bit Registern kann man also \(2^{64} \text{Byte} = 2^{54} \text{KB} = 2^{44} \text{MB} = 2^{34} \text{GB} = 2^{24} \text{TB} = 2^{14} \text{PB} = 16 \text{EB}\)1 Speicherzellen adressieren.

Litte Endian / Big Endian

Mehrer Bytes werden zu größeren Datentypen zusammengefasst, z.B.

Sobald bei einem Datentyp mehr als ein Byte im Spiel ist muss man festlegen in welcher Reihenfolge die Bytes benutzt werden. Die Zahl \(5498 = (0000157A)_{16}\) = 0x0000157A sei an der Speicheradresse \((0010 1010)_2\) = 0010 1010b in einem quad word abgespeichert. Dann gibt es zwei Möglichkeiten:

Auf der Intel Architektur wird aus historischen Gründen2 das little endian Format benutzt.

Laden eines Programms

Ausführbare Programme werden in bestimmten Formaten abgespeichert3. Die Datei enthält den ausführbaren Maschinen Code und (nicht-trivial) initialisierte Daten (Konstanten und Variablen). Außerdem enthält das Programm die Information wie viele nicht initialisierte Variablen benötigt werden.

Wird das Programm gestartet, dann werden die Maschinen Befehle in das sogenannte Text-Segment und die initialisierten Daten ins Data-Segment geladen. Für nicht-initialisierte Daten wird der sogenannte BSS Bereich reserviert und mit Null initialisiert.

In das Stack-Segment wird der Name des Programms und übergebene Argumente kopiert. Das Stack Register wird auf den Anfang des Stapels gesetzt...

Schliesslich wird das %RIP Register (der Program Counter PC) Auf den ersten Maschinenbefehl gesetzt.

Aufgaben

Disassembler

Es gibt die Möglichkeit aus einer Ausführbaren Datei wieder den Assembler Code zu rekonstruieren. Unter Mac OS X gibt es dafür zum Beispiel das Programm otool. Wir wollen dieses Tool benutzen um zu prüfen, ob das oben gesagt auch stimmt.

Folgendes Programm benutzen wir zum Demonstration (abtippen oder abspeichern):

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

int
main()
{
    c = 3;
    return a + b +c;
}

Wir übersetzen und Linken (nachmachen):

$shell> gcc-4.8 -Wall simple.c                                                 

Jetzt rufen wir den Disassembler auf (nachmachen und mittels RTFM herausfinden was die Optionen tun sollen):

$shell> otool -tdV a.out                                                       
a.out:
(__TEXT,__text) section
_main:
0000000100000f82	pushq	%rbp
0000000100000f83	movq	%rsp, %rbp
0000000100000f86	leaq	_c(%rip), %rax
0000000100000f8d	movl	$0x3, (%rax)
0000000100000f93	movl	_a(%rip), %eax
0000000100000f99	movl	%eax, %edx
0000000100000f9b	movl	_b(%rip), %eax
0000000100000fa1	addl	%eax, %edx
0000000100000fa3	leaq	_c(%rip), %rax
0000000100000faa	movl	(%rax), %eax
0000000100000fac	addl	%edx, %eax
0000000100000fae	popq	%rbp
0000000100000faf	ret
(__DATA,__data) section
0000000100001000	01 00 00 00 02 00 00 00 

RTFM: Was stellt die Ausgabe dar??

Man sieht, dass in der Ausführbaren Datei sogar noch Informationen aus dem Source File enthalten sind. Denn es tauchen die Namen der Variablen auf. Ok, bis auf den häßlichen Unterstrich (Die Namen der Variablen ändern und das Ganze Wiederholen).

Diese Meta-Informationen kann man aus der Ausführbaren Datei löschen. Symbole die dort der Lesbarkeit wegen benutzt wurden werden dann mit nackten Zahlen ersetzt. Dazu benutzt man das Tool strip (RTFM) und schaut sich nochmals das Ergebnis von otool an.

$shell> strip a.out                                                            
$shell> otool -tdV a.out                                                       
a.out:
(__TEXT,__text) section
0000000100000f82	pushq	%rbp
0000000100000f83	movq	%rsp, %rbp
0000000100000f86	leaq	0x7b(%rip), %rax
0000000100000f8d	movl	$0x3, (%rax)
0000000100000f93	movl	0x67(%rip), %eax
0000000100000f99	movl	%eax, %edx
0000000100000f9b	movl	0x63(%rip), %eax
0000000100000fa1	addl	%eax, %edx
0000000100000fa3	leaq	0x5e(%rip), %rax
0000000100000faa	movl	(%rax), %eax
0000000100000fac	addl	%edx, %eax
0000000100000fae	popq	%rbp
0000000100000faf	ret
(__DATA,__data) section
0000000100001000	01 00 00 00 02 00 00 00 

Aufschreiben:

(Nachtrag) Global Offset Table

Oben wurde aus dem Source File simple.c direkt eine ausführbare Datei erzeugt, das heisst es wurde übersetzt und gelinkt. Wenn wir den Assembler Code vor dem Linken anschauen sehen wir ein neues Schlüsselwort GOTPCREL:

$shell> gcc-4.8 -Wall -S -fno-asynchronous-unwind-tables simple.c              
$shell> cat simple.s                                                           
	.globl _a
	.data
	.align 2
_a:
	.long	1
	.globl _b
	.align 2
_b:
	.long	2
	.comm	_c,4,2
	.text
	.globl _main
_main:
	pushq	%rbp
	movq	%rsp, %rbp
	movq	_c@GOTPCREL(%rip), %rax
	movl	$3, (%rax)
	movl	_a(%rip), %eax
	movl	%eax, %edx
	movl	_b(%rip), %eax
	addl	%eax, %edx
	movq	_c@GOTPCREL(%rip), %rax
	movl	(%rax), %eax
	addl	%edx, %eax
	popq	%rbp
	ret
	.subsections_via_symbols

Dabei steht GOT für Global Offset Table, PC für Programm Counter (das ist synonym zu Instruction Pointer) und REL für Relative. Mit _c@GOTPCREL(%rip) ist also die Adresse von _c relativ zum Wert von %rip gemeint.

Uninitialisierte Daten werden wie oben erwähnt im BSS Segment angelegt. Wo das Data und BSS Segment in der ausführbaren Datei letztlich beginnt weiss man erst nach dem Linken. Für initialisierte Daten hat man aber in jedem Object File schon einmal ein Label. Damit ist klar was mit _a(%rip) gemeint ist. Da c aber nicht initialisiert ist wird für _c kein Label angelegt (Der Speicher für c wird ja erst vom Programm Lader angelegt und mit Null Initialisiert). Der Linker sammelt aber aus jedem Object File die Information welche Daten später für das BSS Segment benötigt werden. Diese werden in der GOT verwaltet.

Fussnoten