Assemblerprogrammierung Assembler und Maschinensprache Befehle und Pseudo-Befehle C-Konstrukte in Assembler übersetzt Aufrufkonventionen (ABI) Der netwide assembler nasm Binärformate Was ist ein Assembler? Ein (einfacher) Compiler. der den Code eines Assemblerprogramms in Maschinensprache umsetzt Assemblerprogramm = menschenlesbare Instruktionen Maschinenprogramm = binäre Darstellung der Instruktionen Komfortabler zu programmieren: Statt der Bitfolge 11111111 kann der Programmierer schreiben: add ax, 1 Es existiert also eine (nahezu) eindeutige Abbildung von Assemblerbefehlen zu binären Maschineninstruktionen: symbolische Bezeichnung Maschinencode add ax 11 1 (dez.) 111111 Michael Engel <michael.engel@udo.edu> 1 jede CPU-Architektur hat also ihren speziellen Assembler Michael Engel <michael.engel@udo.edu> 2 Was ist ein Assembler? Achtung zusätzlich vertauscht der Assembler noch die Reihenfolge der Bytes des Parameters: 11 1111 11 add ax low byte high byte (1. Byte) (2. Byte) (3. Byte) little endian byte order (niederwertiges Byte an kleinerer Adresse) bei x86! Unter Assembler versteht man also sowohl die symbolische Darstellung der Maschinensprachebefehle wie auch das Übersetzerprogramm, das die symbolische Darstellung in die vom Prozessor verstandene Binärdarstellung umsetzt Michael Engel <michael.engel@udo.edu> 3 Was kann ein Assembler? Im Gegensatz zu echten (Hochsprachen-)Compilern kann ein Assembler nur sehr wenige komplexe Ausdrücke umsetzen. Die Sprache, die der Assembler versteht, entspricht den im Instruktionssatz der jeweiligen CPU verfügbaren Instruktionen Manche Assembler können allerdings zur Assemblier-Zeit auch einfache Berechnungen durchführen und besitzen einen simplen Präprozessor Konstrukte höherer Programmiersprachen werden vom Compiler in einfachere Instruktionen umgesetzt: keine komplexen Anweisungen keine komfortablen Schleifen meist nur goto -Äquivalente keine strukturierten Datentypen keine Unterprogramme mit Parameterübergabe Michael Engel <michael.engel@udo.edu> 4
Übersetzungsvorgang Der Assembler steht also als Komponente zwischen Compiler und Linker Er liest vom Compiler erzeugten Assembler- Quelltext und erzeugt daraus Objektcode-Dateien, die die zugehörigen binären Maschineninstruktionen und Daten enthalten Diese Objektdateien werden (meist) vom Linker zu einem fertigen ausführbaren Programm verbunden Beispiel Die C-Anweisung summe = a + b + c + d ist für einen Assembler zu kompliziert und muss daher in mehrere Anweisungen aufgeteilt werde Der 8x86 Assembler kann immer nur zwei Zahlen addieren und das Ergebnis in einer der beiden verwendeten "Variablen" (Akkumulatorregister) speichern. Dieses C-Programm entspricht von der Struktur her also eher einem Assemblerprogramm: summe = a summe = summe + b summe = summe + c summe = summe + d Michael Engel <michael.engel@udo.edu> 5 Michael Engel <michael.engel@udo.edu> 6 Beispiel Dieses Programm summe = a summe = summe + b summe = summe + c summe = summe + d würde in 8x86-Assembler z.b. so aussehen: mov eax, [a] add eax, [b] add eax, [c] add eax, [d] Assembler unterstützten also nur primitive Operationen Die meisten Assembler arbeiten zeilenorientiert eine Zeile entspricht einer Maschinenanweisung kein Semikolon o.ä. am Ende der Zeile notwendig Kontrollanweisungen: if Einfache if-then-else-konstrukte sind für Assembler auch schon zu komplex: if ( a == 4711 ) {... } else {... } In 8x86-Assembler sieht das wie folgt aus: cmp eax, 4711 -- vergleiche eax mit 4711 jne ungleich -- ungleich -> springe gleich:... -- sonst hier weiter jmp weiter -- else-zweig auslassen ungleich:... -- else-zweig weiter:... -- danach geht's weiter Michael Engel <michael.engel@udo.edu> 7 Michael Engel <michael.engel@udo.edu> 8
Schleifen: einfache for -Schleife Simple Zählschleifen werden vom 8x86 schon besser unterstǘtzt: for (i= i <1 i++) { summe = summe + a }...in Assembler: mov ecx, 1 schleife: add eax, [a] loop schleife Der Loop-Befehl dekrementiert implizit das ecx-register und führt den Sprung nur dann aus, wenn der Inhalt von ecx anschliessend nicht ist Michael Engel <michael.engel@udo.edu> 9 Was ist ein Register? In Assembler existieren keine beliebigen Variablen. Werte als Parameter/Ergebnisse aktueller Berechnungen müssen in CPU-Registern gehalten werden Ein Register ist ein extrem schneller, sehr kleiner Zwischenspeicher innerhalb der CPU, der (beim 8x86) bis zu 32 Bits speichern kann Die eigentlichen Variablen der Hochsprache werden vom Compiler zu Speicherplätzen im Datensegment der Objektcode-Datei zugeordnet Um eine Berechnung mit Variablen durchführen zu können, muss erst der Wert aus der jew. Speicherzelle in ein Register geladen werden Nicht alle Variablen passen gleichzeitig in die wenigen Register! Es existiert also eine zeitlich sich verändernde Zuordnung Register <=> Variable Michael Engel <michael.engel@udo.edu> 1 Register im 8x86 Der 8386 besitzt folgende Register: Befehls- und Stapelzeiger IP SP Vielzweckregister AH AL BH BL CH CL DH DL SI DI BP Flag Register FLAG Segmentregister CS SS DS ES Michael Engel <michael.engel@udo.edu> 11 Code Stack Data Extra Register im 8x86 Befehls- und Stapelzeiger IP SP Vielzweckregister AH AL BH BL CH CL DH DL SI DI BP AX: Flag Accumulator Register Register arithmetisch-logische Operationen I/O FLAG kürzester Maschinencode BX: Base Address Register CX: Count Register für Segmentregister LOOP Befehl für String Operationen mit REP für Bit Shift und CSRotate Code SS Stack DX: Data Register DS Data DX:AX sind 32 Bit für MUL/DIV Portnummer für ESIN und OUTExtra SI, DI: Index Register für Array-Zugriffe (Displacement) BP: Base Pointer Jedes Vielzweckregister erfüllt seinen speziellen Zweck Michael Engel <michael.engel@udo.edu> 12
Speicher Meistens reichen Register alleine zur Lösung eines Problems nicht aus Zugriff auf den Hauptspeicher ist erforderlich Der Hauptspeicher ist wie ein riesiges Array aus Registern, die wahlweise 8, 16 oder 32 Bit breit sind Ein Byte ist die kleinste adressierbare Einheit Speicherzellen werden durchnumeriert => Index Die Zugriffsgeschwindigkeit auf den Hauptspeicher ist zwei Zehnerpotenzen langsamer als die Zugriffsgeschwindigkeit auf Register Um auf einen Wert im Hauptspeicher zugreifen zu können, muss der Programmierer den Index des Wertes im Speicher kennen. Dieser ist die Adresse des Wertes Der Hauptspeicher wird in Bytes von an aufsteigend indexiert Michael Engel <michael.engel@udo.edu> 13 Speicher Beispiel: [SECTION.data] gruss: db 'hello, world' unglueck: dw 13 million: dd 1 [SECTION.text] mov ax, [million] Michael Engel <michael.engel@udo.edu> 14 Der Stack Variablen, die festen Speicherzellen zugeordnet sind, sind von allen Teilen des Assemblerprogramms aus zugreifbar (über die Adresse oder einen zugeordneten symbolischen Namen, ein sog. Label ) => globale Var.! Für bestimmte Zwecke werden aber nicht-globale Variable benötigt Speicherschutz zwischen / Objekten Rekursiv aufrufbare Für diesen Fall wird ein Notizzettel benötigt, der die aktuellen Werte so lange behält, wie sie benötigt werden dies ist ein dynamisch allozierter Speicherbereich auf dem sog. Stack (Stapel) Der Stack ist ein Stück Hauptspeicher, auf dem nicht mit festen, sondern nur mit relativen Adressen gearbeitet wird Michael Engel <michael.engel@udo.edu> Der Stack Werte werden mit der push-operation oben auf den Stack gelegt, die pop-operation entfernt den obersten Wert wieder vom Stack dabei ist die aktuelle Adresse, an der push/pop operieren, in einem speziellen Register, dem sog. stack pointer (Register: esp) gespeichert der Programmierer muss sich aber nicht um den konkreten Wert des stack pointers kümmern man muss sich also nur die Reihenfolge merken, in der man Werte auf den Stack gelegt hat push und pop transferieren immer 32 Bits: Michael Engel <michael.engel@udo.edu> 16
Adressierungsarten Die meisten Befehle des 8x86 können Operanden wahlweise aus Registern, aus dem Speicher oder direkt einer Konstante entnehmen Beim mov-befehl sind (u.a.) folgende Formen möglich, wobei der erste Operand stets das Ziel und der zweite stets die Quelle der Operation angeben: Registeradressierung der Wert eines Registers wird in ein anderes übertragen: mov ebx, edi Unmittelbare Adressierung eine Konstante wird in ein Register übertragen: mov ebx, 1 Direkte Adressierung der Wert, der an der angegebenen Speicherstelle steht, wird ins Register übertragen: mov ebx, [1] Register-indirekte Adressierung der Wert, der an der Speicherstelle steht, die durch das zweite Register bezeichnet wird, wird in das erste Register übertragen: mov ebx, [eax] Basis-Register Adressierung: Der Wert, der an der Speicherstelle steht, die sich durch die Summe des Inhalts des zweiten Registers und der Konstanten ergibt, wird in das erste Register übertragen: mov eax,[1+esi] Michael Engel <michael.engel@udo.edu> 17 Aus den höheren Programmiersprachen ist das Konzept der Funktion oder Prozedur bekannt Der Vorteil dieses Konzeptes gegenüber einem goto besteht darin, daß die Prozedur von jeder beliebigen Stelle im Programm aufgerufen werden kann und das Programm anschließend an genau der Stelle fortgesetzt wird, die nach dem Prozeduraufruf folgt Die Prozedur selbst muß nicht wissen, von wo sie aufgerufen wurde und wo es hinterher weiter geht. Das geschieht irgendwie automatisch Nicht nur die Daten des Programms, sondern auch das Programm selbst liegt im Hauptspeicher und somit gehört zu jeder Maschinencodeanweisung eine eigene Adresse Damit der Prozessor ein Programm ausführt, muß sein Befehlszeiger auf den Anfang des Programms zeigen, also die Adresse der ersten Maschinencodeanweisung in das spezielle Register Befehlszeiger (instruction pointer, eip) geladen werden. Michael Engel <michael.engel@udo.edu> 18 Der Prozessor wird dann den auf diese Weise bezeichneten Befehl ausführen und im Normalfall anschließend den Inhalt des Befehlszeigers um die Länge des Befehls im Speicher erhöhen, so daß er auf die nächste Maschinenanweisung zeigt Bei einem Sprungbefehl wird der Befehlszeiger nicht um die Länge des Befehls, sondern entweder auf die angegebene Zieladresse umgesetzt (absoluter Sprung) oder um die angegebene relative Zieladresse erhöht oder erniedrigt Um nun eine Funktion aufzurufen, wird zunächst einmal wie beim Sprungbefehl verfahren, nur dass der alte Wert des Befehlszeigers (+ Länge des Befehls) zuvor auf den Stack geschrieben wird. Am Ende der Funktion genügt dann ein Sprung an die auf dem Stack gespeicherte Adresse, um zu dem aufrufenden Programm zurückzukehren. Beim 8x86 erfolgt das Speichern der Rücksprungadresse auf dem Stack implizit mit Hilfe des call Befehls. Genauso führt der ret Befehl auch implizit einen Sprung an die auf dem Stack liegende Adresse durch: ----- Hauptprogramm ---- main:... call f1 xy:... ----- Funktion f1 f1:... ret Michael Engel <michael.engel@udo.edu> 19 Michael Engel <michael.engel@udo.edu> 2
Wenn die Funktion Parameter erhalten soll, werden diese üblicherweise ebenfalls auf den Stack geschrieben, natürlich vor dem call Befehl. Hinterher müssen sie natürlich wieder entfernt werden, entweder mit pop, oder durch direktes Umsetzen des Stackpointers: push eax zweiter Parameter für f1 push ebx erster Parameter für f1 call f1 add esp, 8 Parameter vom Stack entfernen Michael Engel <michael.engel@udo.edu> 21 Michael Engel <michael.engel@udo.edu> 22 Um innerhalb der Funktion auf die Parameter zugreifen zu können, wird üblicherweise der Basepointer ebp zu Hilfe genommen. Wenn er gleich zu Anfang der Funktion gesichert und dann mit dem Wert des Stackpointers belegt wird, kann der erste Parameter immer über [ebp+8] und der zweite Parameter über [ebp+12] erreicht werden, unabhängig davon, wieviele push und pop Operationen seit Beginn der Funktion verwendet wurden. f1: push ebp mov ebp,esp... mov ebx,[ebp+8] 1. Parameter in ebx laden mov eax,[ebp+12] 2. Parameter in eax laden... pop ebp ret Michael Engel <michael.engel@udo.edu> 23 Michael Engel <michael.engel@udo.edu> 24
Flüchtige und nicht-flüchtige Register Flüchtige und nicht-flüchtige Register Damit von verschiedenen Stellen des Assemblerprogramms heraus aufgerufen werden können, ist es wichtig festzulegen welche Registerinhalte von der Funktion verändert werden dürfen und welche bei Verlassen der Funktion noch (oder wieder) den alten Wert besitzen müssen Am sichersten ist es natürlich, grundsätzlich alle Register, die die Funktion zur Erfüllung ihrer Aufgabe benötigt, zu Beginn der Funktion auf dem Stack zu speichern und unmittelbar vor Verlassen der Funktion wieder zu laden Michael Engel <michael.engel@udo.edu> 25 Die Assemblerprogramme, die der GNU C Compiler erzeugt, verfolgen jedoch eine etwas andere Strategie: Sie gehen davon aus, dass viele Register nur kurzfristig verwendet werden, zum Beispiel als Zählvariable von kleinen Schleifen oder um die Parameter für eine Funktion auf den Stack zu schreiben Hier wäre es reine Verschwendung, die ohnehin längst veralteten Werte zu Beginn einer Funktion mühsam zu sichern und am Ende wiederherzustellen Da man einem Register nicht ansieht, ob sein Inhalt wertvoll ist oder nicht, haben die Entwickler des GNU C Compilers einfach festgelegt, daß die Register eax, ecx und edx grundsätzlich als flüchtige Register zu betrachten sind, deren Inhalt einfach überschrieben werden darf Das Register eax hat dabei noch eine besondere Rolle: Es liefert den Rückgabewert der Funktion (soweit erforderlich) Die Werte der übrigen Register müssen dagegen gerettet werden, bevor sie von einer Funktion überschrieben werden dürfen. Sie werden deshalb nicht-flüchtige Register genannt. Michael Engel <michael.engel@udo.edu> 26 Der netwide Assembler nasm nasm: Aufruf Es existieren eine Vielzahl von Assemblern für den 8x86 Die meisten sind allerdings eher dafür gedacht, nur den Output eines Compilers in Objektcode zu übersetzen => unkomfortable Syntax, schlechte Fehlerbehandlung Diese Vielfalt führt zu unterschiedlichen Schreibweisen für Maschinenbefehle Reihenfolge Ziel Quelle oder andersherum Namen der Register (eax oder %eax) Syntax für Adressierungsarten Es gibt zwei Standards Der Aufruf von nasm hat das Format: nasm -f <format> <filename> [-o <output>] nasm -f elf myfile.asm assembliert myfile.asm zu einer ELF Objectdatei myfile.o nasm -f bin myfile.asm -o myfile.com assembliert myfile.asm zu einer rohen Binärdatei myfile.com nasm -f elf myfile.asm -l myfile.lst assembliert myfile.asm und erzeugt ein listing file, das auch die generierten Maschineninstruktionen und zugehörigen Adressen (in Hexadezimal) ausgibt intel -Standard, wird (u.a.) von nasm unterstützt AT&T -Standard, wird von gas (GNU Assembler) unterstützt GCC erzeugt AT&T-Aseembler für gas OOStuBS verwendet aber nasm und damit die (lesbarere) intel- Notation für Befehle und Parameter Michael Engel <michael.engel@udo.edu> 27 Michael Engel <michael.engel@udo.edu> 28
nasm: Listing File 1 FILE "while.c" 2 SECTION.text 3 GLOBAL f 4 GLOBAL f:function 5 f: 6 55 push ebp 7 1 89E5 mov ebp,esp 8 3 81EC1 sub esp,16 9 9 E93 jmp L2 1 L3: 11 E D165FC sal dword [ebp-4],1 12 L2: 13 11 817D8 cmp dword [ebp+8], 14 18 F9FC setg al 1B 816D81 sub dword [ebp+8],1 16 22 84C test al,al 17 24 75E8 jne L3 18 26 8B45FC mov eax, [ebp-4] 19 29 C9 leave 2 2A C3 ret Michael Engel <michael.engel@udo.edu> 29 nasm: Instruktionssyntax Eine Zeile in nasm-assemblerquelltext: label: instruction operands comment De meisten der Felder der Zeile sind optional Jede Kombination von Label, Instruktion und Kommentar ist erlaubt, das Vorhandensein des Operanden ist abhängig von der jew. Instruktion Zeilen können mit '\' am Ende fortgesetzt werden Michael Engel <michael.engel@udo.edu> 3 nasm: Pseudo-Instruktionen nasm: Pseudo-Instruktionen Pseudo-Instruktionen dienen dazu, in Assembler Daten (Speicherbereiche) zu deklarieren und optional mit einem Namen (label) zu versehen Initialisierte Daten werden zur Assemblier-Zeit mit den angegebenen Werten vorbelegt, Datentypen sind Byte (db), 16-Bit-Wort (dw), 32-Bit- Double wort (dd) und 64-Bit Quad wort (dq): db x55 just the byte x55 db x55,x56,x57 three bytes in succession db 'a',x55 character constants are OK db 'hello',13,1,'$' so are string constants dw x1234 x34 x12 dw 'a' x61 x (it's just a number) dw 'ab' x61 x62 (character constant) dw 'abc' x61 x62 x63 x (string) dd x12345678 x78 x56 x34 x12 dd 1.234567e2 floating-point constant dq x123456789abcdef eight byte constant dq 1.234567e2 double-precision float dt 1.234567e2 extended-precision float Uninitialisierte Daten reservieren nur einen Speicherbereich, ohne einen Inhalt vorzugeben. Auch hier können wieder Byte, Wort, Doppelwort und Quadwort als Basisgrößen verwendet werden: buffer: resb 64 reserve 64 bytes wordvar: resw 1 reserve a word realarray resq 1 array of ten reals Externe Binärdateien können auch eingefügt werden: incbin "file.dat" incbin "file.dat",124 incbin "file.dat",124,512 include the whole file skip the first 124 bytes skip the first 124, and actually include at most 512 Michael Engel <michael.engel@udo.edu> 31 Michael Engel <michael.engel@udo.edu> 32
nasm: Pseudo-Instruktionen Assemblercode in OOStuBS nasm erlaubt auch die Definition von Konstanten: equ definiert ein Symbol mit einem angegebenen konstanten Wert. Der Name der Konstanten ist gleich dem Label der Zeile (diese muss also ein Label enthalten) Die Zuordnung ist absolut und kann nicht nachträglich geändert werden message db 'hello, world' msglen equ $-message Hier wird eine Konstante msglen mit dem Wert 12 definiert. Das Zeichen $ in einem Ausdruck steht dabei für die aktuelle Adresse, die dem Label zugeordnet ist. Startup Code: boot/bootsect.asm und boot/startup.asm Bootsektor ein Block (512 Bytes), die in den ersten Sektor der Diskette geschrieben werden Real Mode Code 16 Bit Lädt Startup-Code und das eigentliche OOStuBS von Diskette......und springt zum Anfang von startup.asm Startup-Code wird vom Bootsektor geladen bereitet System für die Ausführung von C++-Code vor initialisiert Segmentregister schaltet Interrupts ab setzt IDT und GDT aktiviert die Adressleitung A2 schaltet in den protected mode um und springt schließlich ins Hauptprogramm zu main Michael Engel <michael.engel@udo.edu> 33 Michael Engel <michael.engel@udo.edu> 34 Beispiel: Umschaltg. Protected Mode Beispiel: Umschaltg. Protected Mode Bevor in den 32-Bit protected mode umgeschaltet werden kann, müssen die Segmentdeskriptoren vorbereitet werden Dies kann im Real Mode geschehen, da dieser Segmente nicht über Tabellen verwaltet Initialisierung der GDT Constants SETUPSEG equ x9 Setup-Code (max. 64K incl. Stack) SYSTEMSEG equ x1 System-Code (max. 512K) Die GDT für OOStuBS sieht so aus: [SECTION.data] Descriptor Tables gdt: dw,,, NULL Descriptor dw xffff 4Gb - (x1*x1 = 4Gb) dw x base address= dw x9a code read/exec dw xcf granularity=496, 386 (+5th nibble of limit) [SECTION.text]... set GDT lgdt [gdt_48]... Michael Engel <michael.engel@udo.edu> 35 dw xffff 4Gb - (x1*x1 = 4Gb) dw x base address= dw x92 data read/write dw xcf granularity=496, 386 (+5th nibble of limit) gdt_48: dw x18 GDT Limit=24, 3 GDT Entries dd SETUPSEG*x1+gdt Physical Address of the GDT Michael Engel <michael.engel@udo.edu> 36
Beispiel: Umschaltg. Protected Mode Beispiel: Umschaltg. Protected Mode Die Umschaltung in den Protected Mode erfolgt dann über das Beschreiben des Maschinen-Statusworts mit dem Wert 1 und einen speziellen Inter-Segment-Sprung. Zuvor wird noch die Befehlsqueue der CPU mit einem Sprungbefehl (jmp flush_instr) geleert. switch into protected mode mov lmsw ax,1 ax jmp flush_instr flush_instr: jump to protected mode part of the system jmp dword x8:systemseg*x1 Wohin wird an dieser Stelle aber gesprungen? Wohin wird an dieser Stelle aber gesprungen? jmp dword x8:systemseg*x1...an die Adresse SYSTEMSEG * 16 im Segment, das durch den Deskriptor x8 beschrieben ist... SYSTEMSEG equ x1 System-Code (max. 512K) also x1 * x1 = x1 aber es gibt keinen Segmentdeskriptor x8?!? Nur die oberen 5 Bit der Nummer sind die Nummer des Deskriptors: x8 = 1 Obere 5 Bit = 1, also Segmentdeskriptor 1 - Codesegment: dw xffff 4Gb - (x1*x1 = 4Gb) dw x base address= dw x9a code read/exec dw xcf granularity=496, 386 (+5th nibble of limit) Michael Engel <michael.engel@udo.edu> 37 Michael Engel <michael.engel@udo.edu> 38 Beispiel: Umschaltg. Protected Mode Beispiel: Umschaltg. Protected Mode Was steht aber nun an der Adresse x1 im Code-Segment? Das wird durch den Linker bestimmt Angaben zum Aufbau der Binärdatei von OOStuBS in Datei src/sections: SECTIONS {. = x1 /* Startadresse des Systems */....text : { *(".text") *(".text$") *(".init") *(".fini") *(".gnu.linkonce.*") } src/sections beschreibt auch die Lage der Daten:.data : { *(".data") *(".data$") *(".rodata") CTOR_LIST =. *(".ctors") *(".ctor") CTOR_LIST_END =. DTOR_LIST =. *(".dtors") *(".dtor") DTOR_LIST_END =. *(".got") *(".got.plt") *(".eh_frame") *(".eh_fram") *(".jcr") } Michael Engel <michael.engel@udo.edu> 39 Michael Engel <michael.engel@udo.edu> 4