Die Assemblerprogrammierung der Prozessoren 80 ×86/Pentium Bernd Dumbacher 18. Juni 2004 Zusammenfassung Dieses Manuskript startet mit einem einfachen Assemblerprogramm, das in den Rechnerübungen bearbeitet und modifiziert wird. Mit ihm soll der Einstieg in die - für Hochsprachenprogrammierer ganz andere Welt der Assemblerprogrammierung gewagt werden. Die in diesem Programmbeispiel angeschnittenen Themen wie Speicherverwendung, Adressierung, Datendefinitionen, Assemblerbefehle und Struktur von Assemblerprogrammen werden in den folgenden Abschnitten ausführlich und systematisch behandelt. Danach folgen weiterführende Aspekte wie Programmsteuerung, Prozeduren, Dateienhandhabung, Bitmanipulation und Stringverarbeitung. Schließlich wird auf die Verbindung von Assemblerund Hochsprachenprogrammen, die Programmierung des mathematischen Koprozessors und die Steuerung von peripheren Geräten über I/O-Ports eingegangen. Der Abschnitt über die prozessorabhängigen Teile des Assemblers ist direkt aus [1] übernommen. 1 Inhaltsverzeichnis 1 Einleitung 5 2 Erstes Programmbeispiel 6 3 Primärer Speicher 3.1 Speichertechnologien . . . . . . . . . . . . . . . 3.2 Register . . . . . . . . . . . . . . . . . . . . . . 3.3 Arbeitsspeicher . . . . . . . . . . . . . . . . . . 3.3.1 Schreiben und Lesen im Arbeitsspeicher, 3.3.2 Schutzmaßnahmen im Arbeitsspeicher . 3.4 Stack . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Initialisierung des Stacks . . . . . . . . . 3.4.2 Benutzung des Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . little endian . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 8 9 10 11 11 12 12 13 4 Erstellung eines Programms 4.1 Einführung . . . . . . . . . . . . . . . . . . . 4.2 Grundstruktur eines Assemblerprogramms . 4.3 Erstellung eines lauffähigen Programmmoduls 4.3.1 Ablaufdiagramm . . . . . . . . . . . . 4.3.2 Editieren . . . . . . . . . . . . . . . . 4.3.3 Assemblieren . . . . . . . . . . . . . . 4.3.4 Linken . . . . . . . . . . . . . . . . . . 4.4 Werkzeuge zur Programmentwicklung . . . . 4.4.1 Der GNU-Debugger . . . . . . . . . . 4.4.2 Spezielle Werkzeuge . . . . . . . . . . 4.5 Assembleranweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 15 16 16 16 16 17 18 18 19 20 5 Adressierung 5.1 Direkte Adressierung . . . . . . . . 5.2 Indirekte Adressierung . . . . . . . 5.3 Indizierte Adressierung . . . . . . . 5.4 Parameterübergabe in Prozeduren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 22 23 23 25 6 Datendefinitionen 6.1 Adressierung der Datenblöcke . . . . . . . . . . . . 6.2 Strukturierung der Datenblöcke . . . . . . . . . . . 6.3 Arithmetik für Adressen, Konstante und Festwerte 6.4 Konflikte bei Datenformaten . . . . . . . . . . . . 6.4.1 Operandengrößen-Zusatz (OpSize-Suffix) . 6.4.2 Direkte Umwandlung (Erweiterung) . . . . 6.4.3 Erweiterndes Kopieren . . . . . . . . . . . 6.5 Direktiven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 28 29 30 30 30 30 31 31 7 Allgemeine Befehle 7.1 Datenmanipulation . . . . . . . . . 7.2 Arithmetikbefehle . . . . . . . . . 7.2.1 Arithmetik-Flaggen . . . . 7.2.2 Addition, Subtraktion . . . 7.2.3 Multiplikation und Division . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 33 34 34 35 36 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Programmsteuerung 8.1 Labels und flag-Register . . . . . . . . . . . . . . . . . . . 8.2 Sprünge . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.2.1 Unbedingter Sprung . . . . . . . . . . . . . . . . . 8.2.2 Bedingter Sprung . . . . . . . . . . . . . . . . . . . 8.3 Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.3.1 Fuß- und kopfgesteuerte Schleifen, Verzweigungen 8.3.2 loop-Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 39 39 40 40 42 42 43 9 Prozeduren 9.1 Prozedur und Programm . . . . . 9.2 Aufbau einer Prozedur . . . . . . 9.3 Aufruf . . . . . . . . . . . . . . . 9.4 Parameterübergabe . . . . . . . . 9.4.1 Grundsätzliches . . . . . . 9.4.2 Register . . . . . . . . . . 9.4.3 Stack . . . . . . . . . . . 9.5 Lokale Variable . . . . . . . . . . 9.6 Rücksprung . . . . . . . . . . . . 9.7 Beispiel . . . . . . . . . . . . . . 9.8 Makros . . . . . . . . . . . . . . 9.9 Vergleich Prozedur/Macro . . . . 9.10 Bibliotheken . . . . . . . . . . . . 9.10.1 Prozeduren . . . . . . . . 9.10.2 Makros . . . . . . . . . . 9.11 Programmaufruf mit Parametern 9.12 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 45 46 47 48 48 48 49 49 49 49 50 51 52 52 52 52 53 10 Handhabung von Dateien 10.1 Interruptverarbeitung . 10.2 Zugriff auf Dateien . . . 10.3 Beispiele . . . . . . . . . 10.4 Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 56 57 57 59 11 Bitmanipulation 11.1 Logische Verknüpfungen . . . . . . . . . . . . . 11.2 Testfunktionen . . . . . . . . . . . . . . . . . . 11.3 Schiebeoperationen . . . . . . . . . . . . . . . . 11.3.1 Überblick . . . . . . . . . . . . . . . . . 11.3.2 Shift Left- und Shift Right-Operationen 11.3.3 Shift Arithmetic Left/Right (sal, sar) . 11.3.4 Rotate-Befehle (rol, ror, rcl, rcr) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 60 61 62 62 62 63 63 12 Stringverarbeitung 12.1 Einfache Stringbefehle . . . . . . . . . . . . . . . . . . . . . . . 12.2 Wiederholungsbefehle . . . . . . . . . . . . . . . . . . . . . . . 12.3 Übersetzungsbefehl xlat . . . . . . . . . . . . . . . . . . . . . . 64 64 64 66 . . . . . . . . . . . . 3 . . . . . . . . 13 Hochsprachenprogramme 13.1 Inline-Assemblerbefehle . . . . . . . . . . . . . . . . . . . . . . 13.2 Verbindung von Assembler- und Hochsprachenmodulen . . . . 13.3 GNU-Konventionen . . . . . . . . . . . . . . . . . . . . . . . . . 67 67 70 71 14 Programmierung des Koprozessors 14.1 Ganze Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.2 Gleitkommazahlen . . . . . . . . . . . . . . . . . . . . . . . . . 14.2.1 Mantisse und Exponent . . . . . . . . . . . . . . . . . . 14.2.2 Lage des Dezimalpunktes . . . . . . . . . . . . . . . . . 14.2.3 Darstellung im Koprozessor 80 ×87 . . . . . . . . . . . 14.3 Die Architektur des 80 ×87-Koprozessors . . . . . . . . . . . . 14.4 Befehle, Formate . . . . . . . . . . . . . . . . . . . . . . . . . . 14.5 Koordinierung des Speicherzugriffes von CPU und Koprozessor 14.6 Erzeugung von Gleitkommazahlen . . . . . . . . . . . . . . . . 72 72 72 72 72 72 74 74 77 78 15 Input/Output Ports 15.1 Adressraum der I/O Ports . . . . . . . . . . . . . . . . . . . . . 15.2 Adressierung der I/O Ports . . . . . . . . . . . . . . . . . . . . 15.3 Schutzmechanismen beim Zugriff auf die I/O Ports . . . . . . . 15.4 Beispiel: Tonausgabe mit dem Systemlautsprecher über I/O Ports 15.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . 79 79 80 80 81 84 16 80 386 Dependent Features 16.1 AT&T Syntax versus Intel Syntax 16.2 Opcode Naming . . . . . . . . . . . 16.3 Register Naming . . . . . . . . . . 16.4 Opcode Prefixes . . . . . . . . . . 16.5 Memory References . . . . . . . . . 16.6 Handling of Jump Instructions . . 16.7 Floating Point . . . . . . . . . . . 16.8 Writing 16-bit Code . . . . . . . . 16.9 Notes . . . . . . . . . . . . . . . . 85 85 85 86 86 87 88 88 89 89 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Direktiven 91 18 Systemfunktionen 95 4 1 Einleitung Vorrangiges Ziel der Lehrveranstaltung maschinenorientrierte Programmierung ist die Fähigkeit zur selbständigen Erstellung von Assemblerprogrammen. Diesem Ziel sollen die Vorlesung und die praktischen Übungen am Rechner dienen. In der Vorlesung wird ein vertiefter Einblick in die Arbeitsweise des verwendeten Rechnersystems geboten. Damit soll das Arbeiten am Rechner vorbereitet und intensiviert werden. Da von Anfang an der praktischen Arbeit am Rechner oberste Priorität gilt, muss im Zweifelsfall die systematische Vermittlung des Lehrstoffs hinter den Bedürfnissen der Programmierübungen zurückstehen. Die Lehrinhalte werden in erster Linie in der zeitlichen Folge vermittelt, die durch die Programmierübungen am Rechner gefordert wird. Die praktischen Übungen finden an Linux-Rechnern statt und benutzen den GNU-Assembler, der in der Linux-Distribution enthalten ist. Für die Lehrveranstaltung maschinenorientierte Programmierung (Assemblerprogrammierung) werden Kenntnisse von Grundlagen der Informatik vorausgesetzt. Diese beinhalten Kenntnisse des Aufbaus und der Arbeitsweise des Rechners (von-Neumann-Rechner), insbesondere des Zusammenwirkens von Prozessor und Speicher und der Informationsdarstellung im Rechner. Diese sind z. B. bei [20] und der dort genannten Literatur zu finden. Daneben werden Grundkenntnisse des Betriebssystems Linux, insbesondere der wichtigsten Kommandos und Gewandheit im Umgang mit einem Texteditor, möglichst dem Editor emacs, erwartet. 5 2 Erstes Programmbeispiel .text start: MSG: LMSG: .globl movl movl leal movl int start $4,%eax $1,%ebx MSG,%ecx LMSG,%edx $0×80 movl int $1,%eax $0×80 .data .ascii .long ’’Hallo, how are you?’’ .-MSG Ergebnis dieses Programms ist die Ausgabe der Zeichenkette Hallo, how are you?. Das Quellprogramm besteht aus den beiden Bereiche .text und .data. Mit .data wird ein Bereich (Segment) im Arbeitspeicher festgelegt, der zum Lesen und zum Schreiben verwendet werden kann, für den mit .text definierten Bereich besteht nur Leseberechtigung. Das .text-Segment bietet sich demnach für Assemblerbefehle und Konstante , das .data-Segment für Variable an. Dies gilt auch für obiges Programmbeispiel. Diese Aufteilung dient dem Schutz von Programmteilen und Konstanten gegen irrtümliche Beschädigung. Beim Start wird dem Programm vom Betriebssystem Platz im Arbeitsspeicher für .text- und .data-Segment zugewiesen. • .data-Segment In obigem Beispiel wird im .data-Segment eine Zeichenkette definiert. MSG ist der Name (die Marke, das Label, das Symbol) für das erste Byte der Zeichenkette. Später wird dieser Name durch die reale Adresse (eine Zahl) ersetzt. Die Direktive .ascii bewirkt, dass die zwischen Hochkommas stehende Zeichenkette Zeichen für Zeichen nach der Ascii-Tabelle codiert wird. Die Ascii-Codes werden hintereinander ab der Adresse MSG in den Bytes des Arbeitsspeichers abgelegt. An die Zeichenkette schließt sich ein Doppelwort (.long reserviert 4 Byte) an, in dem das Ergebnis des mathematischen Ausdrucks .-MSG (“Punkt minus MSG”) gespeichert wird. Das Zeichen “.” (Punkt) befragt den location counter. Der location counter (Platzzähler) zählt die Belegung des Arbeitsspeichers mit. Im obigen Beispiel hat der location counter an dieser Stelle den Wert 19. Da die Adressen ab 0 gezählt werden, ist dies die nächste freie Adresse. Der Ausdruck .-MSG liefert als Differenz location counter - Anfangsadresse der Zeichenkette genau die Länge der Zeichenkette. Bei Änderungen in der Zeichenkette wird die Länge immer korrekt angepasst. • .text-Segment Im .text-Segment erscheint zunächst die Adresse start:. Diese Adresse 6 wird vom Betriebssystem benötigt. An dieser Stelle beginnt die Abarbeitung des Programmteils. Die nächste Zeile soll noch zurückgestellt und später besprochen werden. Die nachfolgenden Befehle lauten movl und leal. Mit movl wird der erste Operand in den zweiten Operanden kopiert (nicht verschoben). Mit movl $4, %eax wird beispielsweise der Festwert 4 in das Register eax kopiert. $ kündigt einen Festwert an, % ein Register. Der Befehl leal MSG,%ecx (lea=load effective address) ermittelt die Adresse der Marke MSG im Arbeitsspeicher und speichert sie im Register ecx. Der angehängt Buchstabe l (OpSize-Suffix) zeigt an, dass Datentypen long (4 Byte) verwendet werden. Besondere Aufmerksamkeit soll dem Befehl int $0×80 gelten. Mit diesem Befehl wird ein Satz von Funktionen angesprochen, der vom Betriebssystem zur Verfügung gestellt wird. Diese Funktionen werden SystemCalls genannt. Sie sind mit 0 beginnend fortlaufend nummeriert (siehe Tabelle 33). Um einen ganz bestimmten der insgesamt 164 SystemCalls auszuwählen, müssen einige Informationen übermittelt werden: 1. Die Nummer des gewünschten SystemCall muss ins Register eax kopiert werden. Eine 1 im Register eax wählt den SystemCall exit, d. h. Programmende aus. Die im Programm ebenfalls verwendete 4 in eax wählt den SystemCall write, die Ausgabe einer Zeichenkette. Während für Programmende keine weiteren Informationen benötigt werden, müssen für die Ausgabe einer Zeichenkette noch Angaben über die Zeichenkette und das Ziel der Ausgabe gemacht werden. 2. Die Ausgabe kann grundsätzlich auf Monitor, Drucker, Festplatte, etc. erfolgen. Eine 1 im Register ebx führt zu einer Ausgabe auf dem Monitor (StandardOut). 3. Das Objekt der Ausgabe, die Zeichenkette, wird durch die Anfangsadresse der Zeichenkette im Arbeitsspeicher und die Länge der Zeichenkette (Anzahl der Bytes) festgelegt. Die Anfangsadresse hat in ecx, die Anzahl der auszugebenden Bytes in edx zu stehen. Wenn die Register mit den Informationen belegt sind, kann der gewünschte SystemCall mit int $0×80 gestartet werden. Aus der mit einem Editor (möglichst emacs) erstellten, für den Programmierer lesbaren Quelldatei, muss eine für den Prozessor verständliche, ausführbare Datei gemacht werden. Dies ist in Abschnitt 4.3 näher erläutert. Dort sind auch die dazu erforderlichen Kommandos dargestellt. 7 3 Primärer Speicher Der im Rechner vorhandene Speicher wird in primären und sekundären Speicher eingeteilt. Der primäre Speicher (Arbeitsspeicher (Memory), Cache, Festwertspeicher, Register) ist überwiegend flüchtig (nicht-permanent), gestattet aber schnelle Zugriffe (einige 10 ns und weniger). Der sekundäre Speicher dient als permanenter Speicher zur Langzeitspeicherung. Er wird auch als externer oder peripherer Speicher bezeichnet. Hierzu zählen magnetische Medien (Festplatte, Floppy Disk, Magnetband, Kassette), magnetooptische Medien (MO-CD), optische Medien (CD-ROM) und auch ältere Medien wie Lochstreifen und Lochkarten. 3.1 Speichertechnologien Der Schreib-Lesespeicher (RAM) ist ein flüchtiger Speicher. Er wird entweder als dynamischer RAM D a t e n l e i t u n g A d r e s s le itu n g D a te n le itu n g (DRAM) in MOS-Technologie oder als statischer RAM (SRAM) in Bipolartechnologie realisiert. Die G a te G a te MOS-Technologie gestattet eine extrem hohe PaS D D S ckungsdichte der DRAM-Speicherzellen, dadurch werden die Nachteile (destruktives Lesen, refresh-mode) S p e ic h e r k o n d e n s a to r überkompensiert. Der Speicherzustand des Grundelements (siehe Abbildung 1) wird durch den Ladezustand des Kondensators festgelegt. Die Ladung des Speicherkondensators gibt die beiden Zustände Bit 0 (Kondensatorspannung UC unterhalb eines bestimm- Abbildung 1: Vereinfachtes Schaltten Wertes U1 z. B. 0,5 V) oder Bit 1 oberhalb eines bild einer DRAM-Speicherzelle: die bestimmten Wertes U2 z. B. 5 V) wieder. Die Feld- Anschlüsse des Feldeffekttransistors effekttransistoren haben die Funktion von Ventilen. sind neben Gate Source und Drain. Durch eine Spannung am Gate-Anschluss werden die in Abbildung 1 gestrichelt gezeichneten Strecken (Source-Drain-Strecke) leitend gemacht und damit eine elektrisch leitende Verbindung zwischen den Datenleitungen und dem Speicherkondensator hergestellt. Dieser Vorgang wird Adressieren genannt. In diesem Zustand kann der Speicherkondensator entladen oder geladen werden (Schreiben) oder die Ladung gemessen werden (Lesen). In SRAMs bestehen die einzelnen Speicherzellen (siehe Abbildung 2) aus bistabilen Multivibratorschaltungen (Flip-Flops). Die symmetrische Schaltung kann zwei stabile Zustände einnehmen (Bit 0 und 1). In einem Fall liegt der Punkt P auf hohem Potenzial, Q auf niedrigem Potenzial (z. B. Bit 1), im anderen Fall ist es umgekehrt (Bit 0). Beim Adressieren werden durch Änderung der Spannung auf der Adressleitung die beiden Dioden geöffnet. Damit wird das Lesen ( = Messen der Spannungen) über die Datenleitung und Schreiben ( = gewünschten Zustand herstellen) über die Datenleitung D ermöglicht. Die Bipolartechnologie bedingt einen deutlich nied2: Vereinfachrigeren Integrationsgrad, SRAMs haben aber wegen Abbildung tes Schaltbild einer SRAMdes schnelleren Zugriffes und der fehlenden refreshSpeicherzelle Zeiten Geschwindigkeitsvorteile. Dementsprechend wer- 8 Abbildung 3: Registerstruktur der Intel PC-Prozessoren ab dem Typ 80368 mit der Aufteilung der 32-Bit-Register des Intel PC-Prozessores in 16- und 8-Bit Einheiten den DRAMs für Arbeitsspeicher und SRAMS für schnellen Cache eingesetzt. Die physikalische Struktur des Arbeitsspeichers besteht in einer linearen Anordnung der Grundelemente, der Bytes. Jedes Byte besteht aus 8 Speicherzellen, die eine gemeinsame Adressleitung haben. Jedem Byte ist eine individuelle Zahl zugeordnet, über die es angesprochen werden kann. Diese Zahl wird als Adresse bezeichnet. Die Adressen beginnen bei 0000 0000 und enden z.B. bei 0fff fffc bei einem Arbeitspeicher von 256 MByte (Zahlenangaben hexadezimal). Im Arbeitsspeichers liegen für Benutzer zugängliche Speicherbereiche für Programm-Code, Daten, Puffer, Stack ect. und reservierte Speicherbereiche für Betriebssystem, Interruptadressen, Tastaturpuffer, Videospeicher, BIOS-ROM ect. Als Register wird ein interner Speicher des Prozessors bezeichnet, der zur Zwischenspeicherung von Daten bei der Bearbeitung von Programmen dient. Dabei handelt es sich dabei um eine eng begrenzte Zahl von besonders schnellen Speicherzellen. Lesespeicher (ROM) wird als Festwertspeicher bei PCs zur Speicherung von Startprogrammen oder von Basisprozeduren benutzt (z. B. BIOS). 3.2 Register Die ersten Versionen der Intel PC-Prozessoren waren mit 16-Bit-Registern ausgestattet, die die Namen ax, bx, cx, dx, si, di, bp, sp ip und eflag trugen (siehe Abbildung 3). Die Register ax, bx, cx und dx waren und sind zweigeteilt. Sie können als 16-Bit-Register, aber auch als 8-Bit-Register ah, al, bh, bl, ch, cl, dh, dl angesprochen werden. Ab dem Prozessor 80386 sind die oben genannten Register auf 32 Bit erweitert. Der Vorsatz e der Registernamen steht für exten- 9 ded. Damit verfügt der Intel PC-Prozessor heute über die 32-Bit-Register eax, ebx, ecx, edx , von denen Teile als 16- oder 8-Bit-Register verwendet werden können, und über die 32-Bit-Register esi, edi, ebp, esp, eip und eflag, deren niederwertige Teile auch als 16-Bit-Register angesprochen werden können (siehe Abbildung 3). Der GNU-Assembler as benutzt im Gegensatz zur Intel-Notation des MicrosoftAssemblers die sogenannte AT&T-Notation. Die Register des Intel PC-Prozessors werden in der AT&T-Notation mit dem Zeichen % vor dem Registernamen bezeichnet, z. B. meint %esi das 32-Bit-Register esi, %al das niederwertige Byte des 32-Bit-Registers eax. 3.3 Arbeitsspeicher Der Arbeitsspeicher (Hauptspeicher, Memory) besteht aus einer Anordnung von ByteSpeicherzellen. Jeder Speicherzelle ist eine Adresse zugeordnet, so dass der Arbeitsspeicher eine lineare Anordnung von gleichartigen und gleichberechtigten 8-Bit-Speicherzellen darstellt. Er ist von 0 bis zum Ende des Adressraums durchnummeriert (siehe Tabelle 1). Um eine Speicherzelle anzusprechen (zu adressieren), müssen zwei Maßnahmen erfolgen: 32-Bit-Adresse hexadezimal ffff ffff ffff fffe ffff fffd . . . . . . • Die Nummer der gewünschten Zelle muss in einem Register, dem Adressregister enthalten sein. • Die zur Zelle gehörigen Adressleitungen müssen durch den Adressdekoder aktiviert werden. Speicherbyte = 8 1-Bit-Zellen . . . . . . . . . . . . . . . . . . 0000 0002 0000 0001 0000 0000 . . . . . . . . . . . . Im 32-Bit Adressregister können Adressen von 0 bis 232 − 1 gespeichert werden. Damit Tabelle 1: Lineare Anordnung im durchkann also ein Bereich (Adressraum) von 4 GByte nummerierten Arbeitsspeicher. Je nach adressiert werden. Das Adressregister kann vom Ausbau eines Rechners ist ein Teilbereich Benutzer nicht direkt angesprochen werden. des maximalen Adressraums realisiert. Ein Schaltkreis im Prozessor, der Adressdecoder wählt auf Grund der Adresse (=Zahl im Adressregister) die Adressleitung zur angesprochenen Speicherzelle aus. Zur Adressierung eines bestimmten Adressraumes ist eine der Größe des Adressraums entsprechende Anzahl von Adressleitungen erforderlich. Möglichkeiten, die Anzahl der physikalischen Leitungen zu reduzieren, sind in [22] und [23] angegeben. Für den Zugriff auf den Arbeitsspeicher werden häufig die zwei folgenden Befehle benutzt (siehe auch Abschnitte 2 und 7). Mit dem Befehl movl NUM1,%ebx wird der Inhalt des Doppelworts NUM1 in das Register ebx kopiert (l=long) und mit leal NUM1,%ebp 10 wird die Adresse des Doppelworts NUM1 in das Register ebp kopiert. Für die Ansprache von Wörtern oder Bytes werden die Endungen w bzw. b benutzt. 3.3.1 Schreiben und Lesen im Arbeitsspeicher, little endian Ein großer Teil der Aktivitäten des Prozessors besteht im Transfer von Daten zwischen dem internen Speicher (Register) und dem Arbeitsspeicher. Dies wird als Speichern bzw. Laden (aus Sicht des Prozessors) bezeichnet. Der Transfer wird in zwei Schritten ausgeführt: • Transfer vom Prozessor in den Arbeitsspeicher (Speichern) ◦ Durch ein erstes Signal aktiviert der Prozessor über die Adressleitungen das zu belegende Byte. Dazu werden die Gates der Speicherzellen geöffnet. ◦ Durch ein zweites Signal wird die zu speichernde Information über die Datenleitungen in die aktivierte Zelle übermittelt. Dabei laden die auf den Datenleitungen liegenden Spannungen (low/high) wegen der geöffneten Gates die Speicherkondensatoren (die eigentlichen Träger der Informationen) auf die entsprechende Spannung auf. • Transfer vom Arbeitsspeicher in den Prozessor ◦ Durch ein erstes Signal aktiviert der Prozessor über die Adressleitungen das zu lesende Byte. Dazu werden die Gates der Speicherzellen geöffnet. ◦ Durch ein zweites Signal wird die gespeichernte Information über die Datenleitungen gelesen. Dabei werden die wegen der geöffneten Gates auf den Datenleitungen liegenden Spannungen (low/high) der Speicherkondensatoren gemessen. Aus mehreren Bytes bestehende Datenblöcke wie Wörter (2 Bytes), Doppelwörter (4 Bytes) oder weitere Datenblöcke (siehe Abschnitt 6) werden im Intel PC-Prozessor im little endian-Modus gespeichert. Das bedeutet, dass das niederwertige Byte des Datenblocks bei niedriger Adresse abgelegt wird. Dies ist in Tabelle 2 gezeigt. An der Adresse 0a100 steht das Wort 0x27a1 (=10.145), dabei das niederwertige Byte 0xa1 an der niedrigeren Adresse 0a100, das höherwertige Byte an der höheren Adresse 0a101. Ab der Adresse aa100 ist das Doppelwort 0x1f33702a (=523.464.746) in gleicher Weise gespeichert. 3.3.2 Schutzmaßnahmen im Arbeitsspeicher Unter Linux arbeitet der Prozessor im protected mode. Dabei kann der gesamte Arbeitsspeicher (virtuell 4 GByte) durch 32-Bit-Register linear adressiert werden. Es gibt keine Einteilung in 64-kByte Segmente, wie es bei MSDOS wegen der Abwärtskompatibilität zum ersten 64-kByte-Arbeitsspeicher-PC nötig war. Die Verwendung von Segmenten oder Sektionen (sections, segments) unter Linux verfolgt einen anderen Zweck. Bei MSDOS konnte der gesamte Arbeitspeicher von 1 MByte ungeschützt angesprochen werden. Es bestand keinerlei Schutz gegen Überschreiten von Segmentgrenzen. Wenn z. B. durch fehlerhafte Adressierung der Bereich des Datensegments überschritten und dabei 11 Daten in den Bereich des Programmsegments geschrieben wurden, so wurde dies durch keine Schutzmaßnahmen verhindert. Dies führte i. a. zu einem Absturz des Rechners. Bei Linux als Multiuser-/Multitasking-Betriebssystem dienen Segmente zur Abgrenzung und zum Schutz gegen missbräuchliche Zugriffe von außen. In diesem Sinn enthalten die Segmentregister im protected Adressen Daten mode nicht die Anfangsadressen der Segmente, sondern als Segment.... .... selektoren Informationen über die Lage und die Länge der Segmente aa103 1f und über die Privilegierungsniveaus. aa102 33 Das GNU-Programmiersystem (GNU-Linker) benutzt die folgenaa101 70 den Segmente, die durch Direktiven definiert werden. Ein Überschreiaa100 2a ten der Segmentgrenzen führt zu einem Programmabbruch mit der .... .... Meldung unerlaubter Speicherzugriff bzw. segmentation fault. .... .... 0a101 27 • .text-Segment 0a100 a1 Das Segment .text dient zur Aufnahme des Programmcodes und .... .... der Konstanten. Der Versuch, in diesem Segmentbereich Spei.... .... cherinhalte zu verändern, erzeugt eine Programmabbruch mit 00000 .... der Fehlermeldung unerlaubter Speicherzugriff oder segmentation fault. Tabelle 2: Speicherung von Datenblöcken (Die Zah• .data-Segment len sind in hexadezimaler Das Segment .data dient der Aufnahme von Variablen. Es ist Form angegeben.) auch möglich, Programmcode in diesen Segmentbereich zu speichern, jedoch besteht dann kein Schutz gegen Überschreiben. • .bss-Segment Das bss-Segment dient zu Aufnahme von Datenbereichen, die von verschiedenen Modulen gemeinsam genutzt werden (Common-Bereiche). .bss-Segmente werden durch die Direktiven .comm und .lcomm angelegt. 3.4 Stack Der Stack (“Stapel”) ist physikalisch im Arbeitsspeicher realisiert. Der Zugriff auf Stackspeicherplätze erfolgt in der Regel nicht mit der üblichen Adressierung, sondern durch sequentielle Belegung oder Entnahme. Der Stack wird nach dem Motto “last in, first out” bedient. Er ist als Zwischenspeicher, zur Übergabe von Parametern zwischen Prozeduren oder zur Reservierung von Speicher für lokale Variable konzipiert und ist in Wörtern organisiert. In besonderen Fällen kann es nötig sein, den Stack in der für den Arbeitsspeicher üblichen Art zu adressieren (siehe Abschnitte 5.4 und 9.4). 3.4.1 Initialisierung des Stacks Das Betriebssystem reserviert bei Programmstart im Arbeitsspeicher ein Bereich für die Verwendung als Stack. Die laufende Verwaltung des Stack erfolgt mit dem Register esp. Bei Programmstart wird der Stackpointer esp auf das obere Ende des Stackbereiches (Stack top) - genau betrachtet auf das erste Byte über dem Stack - im Beispiel der Tabelle 3 die Adresse 10200h - ausgerichtet. Der Stackpointer zeigt jeweils auf das letzte belegte Byte. 12 Das Betriebssytem initialisiert einen gemeinsamen Stackbereich für die Module eines Programms, so dass der Programmierer keine eigenen Maßnahmen ergreifen muss. Daneben ist es dem Programmierer möglich, selbst einen Stackbereich zu definieren. Dies kann dadurch geschehen, dass der Progammierer einen Bereich im Arbeitsspeicher reserviert, z. b. durch die Direktiven STPL: LSTPL: .space .long 0x1000 .-STPL und, wie oben beschrieben, den Stackpointer esp auf das erste Byte oberhalb des reservierten Bereichs ausrichtet: leal addl 3.4.2 fffff 10200 . . . 101ff 101fd 101fb 101f9 101f7 101f6 101f7 . . . 10000 00000 Dat1 Dat2 Dat3 Dat4 Dat5 ← esp Tabelle 3: Stackverwaltung mit Hilfe des Stackpointer esp. Im Beispiele wurden die Wörter Dat1, ... mit den Befehlen pushw Dat1, ... auf den Stack gebracht. Der Stack zeigt danach auf das niederwertige Byte des Wortes Dat5. STAPL,%esp LSTPL,%esp Benutzung des Stack Die Belegung des Stacks erfolgt von Stack top ausgehend nach “unten”. In dem Stack der Tabelle 3 war der Stackpointer esp ursprünglich auf die Adresse 10200 ausgerichtet (d. h. der Inhalt von esp war 10200). Die auf dem Stack aufgeführten Wörter wurden in der Reihenfolge Data1, Data2, Data3, Data4 auf den Stackgebracht worden. Die Wörter sind jeweils mit dem niederwertigen Teil (Low Byte) nach unten (in Richtung abnehmender Adressen) angeordnet (“little endian”). Die Benutzung des Stacks erfolgt mit folgenden Befehlen: • Belegen des Stacks mit dem Wort wd bzw. dem Doppelwort dwd pushw pushl wd dwd • Zurückspeichern in das Wort x2b bzw. in das Doppelwort x4b vom Stack popw popl x2b x4b Durch den Befehl pushl opl (opl=Operand Typ long) wird der Stackpointer um 4 erniedrigt, dann wird das Doppelwort opl an der Adresse esp gespeichert. Danach ist der Stackpointer wieder auf das unterste belegte Byte ausgerichtet. Bei dem Befehl popw opw wird das Wort, auf das der Stackpointers in esp ausgerichtet ist, vom Stack entfernt und in opw gespeichert. Dann wird der Stackpointer (d. h. der Inhalt von esp) um 2 erhöht. Für die beiden anderen Varianten pushw opw und popl opl gilt entsprechendes. 13 Eine wichtige Anwendung des Stacks ist die Parameterübergabe zu und von Prozeduren. Dabei wird abweichend von den oben beschriebenen Adressierung eine “normale” Adressierung benutzt. Dies ist näher in den Abschnitten 5.4 und 9 beschrieben. 14 4 Erstellung eines Programms 4.1 Einführung Die praktischen Übungen werden mit dem GNU-Assembler unter Linux durchgeführt. Hierzu sind Grundkenntnisse im Betriebsystem Unix bzw. Linux erforderlich. Im Rechnerraum ist Linux des Distributors S.u.S.E. installiert. Für die Durchführung der Übungen werden folgende Programme benötigt: • Editor emacs, vi oder andere • Assembler as • Linker ld • Debugger gdb • Werkzeuge wie objdump, nm, size, ar, ... • Systemfunktionen (System Calls) wie read, open, .... Sämtliche Programme sind Bestandteil der Linux-Installation. Informationen sind ausschließlich online erhältlich. Werden Informationen zu einem bestimmten Schlüsselwort z. B. objdump gesucht, können diese mit man objdump (oder z. B. man as für den Assembler oder man 2 read Systemfunktion read in Manual 2,) und info objdump (oder z. B. info emacs für Informationen zum Editor emacs) eingeholt werden. Ohne vorhergehende Angabe eines Schlüsselwortes kann mit tkinfo, tkman oder xman (beides per Befehlszeile oder Pulldownmenu erreichbar) oder mit Susehilf (Menu) bzw. hilfe (Befehlszeile) nach Informationen gesucht werden. 4.2 Grundstruktur eines Assemblerprogramms /* Formaler Rahmen eines Assembler Quellprogramms */ /* Beginn des text-Segments für Programmcode und */ /* Konstante */ .text /* Ab der Adresse beginnt die Bearbeitung von Programmcode */ start: /* Anfangsadresse des Programmcodes wird an den Linker gemeldet */ .globl start /* Programmkörper */ <Assemblerbefehle> . . . . . . . . . . . . . . . . /* Programmbeendigung: SystemCall 1 = exit */ movl $1,%eax int $0×80 /* Beginn des data-Segments für Daten */ .data 15 /* Ab der Adresse MAG wird eine Zeichenkette initialisiert */ MAG: .ascii ‘‘Hallo, how are you?’’ /* Location counter (.) minus Adresse MAG ergibt die */ /* Länge der Zeichenkette */ LMAG: .long .-MAG ZAHL: .long 3,437,0xffe8,0b110110001 .byte 65, 0x41,0101, 0b1000001 .space 0x100 . . . . . . . . . . . . . . . . 4.3 Erstellung eines lauffähigen Programmmoduls Die Programmerstellung läuft in den Schritten Zeichnen eines Ablaufplans, Editieren, Assemblieren, Linken und natürlich Ausführen ab. 4.3.1 Ablaufdiagramm Der erste Schritt zum Aufbau eines Assemblerprogramms ist die Analyse des Problems und die Ermittlung eines geeigneten Algorithmus (Lösungsverfahrens). Der Algorithmus wird in einem Ablaufplan formuliert, der die Grundlage für die Codierung in einer speziellen Assemblersprache darstellt. 4.3.2 Editieren Mit Hilfe eines Editors wird der Ablaufplan nach den Regeln der Assemblersprache in eine Quelldatei umgesetzt, die die Befehle in mnemonischer Form (Mnemo-Code) und die Direktiven enthält. Befehle sind Anweisungen an den Prozessor, gewisse Maßnahmen durchzuführen, z. B. von einem Register in den Arbeitsspeicher zu kopieren, den Stack zu bedienen, Register zu vergleichen. Sie werden in für den Programmierer verständlicher Form geschrieben (Mnemocode) und müssen in eine dem Prozessor verständliche Form (Maschinencode) gebracht werden. Direktiven sind Anweisungen, die nicht zu Prozessoraktivitäten führen. Sie regeln den Ablauf eines Programms, die Anordnung der Segmente im Arbeitspeicher, bieten dem Programmierer Hilfen und Vereinfachungen, geben Informationen für die weitere Verarbeitung des Programms weiter. Die Syntax sieht vor, dass Direktiven mit einem Punkt beginnen (z. B. .ascii “hallo” reserviert 5 Byte und belegt sie mit den angegebenen Zeichen bzw. mit deren ASCII-Codes). 4.3.3 Assemblieren Der Assembler as wandelt unter Berücksichtigung der Direktiven den Mnemocode in den Maschinencode um, er erzeugt aus der Quelldatei eine Objektdatei. Der Assembler löst zunächst Makros und ähnliche Programmierhilfen auf und legt eine Tabelle der Namen (Variable, Konstante und Labels) und der zugehörigen Adressen an. Danach werden die Assemblerbefehle in Maschinencode umgewandelt, wobei die Namen durch Adressen in Zahlenform ersetzt werden. Die Informationen über die verwendeten Namen bleiben in der Objektdatei 16 Mnemo-Code movl %ebx,%eax movw %bx,%ax movb %bl,%al jmp LB Maschinencode 8b d8 66 8b d8 eb 14 cbtw cwtl addw %bx,%di 66 98 98 66 01 fb addl %ebx,%edi 01 fb Aktion Inhalt des Registers ebx wird nach eax kopiert Inhalt des Registers bx wird nach ax kopiert Sprung zum 14h Programmzeilen entfernten Label LB Erweitern des Byte al zum Wort ax Erweitern des Wortes ax zum Doppelwort eax Addition der Inhalte der Register di und bx, Ergebnis in di Addition der Inhalte der Register edi und ebx, Ergebnis in edi Tabelle 4: Beispiele von Befehlszeilen, das sog. OpSize-Präfix 66 bewirkt Umschalten zur 16-Bit-Operandengröße, da im protected mode 32-Bit-Größe Standard sind. enthalten. In Tabelle 4 sind einige Beispiel für Assemblerbefehle in Mnemound Maschinencode angegeben. Die Reihenfolge der Befehle und der Datendeklarationen im Maschinencode und in den Speicherreservierungen ergibt sich genau aus deren Reihenfolge in der Quelldatei. Jede vom Assembler as erzeugte Objektdatei enthält mindestens 3 Segmente, die auch leer sein können. Als Erstes wird das .text-Segment angelegt, gefolgt vom .data und dem .bss-Segment. Im Objekt-Status beginnen alle Segment mit der Adresse 0. Da in der Regel ein ausführbares Programm aus mehreren Objektdateien hervorgeht, ist es nötig, die Objektdateien zusammenzufügen und aufeinander abzustimmen. 4.3.4 Linken Der Linker ld ordnet den Objektdateien, die zu einem ausführbaren Programm zusammengebunden werden sollen, Endadressen (runtime-Adressen) zu, so dass keine Überlappung auftritt. Die Segmente werden als feste Blöcke behandelt, die weder in ihrer Länge noch in der Reihenfolge der Bytes innerhalb der Segmente Änderungen erfahren. Weiterhin werden die Aufrufe von Objektdateiadressen an die runtime-Adressen angepasst. Diese Maßnahmen des Linkers werden als Relokation (Relozieren) bezeichnet. Unter Relokation versteht man also das Zuweisen der runtime-Adressen zu den Segmenten und das Anpassen der Aufrufe von Objektdateiadressen an die runtime-Adressen. Betrachten wir in Abbildung 4 die beiden Objektdateien obdat1.o und obdat2.o. Jedes Segment beginnt bei der Adresse 0. Unabhängig davon, in welcher Reihenfolge die .text-, .data- und .bss-Segmente in der Quelldatei angeordnet sind, findet sich nach dem Linken in der ausführbaren Datei die in Abbildung 4 angegebenen Reihenfolge. Auch wenn in der Quelldatei mit den Direktiven .text1, .text2 usf. eine Gliederung versucht wird, werden alle .textSegmente zu einem .text-Segment zusammengefasst. Entsprechendes gilt für die anderen Segmente. Um ein lauffähiges Programm erzeugen zu können, müssen dem Linker meh- 17 Abbildung 4: Zusammensetzung von Objektdateien durch den Linker rere Informationen übergeben werden (z. B. Name der ausführbaren Datei, Anfangsadresse, Format der Objektdatei, ... ). Dies wird i. a. in standardisierter Form durchgeführt. Erwähnenswert ist dabei, dass als Anfangsadresse start: erwartet wird und dass die ausführbare Datei den Namen a.out erhält, wenn in der Befehlzeile des Linkers nichts anderes angegeben wird. Die folgenden Befehlszeilen benutzen die Option o, die dem Benutzer erlaubt, Namen für die Objektdatei und die ausführbare Datei festzulegen. • Assemblieren: as -o datname.o datname.s Die Quelldatei datname.s wird assembliert. Das Ergebnis ist die Objektdatei datname.o . • Linken: ld -o datname.x datname.o /home/stud/liste.o liba1.a Die Objektdateien datname.o und /home/stud/liste.o und die Bibliotheksdatei (Archiv) liba1.a werden zusammengebunden. Das Ergebnis ist die ausführbare Datei datname.x. Nähere Informationen sind mit man as bzw. man ld erhältlich (siehe auch Abschnitt 4.1). 4.4 Werkzeuge zur Programmentwicklung Für die Erstellung von Programmen, insbesondere zur Fehlererkennung und Fehlerbehebung gibt es eine Reihe von Werkzeugen, die in Verzeichnis /usr/bin/ enthalten sind. 4.4.1 Der GNU-Debugger Bei Verwendung des Debuggers wird die ausführbare Datei (das Programm) durch Setzen der trap-flag (siehe Abschnitt 8.1) in Einzelschritten durchlaufen. Es ist möglich, breakpoints zu setzen, an denen das Programm angehalten und durch Betrachten von Register, Stack oder Arbeitsspeicher auf logische Korrektheit geprüft werden kann. Detaillierte Informationen sind unter info gdb, Menüpunkt Index zu finden. Auf den GNU-Debugger setzt der Debugger DDD auf, der eine graphische Oberfläche bietet. 18 Start des Debuggers: gdb file.x (Zu untersuchende ausführbare Datei). Danach meldet sich der Debugger mit . . (gdb) und ist bereit, Kommandos entgegenzunehmen. Beenden: quit, kurz q oder Strg-D Kommandos: Eingabezeile, die mit einem gdb-Kommando beginnt. Es genügt, die Anfangsbuchstaben einzugeben, bis das Kommando eindeutig festgelegt ist, danach evtl. TAB . Eine leere Eingabezeile, d.h. ret wiederholt den vorangegangenen Befehl (Ausnahme: list und x). Start des Programms: run, kurz r Programm-Status: info program Breakpoints: Setzen: break LABEL-Bezeichner, break ProzedurName oder break Dateiname, Prozedur-Name oder break *0xhex Adresse oder break (nächste Instruktion). Information über Breakpoints: info break. Löschen: clear Prozedur-Name oder clear DateiName:Prozedur-Name. delete Breakpoint-Nr. oder delete (alle) Aktivieren/Deaktivieren: enable Breakpoint-Nr. oder enable (alle) bzw. disable Breakpoint-Nr. oder disable (alle) Fortsetzung: continue, kurz c oder nur die nächste Instruktion stepi, kurz si evtl. mit Zahl der auszuführenden Instruktionen. Bei einem Prozeduraufruf sorgt nexti, kurz ni für die Ausführung der gesamten Prozedur und hält danach wieder an. Stack: backtrace zeigt Reihenfolge der Prozeduraufrufe mit den Nr. der Stack-Frames an. frame Nr. wählt den entsprechenden Stack-Frame aus. Das Hauptprogramm hat den frame mit der höchsten Nr., die gerade aktive Prozedur hat den frame mit der Nr. 0. info frame gibt Informationen über den aktuellen Stack-Frame aus. Speicherinhalte: x/NFU 0xHex-Adresse gibt Speicherinhalte ab der angegebenen Adresse aus: N=Anzahl der Speichereinheiten, F=Format, U=Einheitengröße(b=Byte, h=Wort, w=Doppelwort, g=Quadwort). Soll ein Registerinhalt als Adresse verwendet werden, so ist dem Registernamen (ohne %) ein $ voranzustellen. Registerinhalte: info registers oder info all-registers oder info registers $regname gibt die Registerinhalte aus. Besondere Register: $pc(program counter), $sp(stack pointer), $fp(frame pointer), $ps(processor status) Adresse von Symbolen:info address NAME Weitere Information: help info 4.4.2 Spezielle Werkzeuge Die folgende Auswahl von Werkzeugen enthält Programme zum Disassemblieren von Objektdateien oder auführbaren Dateien (objdump), zum Auflisten 19 bzw. Entfernen der in einer Objektdatei benutzten Namen (nm bzw. strip) und zur Ausgabe der Startadresse und der Segment- und Gesamtgrößen einer Objektdatei (size). • Darstellung von Objektdatei oder ausführbarer Datei Befehlszeile: objdump datei.o bzw. datei.x -Option Option Wirkung d Disassemblieren des Textsegments D Disassemblieren des Textsegments und des Datensegments h Ausgabe der Größe und Adressen der Segmente s Kompakte Darstellung des gesamten Programms in hexadezimaler Form • Namen (Symbole) aus der Objektdatei auflisten Befehlszeile: nm datei.o -Option Option Wirkung o, A Dateinamen voraussetzen n nach Adressen sortieren p unsortiert • Namen (Symbole) aus der Objektdatei entfernen Befehlszeile: strip datei.o -Option Option Wirkung s alle Namen entfernen x nur lokale Namen entfernen Nname Namen name entfernen Kname Namen name retten • Startadress, Segment- und Gesamtgröße auflisten Befehlszeile: size datei.o -Option Option Wirkung in Zeilen A In Spalten B in Zeilen 4.5 Assembleranweisungen Die Struktur von Befehlszeilen im Assembler-Quellprogramm ist in Tabelle 5 zu sehen. Anweisungen (statements) treten in zwei Formen auf: • Befehle (instructions) sind Anweisungen an den Prozessor und werden in Maschinencode übersetzt. Sie geben dem Prozessor vor, was gemacht werden soll. • Direktiven (directives) sind Anweisungen an den Assembler oder Linker, sie werden nicht in Maschinencode übersetzt. Direktiven regeln, wie etwas gemacht werden soll. Im Quellprogramm angegebene Zahlen werden vom Assembler in die interne binäre Darstellung umgewandelt. Sie können binär, oktal, dezimal oder hexadezimal vorgegeben werden. Die entsprechende Syntax ist in Tabelle 6 angegeben. 20 Name/ Marke MLDG: Befehl/ Direktive .ascii Operanden Kommentar ”Hallo Du” leal MLDG,%eax addl %ecx,%eax # # # # # # Beginnend mit der Marke MLDG wird die Zeichenkette ‘Hallo Du‘ gespeichert Speichert die Adresse von MLDG in eax Addiert den Inhalt von ecx zu eax Tabelle 5: Struktur von Befehlszeilen im Assembler-Quellprogramm Basis binär Synthax 0b..... / 0B..... oktal dezimal hexadezimal 0..... ..... (ohne führende Null) 0x..... Beispiel 0b111100110100 oder 0B111100110100 07464 3894 0xf34 Tabelle 6: Schreibweise von Zahlen mit verschiedenen Basiswerten Kommentare stehen zwischen den Zeichen /* ... */ oder hinter dem Zeichen #. Kommentare werden vom Assembler ignoriert, d. h. vergrößern das ausführbare Programm nicht. Sie sind sehr wichtig für die Lesbarkeit. Kommentare können in der Form /* ... */ auch zur Gliederung der Quelldatei verwendet werden. Damit wird die Struktur des Programms deutlicher und das Programm besser lesbar.Mit # können einzelne Befehle in derselben Zeile erläutert werden. Im Kommentar sollten keine reservierten Namen verwendet werden. 21 5 Adressierung Daten, die im Arbeitsspeicher abgelegt sind, können durch Angabe der dem Speicherplatz zugeordneten Nummer, der Adresse (siehe Abschnitt 3.3) aus dem Arbeitsspeicher in ein Register geladen werden, umgekehrt können auf diese Weise Daten im Arbeitsspeicher gespeichert werden. Dieses Ansprechen von Speicherplätzen im Arbeitsspeicher wird Adressierung genannt. Die Adressierungsmöglichkeiten des Assemblers sind die direkte, die indirekte und die indizierte Adressierung. Bei der direkten Adressierung wird die Adresse des gewünschten Bytes, Wortes oder Doppelwortes direkt, d. h. als Namen oder Ausdruck, der den Namen enthält, angegeben. Bei der indirekten Adressierung wird der Inhalt eines Register als Adresse des anzusprechenden Wortes interpretiert. Bei der indizierten Adressierung enthält ein Register einen Laufindex, der es erlaubt, in einer Schleife eine Liste von Bytes, Wörtern und Doppelwörtern zu durchlaufen. 5.1 Direkte Adressierung Die direkte Adressierung erfolgt durch die Angabe von Namen (Symbolen, Labels) . Beim Assemblierungsvorgang setzt der Assembler die Namen in Adressen um. Die Daten werden in der Reihenfolge ihres Auftretens berücksichtigt. Dabei stellt der Name die Adresse des ersten Elementes eines Datenblocks dar. Betrachten Sie die nachfolgende Definition eines data-Segments. MSG1: ZAHL: .data .ascii .word .text ‘‘Zeichenkette’’ 3287,2,19786 Die Adresse MSG1 bezieht sich beispielsweise auf das Zeichen “Z”, die Adresse ZAHL auf das LowByte der (Wort-)Zahl 3287. Hinter der Adresse ZAHL sind 3 Integer-Zahlen definiert. Für die Adressierung weiterer Elemente steht eine Adressarithmetik zur Verfügung, die in den folgenden Zeilen erläutert ist. Denken Sie dabei an die Zahlendarstellung im little endian-Modus, beschrieben in Abschnitt 3.3.1. movl ZAHL+2,%edx movb MSG1+13,%al movw MSG1+13,%ax # # # # 1.296.695.298 = 0x4d4a0002 wird nach edx kopiert 12 = 0xc wird nach al kopiert 524 = 0x20c wird nach ax kopiert Beim Lesen der Segmentdefinition legt der Assembler eine Liste der Namen und der zugehörigen Adressen an. Im location counter wird die Belegung des Arbeitsspeichers registriert, der location counter zeigt jeweils das nächste freie, d. h. belegbare Byte an. Treten beim weiteren Assemblierungsvorgang Namen auf, so werden diese durch die Adressen aus der Liste ersetzt. Überprüfen Sie dies dadurch, dass Sie ein Programm (z. B. start.o oder start.x) mit Hilfe des Werkzeugs objdump betrachten. 22 5.2 Indirekte Adressierung Eine flexiblere Verwendung von Größen (Variablen oder Konstanten) wird durch die indirekte Adressierung erreicht. Dabei treten die Adressen als Registerinhalte auf. Durch die Schreibweise (%ebp) - also %ebp in runden Klammern - wird deutlich gemacht, dass nicht der Inhalt des Registers ebp angesprochen wird, sondern der Inhalt des Speicherbytes, dessen Adresse in ebp steht. Diese Adresse wird Basisadresse genannt. Mit einem vorgesetzten Festwert (displacement) kann im Arbeitsspeicher eine bestimmte Anzahl von Bytes weiter- oder zurückgegangen werden: 5(%ebp) entspricht MSG1+5, wenn ebp die Adressse von MSG1 enthält. Nach den Befehlen (Bezug auf das Beispiel in Abschnitt 5.1): leal movb MSG1,%ebp (%ebp),%dl steht im Register dl der ascii-Wert von “Z”, also die Zahl 90. Mit dem Befehl movb 2(%ebp),%dl wird das Zeichen “i” (ascii-Wert 105) ins Register dl kopiert. Frage: Welches Zeichen wird mit leal movb ZAHL,%ebp -5(%ebp),%ch wohin kopiert? Die wichtigste Anwendung der indirekten Adressierung besteht in der Parameterübergabe zwischen Prozeduren. Dies wird in Abschnitt 5.4 ausführlich behandelt. 5.3 Indizierte Adressierung Die indizierte Adressierung erweitert die Möglichkeiten der indirekten Adressierung. Sie wird dazu benutzt, Listen von Bytes, Wörtern, Doppelwörtern ect. elementeweise in einer Schleife zu durchlaufen. Der Laufindex wird in einem Indexregister gehalten und durch die Befehle incl oder decl erhöht oder erniedrigt. Die Schrittweite der Änderung (Skalierung) der Adresse hängt von der Blockgröße der Liste ab und kann 1, 2, 4 oder 8 betragen. Schreibweise der Adresse: Displacement(Basisregister,Indexregister,Skalierung) Die zugehörige Adresse wird nach der Beziehung Effektive Adresse= Basisregister+(Indexregister*Skalierung)+Displacement berechnet. Der Ausdruck stellt eine Adresse im Arbeitsspeicher dar und wird entsprechend in Befehlen verwendet. Mit movb 5(%ebp,%esi,2),%dl wird das Byte von der mit 5(%ebp,%esi,2) angesprochenen Speicherstelle nach dl kopiert. Das folgende Beispiel zeigt die indizierte Adressierung bei der Adressierung einzelner Bytes aus einer Liste, die die Zeichenkette Assemblerprogrammierung macht Spass enthält. 23 .data .ascii ‘‘Assemblerprogrammierung macht Spass’’ .text /* Anfangsadresse der Liste ins Basisregister */ leal MS,%ebp MS: /* Indexregister esi mit 3 initialisieren */ movl $3,%esi /* Das Zeichen e wird nach dl kopiert */ movb (%ebp,%esi),%dl /* alternativ movb MS(,%esi),%dl */ Im einem weiteren Beispiel werden die ersten 30 Byte des Datensegments mit dem Zeichen x bzw. dessen ascii-Code belegt. Hier erfolgt der typische Einsatz der indizierten Adressierung in einer Schleife. Die Schleife ist kopfgesteuert und verwendet das Register esi als Indexregister. Nach jedem Durchlauf wird esi inkrementiert und mit dem Register ecx verglichen, das die Anzahl der Durchläufe enthält. Der Befehl cmpl steht für compare. Der nachfolgende Befehl jne (jump if not equal) bewirkt einen Sprung nach label LB1, wenn esi noch nicht gleich ecx ist, anderenfalls wird die nächste Programmzeile ausgeführt, d. h. die Schleife verlassen (weitere Sprungbefehle in Tabelle 18). LST: .data .space .text . . . movl leal movl 128 Speicher-bytes Adressen/ Namen ffff ffff FELD2+4 FELD2+3 FELD2+2 FELD2+1 FELD2 FELD1+4 FELD1+3 FELD1+2 FELD1+1 FELD1 $30,%ecx LST,%ebp $0,%esi LB1: movb incl cmpl jne $’x’,(%ebp,%esi) %esi %ecx,%esi LB1 Im letzten Beispiel wurde die Anfangsadresse der Liste ins Basisregister kopiert. Im Nächsten wird als Alternative die Anfangsadresse durch das Displacement festgelegt. Dabei wird das Basisregister nicht benutzt. Seine Stelle bleibt leer, das nachfolgende Komma muss aber geschrieben werden. LST: .data .space .text . . . movl FELD+4 FELD+3 FELD+2 FELD+1 FELD 0000 0000 Tabelle 7: Adressierung von 2-dimensionalen Listen 128 $0,%esi LB2: 24 movb incl cmpl jne $’x’,LST(,%esi) %esi $30,%esi LB2 Fehlen die Angabe von Indexregisters und/oder Displacement, werden sie durch Null ersetzt, ein fehlender Skalierungsfaktor wird als Eins angenommen. Betrachten Sie die Liste (siehe Tabelle 7): FELD .space 1600 FELD stelle eine Liste aus 1600 Byte (oder 800 Wörtern oder 400 Doppelwörtern) dar und kann mit 1600 Bytes, 800 Wörtern oder 400 Doppelwörtern belegt werden. Mit der Befehlssequenz SCHL1: leal FELD,%ebp movl $0,%esi movl $400,%ecx movl (%ebp,%esi,4),%edx /* movl FELD(,%esi,4),%edx */ incl %esi cmpl %ecx,%esi jne SCHL1 wird die gesamte Liste FELD durchlaufen und Doppelwort für Doppelwort nach edx kopiert. Mit movl 3(%ebp,%esi,4),%edx wird mit dem 4. Doppelwort begonnen. Allerdings erhält man unter Umständen wegen Bereichsüberschreitung die Fehlermeldung unerlaubter Speicherzugriff oder segmentation fault. Mit movw (%ebp,%esi,2),%dx wird bei jedem Schleifendurchlauf ein Wort, also eine Folge von 2 Bytes in das Register dx kopiert werden. Die in Tabelle 7 gezeigten Listen können als 2-dimensionales Feld (Matrix) aufgefasst werden. Zum Durchlaufen der Matrix werden 2 Indizes (z. B. die Register esi und edi) benötigt. Es ist Sache des Programmierers durch geeignete Schleifenbbildung das gewünschte Durchlaufen der Matrix (spalten- oder zeilenweise) zu organisieren. Aufgabe: Eine Matrix (n Spalten, m Zeilen) ist im Arbeitspeicher zeilenweise abgelegt (siehe Tabelle 7). Schreiben Sie je ein Programmteil, in dem die Matrix zeilenweise und spaltenweise durchlaufen wird. 5.4 Parameterübergabe in Prozeduren Die indirekte Adressierung wird insbesondere bei der Parameterübergabe von einer rufenden Prozedur bzw. vom Programm zur gerufenen Prozedur oder bei Beendigung der Prozedur zur Rückgabe von Parametern angewandt. 25 Die üblicherweise benutzte und schnellsParameter1 te Ansprache des Stack ist in Abschnitt Parameter2 3.4.2 geschildert. Sie erfolgt mit den BefehParameter3 len pushl und popl bzw. pushw und popw. Rücksprungadresse ← esp Dabei wird der Stack, wie in Abschnitt 3.4.2 bzw. Tabelle 3 gezeigt, von oben (stack top) Parameter1 her belegt und nach oben hin abgetragen. Parameter2 Dennoch ist in manchen Situationen ein Parameter3 normaler d. h. wahlfreier Zugriff auf DaRücksprungadresse ten innerhalb des Stack erforderlich. Hierzu ebp ← ebp, esp kurz die Beschreibung der Problematik in Vorgriff auf Abschnitt 9.4: Tabelle 8: Oben Stackbelegung bei Aufruf einer Beim Aufruf von Prozeduren mit dem Prozedur und unten nach “Einfrieren” des akBefehl call wird die Adresse (“Rücksprungtuellen Stackpointers esp im Register ebp (jeadresse”), an der das Programm später fortdes Kästchen entspricht vier Bytes, der Poingesetzt werden soll, auf dem Stack gesichert. ter zeigt jeweils auf das “unterste” Byte). Nach Beendigung der Prozedur mit ret kann das Programm an der richtigen Stelle fortgesetzt werden. Vor dem Aufruf der Prozedur müssen die Parameter, die an die Prozedur übergeben werden sollen, auf den Stack geschrieben werden. In Tabelle 8 (oben) ist eine Stackbelegung mit 3 Parametern direkt nach Aufruf einer Prozedur gezeigt. Würde jetzt mit dem üblichen Stackbefehl popl auf die Parameter zugegriffen werden, würde die Rücksprungadresse verlorengehen und eine ordnungsgemäße Weiterführung des Programms unmöglich gemacht. Die Lösung des Problems besteht darin, dass der Stackpointer, der die Stackbelegung kontrolliert, sich also ständig ändert, in das Register ebp gespeichert, sein aktueller Stand gewissermaßen eingefroren wird. Zuvor muss der ursprüngliche Inhalt des Registers ebp auf dem Stack gesichert werden (Tabelle 8 unten). Mit der indirekten Adressierung disp(%ebp) kann nun auf die Parameter zugegriffen werden (z. B. mit disp = 12 auf Parameter2), ohne die der Rücksprungadresse zu entfernen. Der dafür nötig Rahmen der Prozedur hat folgendes Aussehen: PRZNAME: .globl PRZNAME pushl %ebp # Sicherung des ‘‘alten’’ Inhalts von ebp movl %esp, %ebp # aktueller Stand von esp wird in ebp # festgehalten pushal # Sicherung der 8 Register eax, ..., edi /* Anfang des Prozedurkörpers */ . . . . . . movl 16(%ebp),%eax # Beispiel: Parameter1 wird nach eax kopiert . . . . . . /* Ende des Prozedurkörpers */ popal popl ret %ebp # Restaurierung der 8 Register eax, ..., edi # Restaurierung von ebp # Rücksprung zum rufenden Programm 26 Eine ausführlichere Beschreibung des Aufbaus, der Benutzung und der Einbindung von Prozeduren ist in Abschnitt 9 gegeben. 27 6 Datendefinitionen Beim Assemblierungsvorgang erstellt der Assembler eine Objektdatei, die die Maschinenbefehle und Speicherreservierungen für die Daten enthält. Im Assemblerprogramm muss also der Umfang des für Daten benötigten Speicherbereiches festgelegt werden. Dies geschieht dadurch, dass die verwendeten Variablen und Konstanten mit ihrem Speicherbedarf innerhalb der Definition des Daten- bzw. Textsegments aufgeschrieben werden. Durch den Assembler wird für die Größen (Variable und Konstante) in der Reihenfolge ihres Auftretens im Quellcode Speicherplatz reserviert. Gleichzeitig werden den Namen der Größen Adressen zugeordnet. Zusätzlich zur Reservierung von Speicherplatz ist es möglich, die Größen vorzubelegen. Zu beachten ist, dass im .text-Segment definierte Datenblöcke als Konstante zu verwenden sind, d. h. nicht verändert werden können (read only), Variable werden im .data-Segment definiert. Datenblock Byte Byte Wort Doppelwort Quadwort Oktawort Anz.Bytes 1 1 2 4 8 16 Direktive .ascii zeichenkette .byte ausdrücke .word ausdrücke .long ausdrücke .quad ausdrücke .octa ausdrücke Verwendung Zeichen Zahlen Zahlen, Adressen Zahlen, Adressen Zahlen Zahlen Tabelle 9: Direktiven für die wichtigsten Datenblöcke im Prozessor 80 x86 6.1 Adressierung der Datenblöcke Die zur Definition von Zeichen und ganzen Zahlen zur Verfügung stehenden Direktiven sind in Tabelle 9 angegeben. Die Gleitkommazahlen sind in Abschnitt 14.2 behandelt. Die Namen der Daten repräsentieren die Adressen jeweils des ersten Byte des definerten Datenbereichs. Betrachten wir die Programmzeile: ZAHL: .long 0x12345678, 2882369680 Mit der Direktive .long werden 2 Doppelwörter (8 Byte) im Arbeitspeicher reserviert und mit den angegebenen Zahlenwerten initialisiert. Das erste (niederwertige) Byte (0x78) wird an der Adresse ZAHL angelegt (little endian, Abschnitt 3.3.1). Mit dem Befehl movl ZAHL,%eax wird die erste Zahl (12345678hex =305419896) in das Register eax kopiert, mit dem Befehl movl ZAHL+4,%eax 28 Abbildung 5: Mit Bezeichnungen wie High/Low Order Half, High/Low Order Word, High/Low Order Byte oder High/Low Order Bit werden Teile von Datenblöcken angesprochen. Die High/Low Order Bits werden auch least/ most significant bits (lsb/msb) genannt. die um 4 Byte darüberliegende Zahl (2882369680). 6.2 Strukturierung der Datenblöcke Die Anordnung der Bytes von Wort- bzw. Doppelwortzahlen sind in Tabelle 10 mit den Daten ZAHL1 und NUM erläutert. Aufgabe: Tragen Sie die Byte-Werte von ZAHL2 in die Tabelle 10 ein. Bei der Reservierung von Speicherplatz für Datenblöcke wie Wörter, Doppelwörter etc. werden Bytes lediglich in logischer Form zusammengefasst, physikalisch bestehen sie aus unabhängigen Bytes. So kann in einem solchen Datenblock ohne weiteres ein einzelnes Byte adressiert und herausgegriffen werden. Mit movw ZAHL1+1,%ax (siehe Abbildung 10) wird die Zahl 0x6512 nach ax kopiert. Aufgabe: Welches Doppelwort wird mit movl ZAHL1+3,%edx nach edx kopiert? Die Beispiele zeigen, dass es innerhalb eines Segments für Datenblöcke keinen Bereichsschutz gibt. Wenn bei der Adressierung eines Feldes die Grenzen überschritten werden, wird auf die nächsten Speicherplätze, auch auf Befehlszeilen zugegriffen. Allerdings besteht ein Schutz gegen Überschreitung der Segmentgrenzen und beim .text-Segments überhaupt gegen Überschreiben innerhalb dieses read-onlySegments. Die Interpretation des Speicherinhaltes, d. h. ob das an sich wertneutrale Bitmuster als Zeichen, Vorzeichenzahl, vorzeichenlose 29 Name Adresse Inhalt (hex) 88 87 86 ZAHL2 85 84 12 83 34 82 56 NUM 81 78 80 87 7f 65 7e 12 ZAHL1 7d 34 7c 21 7b 6f 7a 6c 79 6c 78 61 77 68 76 64 75 63 0804 8074 62 DAT 0804 8073 61 .data DAT: .ascii ”abcd” .ascii ”hallo!” ZAHL1: .word 0x1234,34661 NUM: .long 305419896 ZAHL2: .long 439041101 Tabelle 10: Belegung des Arbeitsspeichers (oben) gemäß der Datendefinition unten. Beachten Sie dabei den little endian-Modus (siehe Abschnitt 3.3.1). Zahl, Adresse usw. anzusehen ist, ergibt sich aus dem jeweiligen Programmzusammenhang. Daten können auch ohne Namen deklariert werden. Auf sie kann ähnlich wie bei Listen durch direkte oder indirekte Adressierung zugegriffen werden. 6.3 Arithmetik für Adressen, Konstante und Festwerte Für Berechnungen bei der Initialisierung von Konstanten, für Berechnungen der Länge von Datenstrukturen und Adressen steht eine Integer-Arithmetik zur Verfügung, die mit den Operatoren +, − und ∗ arbeitet. Die Gleichungen werden vom Assembler ausgewertet. Im Objektcode erscheinen nur noch die Ergebnisse. Insbesondere bedeutet dies, dass diese Arithmetik nicht bei Programmausführung möglich ist. Beispiele sind movl ZAHL1+2,%ebx oder leal ZAHL2-4,%ebp. 6.4 Konflikte bei Datenformaten Eine Reihe von Befehlen oder Programmiersituationen erfordern die Verwendung von ganz bestimmten Datenformaten. So können beispielsweise mit dem Befehl add nur Summanden vom selben Datenblock addiert werden (siehe Abschnitt 7.2.2). Es können auch Situationen auftreten, in denen das Datenformat einer verwendeten Größe nicht bekannt ist oder aus einem Datenfeld bestimmte Blocklängen herausgegriffen werden sollen. Für solche und ähnliche Situationen gibt es mehrere Möglichkeiten 6.4.1 Operandengrößen-Zusatz (OpSize-Suffix) In den Assemblerbefehlen wird üblicherweise genau angegeben, welche Blocklänge verwendet werden soll, um Konflikte mit Datenformaten oder Unklarheiten über Datenformate adressierter Größen zu vermeiden. In der AT&T-Notation wird bei den verwendeten Prozessorbefehlen ein Zusatz angehängt (OpSizeSuffix), der die zu behandelnde Blockgröße festlegt: l für long (4 Byte, Doppelwort), w für word (2 Byte, Wort) und b für Byte. Bei Konflikten bestimmt diese Angabe über die auszuführende Maßnahme. Zum Beispiel wird bei dem Befehl movw MEM,%eax nur das Register ax belegt, bei movl MEM,%ax das Register eax. Bei der Kollision von Byte und Doppelwort bzw. Wort gibt es in einigen Fällen Fehlermeldungen. Werden die Zusätze l, w oder b weggelassen, dann entscheidet der Registeroperand. 6.4.2 Direkte Umwandlung (Erweiterung) Zur Umwandlung (Erweiterung) von Datenblöcken (Byte → Wort, Wort → Doppelwort, usf.) stehen die Befehle • cbtw (Erweiterung von al nach ax), • cwtl (Erweiterung von ax nach eax), • cwtd (Erweiterung von ax nach dx:ax), • cltd (Erweiterung von eax nach edx:eax) 30 Befehl cbtw cbtw vorher movzbw %ah,%dx movsbw %ah,%dx ax=ah:al ???? ???? : 0111 1111 ???? ???? : 1000 0000 ax 10111001 11100100 10111001 01100100 10111001 01100100 ax 0000 0000 0111 1111 1111 1111 1000 0000 dx 11000111 00001101 00000000 10111001 11111111 10111001 Tabelle 11: Beispiele für die Auflösung von Datenkonflikten durch Erweiterung auf einen breiteren Datenblock und durch erweiterndes Kopieren zur Verfügung. Diese werden ohne Operanden benutzt und beziehen sich nur auf die angegebenen Register. Die Befehle stellen Abkürzungen dar, so bedeutet cbtw convert byte to w ord. Die Erweiterungen erfolgen vorzeichenerhaltend durch Auffüllen mit Nullen (positves Vorzeichen) oder mit Einsen (negatives Vorzeichen). Beispiele sind in Tabelle 11 gezeigt. 6.4.3 Erweiterndes Kopieren Für das erweiternde Kopieren op1→op2 (z. B. al→bx oder ax→eax) gibt es die Befehle movs op1,op2 für vorzeichenerhaltende Erweiterung und movz op1,op2 für mit Nullen auffüllende Erweiterung. An die Befehle ist die Art der Erweiterung als Suffix anzuhängen: bl (byte→long), bw (byte→word), wl (word→long). Ziel (op2) des Kopierbefehls kann nur ein Register sein. In der Intel-Notation lauten diese Befehle movsx bzw. movzx. Beispiel: movsbl %al,%edx erweitert al vorzeichengerecht zu einem Doppelwort und kopiert nach edx. Weitere Beispiele sind in Tabelle 11 gezeigt. 6.5 Direktiven Direktiven sind Anweisungen an den Assembler. Sie betreffen die Reservierung und Vorbelegung von Arbeitsspeicher für Daten, die Zugriffsarten auf Programmteile und die weitere Verarbeitung der Objektdateien. Daneben bieten sie dem Programmierer eine Vielzahl von Hilfen und ermöglichen die Steuerung des Assemblierungsablaufs. Die Syntax sieht vor, dass Direktiven mit einem Punkt beginnen. Der Assembler wertet die Direktiven aus und ersetzt sie durch die ermittelten Ergebnisse. Der Abschnitt 17 behandelt die wichtigsten Direktiven und enthält eine komplette Liste der verfügbaren Direktiven. Viel verwendete Direktiven sind die Datendefinitionsdirektiven .ascii, .byte, .word, .long, die Set-Direktiven .set, .equ, .equiv, die Reservierungs-Direktiven .space, .fill, .skip, die Makro-Direktive .macro und die Include-Direktive .include. An dieser Stelle soll auf die location counter-Direktive “.” (ein Punkt) hingewiesen werden, die den aktuellen Stand des Speicherplatz-Zählers an den Assembler übergibt, der ihn in die betreffenden Ausdrücke einsetzt. Der location counter wird häufig benutzt, um die Länge von Bereichen durch den Assembler ermitteln zu lassen. Beispiel: STR:.ascii‘‘Zeichenkette unbestimmter Länge’’ 31 .set LSTR,.-STR Durch die Differenz .-STR wird die Differenz der aktuellen Adresse und der Adresse STR gebildet, d. h. die Länge der bei STR beginnenden Zeichenkette berechnet. Diese Länge ist unter dem Namen LSTR verfügbar. 32 7 Allgemeine Befehle Informationen über die (prozessorabhängigen) Befehle des Intelprozessors sind nicht in den Linux-Infodateien enthalten, sondern müssen der Beschreibung des Prozessors entnommen werden [8]. Hierfür eignen sich auch die in großer Zahl vorhandenen Bücher zum Microsoft Macro Assembler (MASM), die fast alle die Befehle in lexikalischer Form enthalten z. B. [3][4][10]. Allerdings ist zu berücksichtigen, dass statt der dort üblichen Intel-Schreibweise für den GNUAssembler die AT&T-Notation zu benutzen ist. Diese ist in der Broschüre The GNU Assembler der Free Software Foundation von Dean Elsner, Jay Fenlason & friends beschrieben [1] und als Auszug in Abschnitt 16 wiedergegeben. 7.1 Datenmanipulation Die meistbenutzten Befehle zur Datenmanipulation sind: movl quelle, ziel Kopieren von quelle nach ziel mit den Regeln: • gleiche Länge von quelle und ziel, • erlaubt: Register ↔ Register und Register ↔ Speicher, nicht erlaubt: Speicher ↔ Speicher xchgl op1,op2 Austausch der Inhalte von op1 und op2 mit denselben Regeln. leal Adresse von op1 (m) bestimmen und in op2 (r32) speichern op1,op2 pushl op Ablegen von op (m16, m32, r16, r32) auf dem Stack (siehe Abschnitt 2.2) popl Zurückspeichern vom Stack in op (m16, m32, r16, r32, siehe Abschnitt 2.2) op pushal (a=all, l=long) Ablegen der Register eax, ecx, edx, ebx, esp, ebp, esi, edi bzw. ax, cx, dx, bx, sp, bp, si, di auf dem Stack popal Rückspeichern vom Stack in die Register edi, esi, ebp, esp, ebx, edx, ecx, eax bzw. di, si, bp, sp, bx, dx, cx, ax Die Befehle sind mit dem OpSize-Suffix l (Datenblock .long) geschrieben. An der Stelle des Suffix l ist gegebenenfalls auch eines der OpSize-Suffizes w oder b zu setzen. Bei den Stackbefehlen push und pop sind nur die Suffices l und w möglich (siehe Abschnitt 3.4.2). Die in Klammern gesetzten Angaben beziehen sich auf die erlaubten Speicherarten: m=Arbeitsspeicher, r=Register, z. B. bedeutet r16: 16-bit-Register. Ausführliche und detailierte Informationen zu diesen und zu weiteren Befehlen können Sie den Manpages entnehmen (siehe Abschnitt 4.1). 33 7.2 Arithmetikbefehle Im Intelprozessor 80 x86 können direkt nur ganze Zahlen mit oder ohne Vorzeichen verarbeitet werden. Zur Gleitkommaarithmetik wird der mathematische Koprozessor verwendet, der als ein selbständiger Prozessor mit eigenem Befehlssatz und eigenen Registern anzusehen ist. Die Darstellung der ganzen Zahlen mit und ohne Vorzeichen wurde in der Lehrveranstaltung Grundlagen der Informatik behandelt und sollte, wenn nötig, in Erinnerung zurückgerufen werden [20]. 7.2.1 Arithmetik-Flaggen Bei arithmetischen Operationen können aus verschiedenen Gründen besondere Ereignisse auftreten. Damit sind Bereichsüberschreitungen, Vortäuschen eines Vorzeichenwechsels (overflow ), Überträge vom high bit nach außen (carry), Null-Ergebnis (zero), Negativ-Ergebnis (sign) gemeint. Sie werden durch Veränderung von Flaggen (flags) im flag-Register gemeldet (siehe Abschnitt 8.1 und Tabelle 17). Durch bedingte Sprungbefehle können Programmverzweigungen vom Zustand der flags und damit von bestimmten Ereignissen abhängig gemaccht werden. • CARRY-flag Das carry-flag (cf ) wird gesetzt, wenn bei einer Operation ein Übertrag aus dem höchstwertigen Bit hinaus erfolgt (z.B. bei der Addition oder Subtraktion), andernfalls wird dieses flag gelöscht. Das carry flag meldet also fehlerhafte Ergebnisse bei Operationen mit vorzeichenlosen Zahlen. • OVERFLOW-flag Das overflow-flag (of ) wird gesetzt, wenn eine Operation eine Veränderung des Vorzeichenbits (high bit) verursacht. Dies geschieht, wenn einen Übertrag in das Vorzeichenbit, aber kein Übertrag aus dem Vorzeichenbit (cf=0) oder umgekehrt, wenn durch die Operation ein Übertrag aus dem high bit (cf=1) erfolgte, aber kein Übertrag in das Vorzeichenbit. Andernfalls wird das overflow flag gelöscht. Das overflow flag meldet also Fehler bei Operationen mit vorzeichenbehafteten Operanden. • ZERO-flag Das zero-flag (zf ) wird gesetzt, wenn das Ergebnis nach einer Operation 0 ist; andernfalls wird dieses flag gelöscht. • SIGN-flag Das sign-flag (sf ) wird gesetzt, wenn das höchstwertigste Bit des Ergebnisses nach einer Operation gesetzt ist; andernfalls wird dieses flag gelöscht. Das sign-flag enthält somit eine Kopie des Vorzeichenbits. • AUXILIARY CARRY-flag Das auxiliary carry-flag (af ) wird gesetzt, wenn bei einer Operation ein Übertrag von Bit 3 hinaus erfolgt; andernfalls wird dieses flag gelöscht. af wird für die BCD-Arithmetik verwendet, bei der eine Dezimalziffer in einem Halbbyte gespeichert wird. 34 Befehl jc jnc jno jns jnz jo js jz Sprung ausführen, wenn carry flag gesetzt carry flag nicht gesetzt overflow flag nicht gesetzt Ergebnis positiv Ergebnis ungleich Null overflow flag gesetzt Ergebnis negativ Ergebnis Null Bedingung cf=1 cf=0 of=0 sf=0 zf=0 of=1 sf=1 zf=1 Tabelle 12: Flaggengesteuerte Sprungbefehle und deren Bedeutung. Der Buchstabe j steht jeweils für jump. Die nachfolgenden Buchstaben beziehen sich auf das betrachtete flag (z. B. jz: verzweige, wenn das zero flaggesetzt ist (zf=1) oder jno: verzweige, wenn das overflow flag nicht gesetzt ist (of=0). Um Fehler bei Arithmetikoperationen zu erfassen, müssen die passenden flags nach der Rechenoperation abgefragt werden. Das bedeutet, dass bei Operationen mit vorzeichenlosen Zahlen (unsigned integer) das carry flag, bei vorzeichenbehafteten Zahlen (signed integer) das overflow flag erfolgreiche oder fehlerhafte Aktionen anzeigen. Das Vorgehen ist im folgenden Beispiel für vorzeichenlose Zahlen verdeutlicht. addl jc movl %ebx,%eax # ebx+eax → eax ERR1 %eax,Z1 Die direkt auf den Additionsbefehl folgende Zeile (jc= jump if cf=1 (cf gesetzt)) führt im Fehlerfalle eine Verzweigung zum Label ERR1 aus, wo eine Fehlerbehandlung zu erfolgen hat. Wenn die Bedingung cf=1 nicht erfüllt ist, wird mit der nächsten Zeile (kopiere eax nach Z1) fortgefahren. Weitere Verzweigungsmöglichkeiten sind in Tabelle 12 aufgeführt. 7.2.2 Addition, Subtraktion Für die Addition und Subtraktion von Festkommazahlen wird nicht zwischen Zahlen mit und ohne Vorzeichen unterschieden. Dies liegt in der Darstellung negativer Zahlen durch das Zweier-Komplement begründet. Die folgenden AssemblerBefehle gelten, wobei an Stelle des Opcode-Suffix l auch b oder w stehen kann. addl subl op1,op2 op1,op2 # op1+op2 → op2 # op2-op1 → op2 Für die Operanden op1 und op2 gelten diese Einschränkungen: • Die Operanden sind entweder Byte-, Wort- oder Doppelwortgrößen. • Die Operanden sind vom gleichen Datenblock, sonst ist eine Konvertierung erforderlich (siehe Abschnitt 6.4). • Nur ein Operand darf eine Speichervariable sein. 35 Beispiel: Addition von 64-Bit-Quadwörtern. Tabelle 13 zeigt die im .data-Segment definierten und initialisierten Quadwörter. Beachten Sie, dass die Speicherung der Zahlen im little-endianModus erfolgt. Da der Prozessor ’nur’ über Additionsbefehle für maximal 32-Bit-Zahlen verfügt, wird die Addition in 2 Schritten durchgeführt, wobei der eventuell auftretende Übertrag durch den Befehl adcl (’Additiere unter Einbezug des Übertrags aus der vorangehenden Addition’) erfasst wird. Die Addition der Doppelwort-Teile der Quadwörter wird also von ’rechts nach links’ vorgenommen. Q1: Q2: .data .quad .quad .text . . . . movl addl movl adcl . . . . 0x2c4f3ee876aa4520 0x773ac9021f3daaaa Q2,%eax %eax,Q1 Q2+4,%eax %eax,Q1+4 ffff.. Q2+7 Q2+6 Q2+5 Q2+4 Q2+3 Q2+2 Q2+1 Q2 Q1+7 Q1+6 Q1+5 Q1+4 Q1+3 Q1+2 Q1+1 Q1 77 3a c9 02 1f 3d aa aa 2c 4f 3e e8 76 aa 45 20 0000.. Aufgabe: Wie geht die Subtraktion Q2 − Q1 ? Vielfach werden auch die folgenden Arithmetik-Befehle benutzt: • Inkrementieren (Erhöhen um 1) incl op Tabelle 13: Addition von Quadwörtern (8Byte) • Dekrementieren (Erniedrigen um 1) decl op • 2er-Komplement (Vorzeichenwechsel bei ganzen Zahlen) negl op • 1er-Komplement not op 7.2.3 Multiplikation und Division Eine Übersicht über die möglichen Multiplikations- und Divisionsbefehle wird in den Tabellen 14, 15 und 16 gegeben. Auf den ersten Blick ist es vielleicht verwunderlich, dass ein Großteil der Multiplikationsbefehle und alle Divisionsbefehle nur einen Operanden benutzen. Die Operanden sind der Multiplikator (Tabelle 14) und der Divisor (Tabelle 15). Als zweiter, im Befehl nicht geschriebene Operand werden das Register eax und der Registerverbund eax:edx verwendet. Da daran nichts geändert werden kann, unterbleibt die Angabe in den Befehlen. Die genauen Regelungen sind in den Tabellen 14 und 15 aufgeführt. Bei Multiplikation und Division gibt es unterschiedliche Befehle für Zahlen mit und ohne Vorzeichen. mul bezieht sich auf die Multiplikation von Zahlen ohne Vorzeichen, imul auf die Multiplikation von Zahlen mit Vorzeichen. Entsprechendes gilt für die Division. Der Befehl imul für Vorzeichenzahlen existiert zusätzlich zur Ein-OperandenForm in einer Zwei-Operanden-Form (imul op1,op2 mit op1 · op2 → op2) und 36 Befehl mulb mulw mull imulb imulw imull imulw imull imulw imull Multiplikand op unsigned op unsigned op unsigned op signed op signed op signed op1,op2 op1,op2 op1,op2,op3 op1,op2,op3 al ax eax al ax eax op1 op1 op1 op1 (r16/m16/i) (r32/m32/i) (i) (i) Multiplikator (Operand) r8 oder m8 r16 oder m16 r32 oder m32 r8 oder m8 r16 oder m16 r32 oder m32 op2 (r16) op2 (r32) op2 (r16/m16) op2 (r32/m32) → Produkt → → → → → → → → → → ax dx:ax edx:eax ax dx:ax edx:eax op2 (r16) op2 (r32) op3 (r16) op3 (r32) Tabelle 14: Multiplikation von ganzen Zahlen für vorzeichenlose Zahlen (op unsigned) und vorzeichenbehaftete Zahlen (op signed). op1, op2, op3 sind sämtlich Vorzeichenzahlen. Zu den Bezeichnungen in der Tabelle: r32 bedeutet 32-Bit-Register, m16 16-bit Speichergröße (m von memory), i Festwert (i von immediate) Befehl divb divw divl idivb idivw idivl Dividend op op op op op op unsigned unsigned unsigned signed signed signed ax dx:ax edx:eax ax dx:ax edx:eax Divisor (Operand) r8 oder m8 r16 oder m16 r32 oder m32 r8 oder m8 r16 oder m16 r32 oder m32 → Quotient Rest al ax eax al ax eax ah dx edx ah dx edx Tabelle 15: Division von ganzen Zahlen für vorzeichenlose Zahlen (op unsigned) und vorzeichenbehaftete Zahlen (op signed) einer Drei-Operanden-Form (imul op1,op2,op3 mit op1 · op2 → op3). Im Gegensatz zur Ein-Operanden-Form sind bei diesen Befehlen auch Festwerte als Multiplikatoren zugelassen. Wenn bei Multiplikationen der high-Teil des Ergebnisses nicht 0 ist, werden die flags cf und of gesetzt. Umgekehrt werden die flags cf und of gelöscht, wenn das Ergebnis in al (Byte mal Byte), in ax (Wort mal Wort) oder in eax (Doppelwort mal Doppelwort) passt, die high-Teile also nicht benötigt werden. Bei der Division wird bei Ergebnisüberlauf keine flag gesetzt. In diesem Falle wird der Interrupt 00hex ausgelöst. Dies bewirkt eine Anzeige (z. B. division overflow) und die Beendigung des Programms. Ergebnisüberlauf tritt bei Division (mit div) durch 0 oder 1 und bei Zerstörung des Vorzeichens (mit idiv) auf. Deswegen ist dringend eine Überprüfung auf Ergebnisüberlauf zu empfehlen. Bei den Operationen in Tabelle 14 und Tabelle 15 verhalten sich die Register edx und eax wie ein 64-Bit-Register. Dabei stellen edx den higher und eax den lower part dar. In den folgenden Beispielszeilen sollen die Variablen ZL, ZW und ZB 32Bit-, 16-Bit und 8-Bit-Speichervariable darstellen. 37 Dividend / + + - Divisor (op) + + - → Quotient + + und Rest + + + - Tabelle 16: Vorzeichenregelung bei der Division von ganzen Zahlen mull %ebx mull imull imull divl idivw ZW $10,%ecx $0xa,ZL,%eax ZL %bx # # # # # # # Inhalt von eax wird mit ebx multipliziert, Ergebnis steht in edx:eax ZW mal ax → dx:ax 10 mal ecx → ecx 10 mal ZL → eax edx:eax durch ZL → eax, Rest in edx dx:ax durch bx → ax, Rest in dx 38 8 Programmsteuerung 8.1 Labels und flag-Register Die Programmsteuerung geschieht auf Assemblerebene durch Sprünge zu Labels. Die Sprünge können ohne oder mit Bedingung erfolgen. Bedingte Sprünge werden durch die flags des 32-Bit-flag-Registers eflag gesteuert. Die Bits 0 bis 15 des flag-Registers sind in Tabelle 17 erläutert. Die Status-flags des flagRegisters werden in bestimmten Programmsituationen gesetzt und gelöscht. Sprünge können von deren Zustand abhängig gemacht werden. flags werden in der Regel nur ausgewertet (gelesen), aber nicht direkt verändert. Eine Ausnahme bilden das carry flag, das direction flag und das interrupt flag. Sie werden mit stc, std, sti gesetzt und mit clc, cld cli gelöscht. In Abschnitt 7.2.1 sind die flags cf, of, zf, sf und af erläutert, die zur Kontrolle der arithmetischen Operationen dienen Im folgenden wird auf weitere flags eingegangen. • PARITY-flag Das parity-flag (pf) wird gesetzt, wenn nach einer Operation die Anzahl der Bits 1 im low-Byte des Ergebnisses geradzahlig ist, andernfalls wird dieses flag gelöscht. • DIRECTION-flag Das direction-flag (df ) findet bei der Verarbeitung von Zeichenketten (Strings) Verwendung (siehe Abschnitt 12). Wird df gesetzt, so werden Strings von hinten nach vorne (d. h. mit absteigender Adresse), wird df gelöscht, von vorne nach hinter durchlaufen. • INTERRUPT ENABLE-flag Das interrupt enable-flag (if ) findet bei der Interruptverarbeitung Verwendung. Wird if gesetzt, so können externe maskierbare Interrupts das momentan laufende Programm unterbrechen. Wird if gelöscht, ist dies nicht möglich. • TRAP-flag Das trap-flag (tf ) wird zum Ein-/Ausschalten des Single-Step-Modus verwendet. Wird das Trap-flag gesetzt, so erzeugt der Prozessor nach jedem Befehl einen Single-Step-Interrupt und verzweigt in eine spezielle Interruptroutine (z.B. in einen Monitor oder ein Debugger-Programm). Wird das Trapflag gelöscht, so ist der Single-Step-Modus wieder aufgehoben. Dieses flag wird z. B. von Debugging-Programmen benutzt. 8.2 Sprünge Sprungbefehle bestehen aus einem Teil, der den Maschinenbefehl des gewünschten Sprungtyps enthält, und einem Zusatz, der die Sprungweite (displacement) angibt. Der GNU-Assembler optimiert den Platzbedarf insoweit, dass - wenn möglich - ein Byte-Displacement benutzt wird (Sprünge um bis zu -128 bzw. +127 Programmschritte bezogen auf die Position des Sprungbefehls). Anderenfalls wird ein Doppelwort benutzt (bis zu -2 GByte bzw. +2 GByte Programmschritte, 32 Bit-Displacements). 16 Bit-Displacements werden nicht unterstützt. 39 Bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 flags of df if tf sf zf af pf cf Typ Unused Protected mode Protected mode Protected mode Status Control Control Control Status Status Unused Status Unused Status Unused Status Name Erläutert in Overflow Direction Interrupt Trap Sign Zero Abschnitt Abschnitt Abschnitt Abschnitt Abschnitt Abschnitt Aux. Carry Abschnitt 7.2.1 Parity Abschnitt 8.1 Carry Abschnitt 7.2.1 7.2.1 8.1 8.1 8.1 7.2.1 7.2.1 Tabelle 17: Die 16 unteren Elemente des 32-Bit-flag-Registers und deren Bedeutung Zu bemerken ist, dass die Sprungbefehle jcxz, jecxz, loop, loopz, loope, loopnz und loopne nur mit Byte-Displacements benutzt werden dürfen. Diese Sprungbefehle sollten also nur dann benutzt werden, wenn sicher ist, dass die Sprungweite unter -128 bzw. +127 ist (Die C-Compiler gcc und g++ benutzt diese Befehle nicht). 8.2.1 Unbedingter Sprung MRK: . . . . jmp 8.2.2 MRK # Sprung zur Marke MRK Bedingter Sprung Die Sprungbedingung für den bedingten Sprung kann direkt aus einer Statusprüfung (direkte Steuerung durch flags) oder aus einem vorhergehenden Vergleich abgeleitet werden. Im ersten Fall kann ein Sprung vom Wert eines flag-Bits abhängig gemacht werden. Damit kann auf Besonderheiten reagiert werden, die bei bestimmten Operationen (z. B. Überläufe oder Überträge bei Arithmetikoperationen) durch Setzen oder Löschen von flag-Bits angezeigt werden. Zu diesem Zweck sind in den Listen der Prozessorbefehle (z. B. [3] oder andere) die möglichen Änderungen des flag-Registers bei Ausführung des jeweiligen Befehls aufgeführt. addl jc %edx,%ebx FEHLER # Addition # Sprung nach FEHLER, wenn bei der 40 Befehl ja jae jb jbe jc jcxz jecxz je jg jge jl jle jna jnae jnb jnbe jnc jne jng jnge jnl jnle jno jnp jns jnz jo jp jpe jpo js jz Sprung ausführen, wenn größer größer oder gleich kleiner kleiner oder gleich carry=1 cx-register=0 ecx-register=0 gleich größer größer oder gleich kleiner kleiner oder gleich nicht größer nicht größer oder gleich nicht kleiner nicht kleiner oder gleich kein Übertrag nicht gleich nicht größer nicht größer oder gleich nicht kleiner nicht kleiner oder gleich kein Überlauf Parität ungerade positiv ungleich Überlauf Parität gerade Parität gerade Parität ungerade negativ null Bedingung cf=0 and zf=0 cf=0 cf=1 cf=1 or zf=1 cf=1 cx-register=0 ecx-reg.=0 zf=1 sf=of and zf=0 sf=of sfeqof zf=1 or sfeqof cf=1 or zf=1 cf= l cf=0 cf=0 and zf=0 cf=0 zf=0 zf=1 or sfeqof sfeqof sf=of zf=0 and sf=of of=0 pf=0 sf=0 zf=0 of=1 pf=1 pf=1 pf=o sf=1 zf=1 Bemerkungen gleich wie jnbe gleich wie jnb gleich wie jnae gleich wie jna ab 80386 gleich wie gleich wie gleich wie gleich wie gleich wie gleich wie gleich wie gleich wie gleich wie gleich wie jz jnle jnl jnge jng jbe jb jae ja jaf gleich gleich gleich gleich jle jl jge jg wie wie wie wie gleich wie jnp Tabelle 18: Sprungbefehle und deren Bedeutung. Man beachte, dass die Bezeichnungen g (greater) und l (less) für vorzeichenbehaftete Zahlen, die gleichbedeutenden Bezeichnungen a (above) and b (below) für vorzeichenlose Zahlen verwendet werden. 41 movl %ebx,RS # Addition ein Übertrag auftrat # siehe Abschnitt 7.2.1 # Fortsetzung,wenn fehlerfrei . . . . FEHLER: # Beginn der Fehlerbehandlung Häufig wird die Sprungbedingung aus einem Vergleich abgeleitet. Der Sprungbefehl kann dann als Bedingungssatz interpretiert werden: cmpl je $0,%eax LB # Vergleiche eax mit 0 # Springe zur Marke LB, wenn # gleich (je = jump if equal) Der cmp-Befehl arbeitet ganz ähnlich dem sub-Befehl (siehe Abschnitt 7.2.2). Er bildet die Differenz der Operanden (Zweiter minus Erster), speichert sie aber im Gegensatz zum sub-Befehl nicht. Wie bei der Subtraktion werden aber die entsprechenden flags gesetzt. Für den cmp-Befehl gilt: • cmpl op1,op2 berechnet op2-op1 und setzt entsprechend zf, cf, of, sf. • op1 und op2 können Register, Festwerte (op1) und Speichergrößen sein. • Größen mit und ohne Vorzeichen dürfen nicht gemischt werden 8.3 8.3.1 Schleifen Fuß- und kopfgesteuerte Schleifen, Verzweigungen Der Aufbau von Schleifen muss vom Programierer mit Hilfe elementarer Befehle gestaltet werden. Elemente der Schleife sind: • Initialisierung eines Indexregisters (häufig esi) als Laufvariable • Festlegung eines Labels für den Schleifenanfang • Erhöhung oder Erniedrigung des Indexregisters • Vergleich des Indexregisters mit der Zielgröße und ◦ Sprung zum Schleifenanfang, wenn die Zielgröße nicht erreicht ist oder ◦ Verlassen der Schleife, wenn die Zielgröße erreicht ist Fuß- und kopfgesteuerte Schleifen (Repeat- bzw. While-Schleifen), Verzweigungen (If-Then-Else, Case- bzw. Switch-Strukturen) unterscheiden sich durch die Position, an der der Vergleichsbefehl (cmp) und der Befehl für den bedingten Sprung (je, jnge, ...) auftreten. Am Beispiel einer kopfgesteuerten Schleife soll die Schleifenkonstruktion exemplarisch erläutert werden. /* Festlegung der Anzahl der Durchläufe N D*/ movl N D, %ecx /* Initialisierung des Schleifenzählers esi */ movl $0,%esi 42 /* Kopfsteuerung: Abfrage, ob der Schleifenzähler */ /* das Schleifenende erreicht hat */ SCHL: cmpl %ecx,%esi jge AUS Schleifenkörper jmp SCHL AUS: Programmfortsetzung Für die anderen genannten oder weitere Strukturen wie z. B. in einander verschachtelte Schleifen muss ein Ablaufdiagramm erstellt werden, das dann in Assemblerbefehle umgesetzt wird. 8.3.2 loop-Schleifen Der loop-Befehl ist eine vereinfachte Form der Schleifenbildung und kann nur für einfache, unverschachtelte Schleifen verwendet werden. loop benutzt das Register ecx als Schleifenzähler. Das Register ecx wird mit der gewünschten Zahl von ecx Schleifenkörper dekrementieren Durchläufen initialisiert. Der Befehl loop dekrementiert den Inhalt des Register ecx und prüft dann auf den Wert 0. Wenn ecx ungleich Null ist, ecx = 0 ? so wird ecx um 1 erniedrigt und ein Sprung zur angegebenen Marke (i. a. Schleifenanfang) durchgeführt. Ist ecx Null, wird der nachfolgende Befehl ausgeführt (siehe Abbildung 6). Da der Befehl loop den Inhalt von ecx lediglich auf die Bedingung ecx = 0 und nicht auf ecx 5 0 Abbildung 6: Ablaufplan der loopüberprüft, ergeben sich dann Endlos- Schleife schleifen, wenn ecx mit 0 oder einem negativen Wert initialisiert wurde. Struktur der loop-Schleife: movl N D,%ecx # Register ecx (Zähler) enthält # die Anzahl der Durchläufe N D ABC: Schleifenkörper loop ABC # loop dekrementiert ecx um 1, # bei ecx > 0 Sprung zum Label ABC # bei ecx = 0 Verlassen der Schleife 43 Die loop-Schleife verwendet als einziges Abbruchkriterium den Inhalt des Zählers ecx. Die Schleifen mit loope oder loopne verfügen in Kombination mit dem cmp-Befehl (e = equal, ne = not equal) über ein zusätzliches Abbruchkriterium. cmpl loopne WERT1,WERT2 ABC # Sprung zum Label abc wenn # ecx>0 und(∧) WERT16=WERT2 (d.h. zf=0) Die Schleife wird verlassen wenn ecx Null wurde oder(∨) wenn die verglichenen Werte nicht gleich sind. Die Befehle loopne bzw. loope können auch als loopnz bzw. loopz geschrieben werden. Beispiel: /* Suche in einer Liste von 100 Wörtern nach dem Wert -1 */ /* Im Datensegment: TABLE: .space 200 */ movl $-2,%esi movl $100,%ecx leal TABLE,%ebp L2: addl $2,%esi cmpw $-1,(%ebp,%esi) loopne L2 44 9 Prozeduren Ein Programm sollte nicht als ein einziger kompakter Block angelegt sein, sondern in mehrere (so viele wie nur möglich) kleine, möglichst unabhängige Module zerlegt werden, die jeweils überschaubare Aufgaben übernehmen. Damit kann erreicht werden: • Übersichtlichkeit, Lesbarkeit, Veränderbarkeit • Sparsamer Umgang mit Arbeitsspeicher • Teilaufgaben, die mehrfach benutzt werden, liegen nur einmal im Arbeitsspeicher vor. • Verwendung von Programmeinheiten in anderen Programmen Ein guter Programmierstil, um den Sie sich bemühen sollten, beinhaltet folgende Regeln: • Ein Programm sollte in Prozeduren zerlegt werden. • Jede Prozedur führt nur eine Funktion aus. • Prozeduren sollten klein gehalten werden. Aber: • Die Zerlegung sollte sinnvoll sein, da der Overhead von Prozeduren (Parameter übergeben, Datensicherung, Sprünge, ect. ) ziemlich aufwändig ist. 9.1 Prozedur und Programm Die ersten Assemblerprogramme, die wir erstellten, bestanden aus einer einzigen Prozedur, deren Programmcodeteil mit der Marke start: begann. Wenn die ausführbare Datei aus mehreren Prozeduren besteht, wird im allgemeinen die Prozedur, die vom Betriebssystem aus gestartet wird, als Programm bezeichnet. Der Aufbau dieses Programms unterscheidet sich im Prinzip nicht von dem einer Prozedur. Im Programm muss lediglich für die Schnittstellen zum Betriebssystem gesorgt werden. Diese Schnittstellen sind die • Anfangsadresse start: Bei dieser Adresse startet das Betriebssystem die Abarbeitung der Programmzeilen. Mit der Direktive .globl start wird die Anfangsadresse an den Linker weitergegeben. • und die Beendigungsbefehle mit dem Aufruf des SystemCalls exit Damit wird das Programm beendet und die Regie an das Betriebssystem zurückgegeben. Bei einem in Prozeduren strukturierten Assemblerprogramms können die Prozeduren vor oder hinter dem Programm angeordnet sein. Dies zeigt das folgende Programmmodell: 45 .text /* Beginn der Prozedur 1 */ PROZ1: ....... ret /* Ende der Prozedur 1 */ /* Beginn der Prozedur 2 */ PROZ2: ....... ret /* Ende der Prozedur 2 */ /* Beginn des Hauptprogramms */ start: .globl start ....... movl $1,%eax int $0×80 /* Ende des Hauptprogramms */ /* Beginn der Prozedur 3 */ PROZ3: ....... ret /* Ende der Prozedur 3 */ Prozeduren, die von mehreren Programmen oder Programmierern benutzt werden können - das sollte der Regelfall sein -, werden als separate Dateien formuliert, getrennt assembliert und beim Linken zu den anderen Objektdateien dazugebunden (siehe Abschnitt 4.3). Eine elegantere Lösung besteht darin, die Prozeduren in einer oder mehereren Bibliotheken zusammenzufassen und beim Binden die entsprechenden Bibliotheken anzugeben (siehe 9.10). 9.2 Aufbau einer Prozedur Das Mustergerüst einer Prozedur mit dem Namen (Adresse) PROZ1 hat folgendes Aussehen. #-----------------------------------------------------------# Name: PROZ1 # Zweck: # Eingabe: # Ausgabe: # Prozeduren: #-----------------------------------------------------------PROZ1: /* Sicherung des Registers ebp */ pushl %ebp /* den aktuellen Stackpointer (esp) in ebp für den späteren */ /* Zugriff auf Übergabeparameter fixieren */ movl %esp, %ebp /* ggfs. Reservierung von lokalem Speicher, hier 100 Byte */ subl $100,%esp /* Sicherung der Register */ pushal /* - - Prozeduranweisungen - - */ /* Register zurückspeichern */ popal 46 /* den lokalen Speicher auflassen */ addl $100,%esp /* Register ebp zurückspeichern */ popl %ebp /* Rückkehr zur rufenden Prozedur*/ ret #-----------------------------------------------------------Die meisten Zeilen sind aus Abschnitt 5.4 bekannt. Dort ist auch die Problematik dargestellt. Die Befehle pushal und popal dienen zum Sichern der Registerinhalte der rufenden Prozedur, damit nach dem Rückspeichern mit popal die rufende Prozedur in der Umgebung fortgesetzt werden kann, in der sie verlassen wurde. Wenn nur wenige Register in der gerufenen Prozedur benutzt werden, kann auch auf andere Weise gesichert werden (pushl, popl). Lokaler Speicher (lokale Variable) wird dynamisch auf dem Stack angelegt. Er wird nach Beendigung der Prozedur freigegeben. Wichtig ist eine ausreichende Beschreibung der Prozedur im Kommentarkopf. Die Beschreibung muss mindestens • eine kurze Darstellung des Zwecks der Prozedur, • den Namen der Prozedur, • den Namen der Datei, • die Eingabewerte und deren Reihenfolge, • die Rückgabewerte und deren Reihenfolge • verwendete Prozeduren enthalten. 9.3 Aufruf Der Aufruf einer Prozedur erfolgt durch den Befehl call zusammen mit der Anfangsadresse der Prozedur. Die Adresse wird im allgemeinen durch den Prozedurnamen angegeben. Auch kann jede andere in Abschnitt 5 besprochene Adressierungsart, also auch indirekte oder indizierte Adressierung verwendet werden, da Assembler und Linker dafür sorgen, dass in der ausführbaren Datei eine physikalische Adresse an die Stelle der mnemonisch codierten Adressen steht: call call ADRESSE PRZ1 oder auch in Verbindung mit Tabelle 19: call call (%ebp) (%ebp,%esi) Der Befehl call führt zwei Aktionen aus: 47 • Die Adresse, die im rufenden Programm auf call folgt (=Rücksprungadresse) wird wird auf dem Stack abgelegt. Sie belegt dort als 32-bitAdresse 1 Doppelwort. • Die rufende Prozedur verzweigt zur angegebenen Prozeduradresse und beginnt dort mit der Abarbeitung der Befehle. Eine weitergehende Sicherung von Registerinhalten, flags oder anderem erfolgt nicht, sondern wird dem Programmierer überlassen. Ebenso muss der Programmierer dafür Sorge tragen, dass bei Beendigung der Prozedur auf die Rücksprungadresse zugegriffen werden kann. Bei indirekter oder indizierter Adressierung wird die Adresse der gerufenen Prozedur einem Register entnommen. Dies kann dann verwendet werden, wenn die Adresse der aufgerufenen Prozedur .text erst bei Programmablauf bestimmt wird. ADR1: .long Im Beispiel der Tabelle 19 werden die Adressen (Namen) der ADR2: .long Prozeduren PRZA und PRZB auf den Speicherplätzen mit den start: Adressen ADR1 und ADR2 abgelegt. Die Adresse von ADR1 wird # Aufruf mit leal in ebp gespeichert. Mit der indirekten Adressierung (%ebp) . . . wird der Inhalt des Speicherplatzes angesprochen, dessen Adresse in movl ebp steht, das ist die Adresse der Prozedur PRZA. Durch Erhöhung leal von ebp um 4 steht die Adresse von ADR2 in ebp, so dass beim SCHL: Aufruf mit call die Prozedur PRZB aufgerufen wird. Diese Art des call Aufrufs von Prozeduren kann dazu benutzt werden, um in Schleifen addl Prozeduren aufzurufen, die an beliebigen Stellen im Textsegment loop stehen. . . . 9.4 9.4.1 Parameterübergabe $2,%ecx ADR1,%ebp (%ebp) $4,%ebp SCHL Tabelle 19: Aufruf der Prozeduren PRZA und PRZB in einer Schleife mit indirekter Adressierung. Grundsätzliches Prozeduren müssen so angelegt sein, dass sie von der rufenden Prozedur unabhängig sind. Die Übergabe von Parametern muss über Speicherbereiche geschehen, die beiden beteiligten Prozeduren a priori zur Verfügung stehen. Diese Bedingung erfüllen die Register und der Stack. Als Übergabegrößen werden die Werte der Parameter (“call by value”) oder die Adressen der Parameter (“call by reference”) verwendet. Der Unterschied dieser beiden Übergabearten wird als aus Programmieren 1 bekannt vorausgesetzt. 9.4.2 PRZA PRZB Register Wenn die Anzahl der Parameter nicht zu groß ist, können Register zur Parameterübergabe benutzt werden. Bei Compilern höherer Programmiersprachen (Pascal, C, ... ) wird bei Funktionen ein Register zur Übergabe des Funktionsergebnisses verwendet (siehe Tabelle 26). Als weiteres Beispiel können die SystemCalls (siehe Tabelle 33) genannt werden, bei denen die Parameterübergabe ausschließlich über die Register eax, ebx, ecx, edx stattfindet. 48 9.4.3 Stack Bei Auftreten einer größeren Zahl von Parametern oder um eine unbegrenzte Zahl von Parametern zu ermöglichen, wird der Stack zu Übergabe benutzt. Dies gilt insbesondere für die Kompilierung von Prozeduren bei höheren Programmiersprachen, bei denen ein generell anwendbares Verfahren benötigt wird. Das diesbezügliche Vorgehen wurde im Zusammenhang mit der indirekten Adressierung in Abschnitt 5.4 ausführlich dargestellt. Häufig werden die Rückgabeparameter (sekundäre Parameter) über die nicht mehr benötigten primären Parameter geschrieben. Wenn die Anzahl der primären und sekundären Parameter nicht gleich sind, können zwei Fälle unterschieden werden. 1. Es gibt mehr primäre als sekundäre Parameter. Dann müssen die überzähligen Speicherplätze auf dem Stack bei oder nach Beendigung der Prozedur freigegeben werden. Dies geschieht am einfachsten durch einen Operanden der ret-Befehl. Mit ret $n werden n Byte vom Stack entfernt. 2. Die sekundären Parameter überwiegen. In diesem Fall muss der benötigte Platz auf dem Stack vor Aufruf der Prozedur reserviert werden. Dazu kann der Befehl subl $n,%esp benutzt werden, der den Stackpointer um n Bytes nach unten verschiebt und damit n Bytes reserviert. 9.5 Lokale Variable In der Prozedur verwendete lokale Variable werden ebenso wie die Parameter auf dem Stack realisiert. Dies geschieht durch Verschieben des Stackpointers. Damit wird eine bestimmte Anzahl von Bytes dem Zugriff durch pushl entzogen und kann durch indirekte oder indizierte Adressierung angesprochen werden. Der so reservierte Bereich wird als “Stackframe” der Prozedur bezeichnet (siehe Beispiel in Abschnitt 9.2). 9.6 Rücksprung Der Rücksprung zum rufenden Programm erfolgt mit dem Befehl ret. Bei Auftreten dieses Befehls laufen folgende Schritte ab: • Von der aktuellen Position des Stackpointers aus wird ein Doppelwort als Rücksprungadresse vom Stack in das Instruction Pointer-Register eip gebracht. Dazu ist eine vorhergehende Stackbereinigung erfoderlich, d. h. sämtlich Stackbelegungen in der gerufenen Prozedur müssen rückgängig gemacht sein. • Sprung zur Rücksprungadresse und Fortsetzen der rufenden Prozedur an dieser Stelle. 9.7 Beispiel In einem Beispiel sollen die geschilderten Zusammenhänge deutlich veranschaulicht werden. Das rufende Programm habe 2 Parameter (Doppelwörter) VALA und VALB mit pushl VALA und 49 pushl VALB in der genannten Reihenfolge auf dem Stack gespeichert. Die Prozedur SUBR ermittelt aus den Parametern die Summe als Ergebniswert, der zurückgegeben wird. Für die Berechnung benötigt die Prozedur aus nicht näher erläuterten Gründen 1 Doppelwort, 1 Wort und 1 Byte als lokale Variablen. Die lokalen Variablen sollen dynamisch angelegt werden. SUBR: .globl SUBR /* Sicherung von ebp */ pushl %ebp /* Festhalten des aktuellen Werts von esp in ebp */ movl %esp,%ebp /* Verschieben des Stackpointers um 7 Byte nach unten, damit Reservieren von 7 Byte (je 1x long, word, byte) als Stackframe */ subl $7,%esp /* Sicherung der Register */ pushal /* Prozedurkörper */ /* Die Parameter werden vom Stack geholt */ movl 12(%ebp),%eax movl 8(%ebp),%ebx /* Summe */ addl %eax,%ebx /* Ergebnis auf den Stack */ movl %ebx,12(%ebp) /* Rückspeicherung (Restaurierung) der Register */ popal /* Aufgeben des Stackframes */ movl %ebp,%esp /* Restaurieren von ebp */ popl %ebp /* Rücksprung ins rufende Programm und Auflassen von 4 Byte auf dem Stack (ursprünglicher Platz des Parameters VALB) */ ret $4 9.8 Makros Ein Makro ist eine Zusammenfassung von Anweisungszeilen unter einem Namen, dem Macronamen. Ein Makro kann beim Erstellen der Quelldatei benutzt werden, wenn ein Satz von Programmzeilen mehrfach vorkommt. In diesem Fall muss bei jedem Auftreten nur der Makroname geschrieben werden. Ein zusätz- 50 licher Komfort für den Programmierer besteht darin, dass Platzhalter verwendet werden können, so dass das Makro bei der Verwendung an die aktuelle Umgebung angepasst werden kann. Das Makro besteht aus den Programmzeilen, dem Makrokörper, der durch die Direktiven .macro (Makroanfang) und .endm (Makroende) eingerahmt wird. In der Direktive .macro werden der Name des Makros und die Platzhalter genannt. Im Makrokörper muss den Platzhaltern ein “backslash” vorangestellt werden. Der Assembler ersetzt beim Assemblieren den Makronamen bei jedem Auftreten im Programmteil durch den Makrokörper und die Platzhalter durch die Ausdrücke, die in der Anfangszeile genannt sind. Die Makrodefinition muss vor dem ersten Aufruf im Programm auftreten. Beispiel: .macro movl movl leal movl int .endm EINAUS PH1 PH2 PH3 PH4 \PH1,%eax \PH2,%ebx \PH3,%ecx \PH4,%edx $0×80 An der Stelle der Programmzeile EINAUS $4 $1 MSG LMSG wird die Befehlsfolge movl movl leal movl int $4,%eax $1,%ebx MSG,%ecx LMSG,%edx $0×80 ins Programm eingesetzt und mit dem Programm assembliert. 9.9 Vergleich Prozedur/Macro Prozedur: • Geringerer Speicherbedarf, da der Programmcode nur einmal im Arbeitsspeicher steht. • Aus höheren Programmiersprachen aufrufbar. • Intensive Benutzung von Prozeduren ist guter Programmierstil. Macro: • Der Makrokörper wird bei jedem Aufruf eingefügt, d. h. derselbe Platzbedarf wie bei ausgeschriebenem Programm. • Gewisser Komfort durch Verwendung der Platzhalter. • Makros sind lediglich Programmierhilfen. 51 9.10 Bibliotheken 9.10.1 Prozeduren • Bibliotheken (Archive) von Prozeduren bestehen aus Objektdateien, die einzeln assembliert wurden. • Die Objektdateien werden durch das Werkzeug ar in die Archivdatei eingebunden. Standardmäßig hat die Archivdatei die Form libname.a, wobei name frei wählbar ist. • Durch den Linker ld werden die benutzten Bibliotheksprozeduren den Objektdateien hinzugefügt, um einen ausführbaren Modul zu erhalten. Der Linker wählt nur die benötigten Bibliotheksprozeduren aus, wenn diese einzeln assembliert wurden. ld -o datei.x datei.o andere.o libname.a • Die Erstellung und Bearbeitung der Bibliotheksdatei erfolgt mit Hilfe des Werkzeugs ar : ar -befehlscode archivname objektdateien r = Erzeugung eines Archivs oder Hinzufügen bzw. Ersetzen von Objektdateien t = Auflisten der enthaltenen Objektdateien d = Löschen von Objektdateien archivname: Standardform libname.a objektdateien: Die Objektdateien werden durch Leerzeichen getrennt. befehlscode: 9.10.2 Makros Vorgefertigte Makros können in einer Textdatei (Makrobibliothek) abgelegt und mit Hilfe der Direktive .include dateiname eingebunden werden (siehe Abschnitt 17). Ähnliche Programmierhilfen für kleinere Anwendungen sind über die Direktiven .set, .equ , .equiv möglich. 9.11 Programmaufruf mit Parametern Beim Start eines Programms (Prozedur main) aus der bash können Parameter ans Programm übergeben werden. Diese sind durch Leerzeichen zu trennen. Die Kommandozeile, mit der das Programm gestartet wurde, wird im Arbeitsspeicher abgelegt. Dabei sind die Elemente der Kommandozeile, also der Programmaufruf und die Parameter, durch Nullzeichen getrennt. Die Anfangsadressen der Elemente werden von rechts beginnend auf dem Stack gespeichert. Zuletzt wird die Anzahl der Elemente auf den Stack gebracht. Mit der Kommandozeile erde@linux:∼>prog1.x auf die Plätze fertig los ! wird die Zeichenkette prog1.x∅auf∅die∅Plätze∅f ertig∅los∅!∅ 52 im Arbeitspeicher abgelegt (∅ steht für Nullzeichen). Die Adressen der ersten Bytes der Elemente der Kommandozeile (jeweils fett gesetzt) werden beginnend mit der Adresse von ! über l, f, P, d, a bis hin zu p auf den Stack geschrieben, danach die Zahl 7 der Elemente der Kommandozeile. In den nachfolgenden Beispielen werden die Parameter als Zeichenketten aufgefasst und ausgegeben. Wenn Zahlen bei Programmstart mitgegeben werden, müssen die aus Ziffern bestehenden Zeichenketten in (binär verschlüsselte) Zahlen umgewandelt werden. In Assembler geschieht dies mit einer Abwandlung der Prozedur zur Zahleneingabe, in C mit Funktionen aus der Runtime Library stdlib.h, z. B. atoi(), atol(), atof(), strtod(). 9.12 Beispiel Als Beispiele sind zwei Programme angegeben, ein Assemblerprogramm und ein C-Programm. Das Assemblerprogramm benutzt die Prozedur COUTSTR, die eine Zeichenkette ausgibt, die bei der übergebenen Adresse beginnt und mit einer ASCII-Null endet. Das Assemblerprogramm gibt die Elemente der Programm-Kommandozeile in jeweils neue Zeilen aus. Dabei werden durch COUTSTR die Adressen in einer Schleife vom Stack geholt und als Anfangsadressen für die Ausgabe der Elemente benutzt. /* Wiedergabe der Kommandozeilen-Elemente */ start: .globl start /* Anzahl der Nullzeichen (=Anzahl der Elemente der Kommandozeile) vom Stack holen, in ecx speichern und ausgeben */ movl (%esp), %ecx call putsigned call newline /* Adresse der Zeichenketten vom Stack holen */ movl $0, %esi LAB: /* Ausgabe einer Zeichenkette ab der Adresse ebp bis zu einem Nullzeichen */ call COUTSTR call newline incl %esi cmpl %ecx, %esi jne LAB call newline movl $1,%eax int $0x80 /****************************************************************/ /* Ausgabe einer Zeichenkette, die durch ASCII-0 terminiert ist */ /* Prozedurname: COUTSTR */ /* Dateiname: string null out.s */ /* Übergabeparameter: pushl Anfangsadresse */ /* Rückgaberparameter: keine */ /****************************************************************/ 53 COUTSTR: .globl COUTSTR /* Die üblichen Präliminarien */ pushl %ebp movl %esp, %ebp pushal /* Parameter (Adresse eines Komandozeilenelements) vom Stack */ movl 8(%ebp),%ecx movl $-1, %edx /* edx wird hochgezählt, bis Nullzeichen erscheint */ STA: incl %edx cmpb $0, (%ecx,%edx) jne STA /* Ausgabe */ movl $1, %ebx movl $4, %eax int $0x80 /* Rückabwicklung */ popal movl %ebp, %esp popl %ebp ret $4 Für das nachfolgende C-Programm sind die aus dem Stack befindlichen Adressen Zeiger auf char, genauer ein Array von Zeigern auf char. Dieser Array wird durch die Größe argv beschrieben (char *argv[]). Die Anzahl der Elemente steht als int-Zahl in argc. Das Programm gibt nur die Parameter und nicht den Programmaufruf aus. /* Wiedergabe der Kommandozeilen-Argumente */ #include <iostream> void main (int argc, char *argv[]) { argv=argv+1; for (int n=0; n<argc-1; n=n+1) { cout << *argv; argv = argv + 1; cout << " "; } cout << endl; } Aufgabe: Schreiben sie ein entsprechendes Programm in Assembler! Das nächste Beispielprogramm wertet die Parameter als Zahlen aus. Im Ernstfall sollte noch vor der Umwandlung der Zeichenketten zu Zahlen überprüft werden, ob eine Umwandlung möglich ist, oder eine Funktion mit Fehlermeldung verwendet werden. 54 /* Übergabe einer Zahl bei Aufruf des Programms */ /* und Ausgabe dieser Zahl */ /* Dateiname: mainpranum.cc */ #include<stdlib.h> #include<iostream.h> void main(int argc, char *argv[]) { argv = argv + 1; /* Ausgabe: Erste Zahl * 2 als Ganze Zahl */ cout << endl << "Doppelter Wert der ersten Zahl: < atoi(*argv)*2; argv = argv + 1; /* Ausgabe: Wurzel aus zweiter Zahl (als double) */ cout << endl << Wurzel aus der zweiten Zahl : < sqrt(atof(*argv)); cout << endl; } 55 10 10.1 Handhabung von Dateien Interruptverarbeitung Der Intel 80×86/Pentium-Prozessor arbeitet im Anforderungsbetrieb, d. h. alle auftretenden Betriebssystemfunktionenen, unvorhersehbare oder vorhersehbare Probleme, werden durch sogenannte Interrupts gesteuert. Interrupts sind Unterbrechungen des laufenden Programms, die von der Hardware oder von der Software (vom laufenden Programm) ausgelöst werden. Der Prozessor besitzt sogenannte Interruptleitungen, über die aufgetretene Ereignisse, die eine Bearbeitung durch den Prozessor erfordern, dem Prozessor gemeldet werden. Ein Ereignissignal kann auf verschiedene Weise ausgelöst werden z. B. durch Drücken einer Tastaturtaste, durch eine Druckerstörung (z. B. Fehlen von Papier), durch die Beendigung der Datenübertragung zum Massenspeicher, durch ein Programm, in dem durch Null geteilt wurde. Interrupts sollen den Computer in die Lage versetzen, fast unmittelbar auf unvorhersehbare Ereignisse, die durch Hardware verursacht werden, zu reagieren und den Erfordernissen dieser Hardware nachzukommen. Solche Interrupts, die von Hardware ausgelöst werden heißen Hardware Interrupts oder externe Interrupts. Interrupts, die durch den Prozessor selbst ausgelöst werden (z. B. bei Division durch Null, werden auch Exceptions genannt. Tritt ein Interrupt auf, so wird diese Anforderung durch eine Routine, Interrupthandler oder Interrupt Service Routine (ISR) genannt, bearbeitet. Jedem Ereignistyp ist eine Interrupt Service Routine (ISR) zugeordnet. Die ISR werden vom Betriebssytem beim Start in den Arbeitsspeicher geladen. Ihre Adressen werden Interruptvektoren genannt und sind in der Interrupt Descriptor Table aufgeführt. Die vom Interrupt ausgelösten Maßnahmen ähneln sehr einem Prozeduraufruf (siehe Abschnitt 9). In Tabelle 20 sind die Schritte gegenübergestellt, die auf einen Interrupt- bzw. Prozeduraufruf folgen. Interruptbearbeitung Gemeinsam Prozeduraufruf Unterbrechung des laufenden Programms aktueller Prozessorstatus (flagRegister) → Stack aktueller Programmzeiger eip → Stack Löschen des Interrupt-flag: Prozessor berechnet aus der Interruptnummer die Adresse der ISR und lädt diese in das Register eip. Prozessor lädt die in der callInstruktion angegebene Adresse in das Register eip. Die ISR bzw. Prozedur wird gestartet. Beendigung der ISR mit iret. Zur Programmfortsetzung werden der Programmzeiger eip und das flag-Register vom Stack zurückgeholt Beendigung der Prozedur mit ret. Zur Programmfortsetzung wird eip vom Stack geholt. Tabelle 20: Vergleich der Abwicklung von Interrupt- und Prozeduraufrufen 56 Interrupt-Mechanismen können auch von Programmen benutzt werden, um vorgefertigte Prozeduren aufzurufen, die z. B. vom Betriebssystem zur Verfügung gestellt werden. Diese Interrupts, die Programm aufgerufen werden können, heißen Software Interrupts oder interne Interrupts. Sie haben große Ähnlichkeiten mit Prozeduren (siehe Tabelle 20). Tritt im Programmablauf ein Interrupt auf, so muss bei einem externen Interrupt der Interrupttyp und damit der Interruptvektor bestimmt werden. Beim internen Interrupt wird der Interruptvektor im Aufruf angegeben. Mit Hilfe der Interrupt Deskriptor Tabelle ermittelt der Prozessor die Adresse der zuständigen ISR, die dann gestartet werden kann. Das Betriebssystem LINUX stellt dem Anwender unter der Interruptnummer 80hex eine Vielzahl von Funktionen zur Verfügung. Diese Funktionen werden SystemCalls genannt. Sie sind in Abschnitt 18 namentlich aufgelistet. Nähere Informationen sind aus dem Manpages zu entnehmen. Für die SystemCalls gilt, dass Parameter, ob Werte oder Adressen, nur in Registern (und zwar in eax, ebx, ecx, edx) übergeben werden. Angaben über die Funktionen des Softwareinterrupts 80hex sind den Manpages zu entnehmen. Wie die dort zu findenden Angaben zu verwenden sind, ist in Abschnitt 18 erläutert. 10.2 Zugriff auf Dateien Der Zugriff auf eine Datei wird mit creat oder open unter Angabe der Dateinamen eingeleitet, die Benutzung der Datei (write, read, ... ) erfolgt unter Verwendung von Dateinummern (file descriptor, file handle). Der Zugriff auf Dateiinhalte erfolgt in den folgenden Schritten: • Erzeugen und Öffnen einer (nicht vorhandenen) Datei (SystemCall # 8) oder Öffnen einer vorhandenen Datei (# 5) (siehe Tabelle 21) • Rückgabe einer Zahl, mit der auf den Inhalt der Datei zugegriffen werden kann (file descriptor, handle, Dateinummer) • Zugriff auf den Dateiinhalt mit Schreiben (# 4), Lesen (# 3), Filepointer bewegen (# 19), Schließen (# 6) ect. unter Benutzung der Dateinummern Dateinummern werden ab 03hex vergeben. Darunter liegen Standardnummern für Standarddateien, die weder geöffnet noch geschlossen werden müssen: 00h Standard Input 01h Standard Output 02h Standard Error 10.3 Beispiele In Tabelle 21 sind einige SystemCall-Funktionen für Erzeugen, Öffnen, Lesen, Schreiben und Schließen von Dateien, sowie Bewegen des Filepointers aufgeführt. Die Zugriffsrechte werden bei der Erzeugung der Datei in der bei UNIX üblichen Weise (rwx-rwx-rwx) vergeben, beim Öffnen einer vorhandenen Datei wird die Zugriffsart mit 0 = read only, 1 = write only oder 2 = read&write für die 57 Funktion Rückgabeparameter Eingabeparameter Erzeugen und Öffnen creat Öffnen open Schließen close Schreiben write Lesen read Filepointer bewegen lseek eax ebx ecx edx eax ebx ecx edx eax ebx ecx edx eax ebx ecx edx eax ebx ecx edx eax ebx ecx edx Erfolg Fehler 8 Adresse des Pfads Zugriffsrechte Dateinummer <0 5 Adresse des Pfads Zugriffsart Dateinummer <0 6 Dateinummer 0 <0 4 Dateinummer Pufferadresse Pufferlänge 3 Dateinummer Pufferadresse Pufferlänge 19 Dateinummer Verschiebung in Anzahl der Bytes Bezug: 0=Anfang, 1=Aktuelle Position, 2=Ende Anzahl der geschriebenen Bytes <0 Anzahl der gelesenen Bytes <0 Pointerposition Anfang <0 bzgl. Tabelle 21: SystemCall-Funktionen zum Dateienhandling aktuelle Benutzung festgelegt. Eine mit creat erzeugte Datei wird gleichzeitig geöffnet. Sie steht dem Eigentümer (owner) zur Benutzung offen, die Zugriffart ergibt sich dabei aus dem Owner-Anteil der Zugriffsrechte. Bei Verwendung des SystemCalls creat für eine existierende Datei wird diese leer angelegt. Die ursprünglich vergebenen Zugriffsrechte bleiben erhalten. Das folgende Beispiel soll die Verwendung der SystemCall-Funktionen erläutern, stellt aber kein vollständiges Assemblerprogramm dar: /* Dateiname wird eingelesen oder als Konstante initialisiert */ FILE: .asciz ’’~ /proto1.t’’ DESCR: .long 0 ZK: .ascii ’’Dies ist der Dateiinhalt’’ LZK: .long .-ZK /* Erzeugen und Öffnen der Datei proto1.t im Homeverzeichnis / */ movl $8,%eax # Nr. des SystemCall 8=creat leal FILE,%ebx # Pfad movl $066,%ecx # Rechte (----rw-rw-) int $0×80 # Interrupt 80h movl %eax,DESCR # File Descriptor /* Abfrage, ob Vorgang erfolgreich */ . . . . /* Die Zeichenkette ZK wird in die erzeugte Datei geschrieben */ 58 movl movl leal movl int /* Abfrage, ob . . . . /* Datei DESCR movl movl int /* Abfrage, ob . . . . $4,%eax # Nr. des SystemCall 4=write DESCR,%ebx # Benutzung der Datei Nr. DESCR ZK,%ecx # Anfangsadresse LZK,%edx # Länge der Zeichenkette $0×80 # Interrupt 80h Vorgang erfolgreich */ schließen */ $6,%eax DESCR,%ebx $0×80 Vorgang erfolgreich */ Weitere SystemCall-Funktionen sind analog zu verwenden. Die benötigten Daten sind den entsprechenden Manpages zu entnehmen. 10.4 Fehlerbehandlung Die Fehlerbehandlung erfolgt mit Hilfe des Funktionswertes, der bei SystemCalls im Register eax zurückgegeben wird. Falls die betrachtete Funktion fehlerhaft gearbeitet hat, lautet der Funktionswert “-1”. Es ist unbedingt zu empfehlen, den Funktionswert auf Fehler zu überprüfen, da dies die einzige Stelle ist, an der ein fehlerhaftes Arbeiten des SystemCall gemeldet wird. 59 11 Bitmanipulation Ein direkter Zugriff auf einzelne Bits durch Adressierung einzelner Bits ist nicht möglich. Eine Gruppe von Prozessorbefehlen erlaubt, gezielt Bits innerhalb eines Bytes, Wortes oder Doppelwortes zu verändern. Diese Prozessorbefehle können in 3 Untergruppen eingeteilt werden: 1. Logischen Funktionen, die Bitsequenzen unter Benutzung von Bitmasken verändern, 2. Testfunktionen, die nach gesetzten Bits suchen und 3. Schiebebefehle, die Bitsequenzen verschieben oder rotieren lassen. 11.1 Logische Verknüpfungen Bitmasken sind Bit-Sequenzen, die zur Modifikation anderer Bit-Sequenzen dienen. Bitmaske A und zu modifizierende Bit-Sequenz B werden “übereinandergelegt” und nach einer der in Tabelle 22 angegebenen Regeln modifiziert. Assemblerbefehle stehen für die logischen Verknüpfungen and, or, A B A and B A or B A xor B xor und not zur Verfügung. Die Syn0 0 0 0 0 tax hat folgende Form für die logi1 0 0 1 1 sche Verknüpfung Opcode und die 0 1 0 1 1 Operanden bitmaske A und bitse1 1 1 1 0 quenz B. Der Befehl lautet am Beispiel des logischen UND, wenn die Tabelle 22: Logische Operationen Bitmaske in al und die zu modifizierende Bitsequenz in cl stehen. OpCode andb bitmaske A, bitsequenz B %al,%cl bitmaske A und bitsequenz B können Register und Speicherwerte als Bytes, Wörter oder Doppelwörter sein, A auch ein Festwert. Wichtige Anwendung für die logischen Operationen sind Ausschalten von Bits (and), Einschalten von Bits (or) und Komplementieren von Bits (xor). Beispiele: 1. Die linke Hälfte des Registers al soll komplementiert werden, der Rest soll unverändert bleiben: xorb 0xf0,%al # (0xf0 = 1111 0000b) 2. In Register %esi soll das low word gelöscht werden: andl 0xff00,%esi 3. In Register bp sollen die Bits 1, 3 und 6 eingeschaltet werden: orw 0x4a,%bp Weitere zur Bitmanipulation dienliche Operatoren sind: not: Bildung des 1er-Komplementes neg: Bildung des 2er-Komplementes 60 not A 1 0 1 0 Befehl bsf op1,op2 bsr op1,op2 bswap op bt op1,op2 Beschreibung Bit Scan Forward Sucht in op1 nach dem ersten gesetzten Bit, beginnend bei Bit Nr.0, die Nr. des Bit wird in op2 gespeichert und zf wird gelöscht. Wird kein gesetztes Bit gefunden, wird zf gesetzt. Bit Scan Reverse Sucht in op1 nach dem ersten gesetzten Bit, beginnend bei Bit Nr.15 bzw. Nr.31, die Nr. des Bit wird in op2 gespeichert und zf wird gelöscht. Wird kein gesetztes Bit gefunden, wird zf gesetzt. Byte Swap Die Reihenfolge der Bytes wird vertauscht Bit Test Prüft in op2, ob das Bit an der in op1 gegebenen Position gesetzt ist. Falls ja, wird cf auf 1 gesetzt. op1 r16,m16 r32,m32 r16,r32 i8,i8 Bit Test and Complement Wie bt, zusätzlich wird das geprüfte Bit invertiert. r16,r32 i8,i8 btr op1,op2 Bit Test and Reset Wie bt, zusätzlich wird das geprüfte Bit auf 0 gesetzt. r16,r32 i8,i8 bts op1,op2 Bit Test and Set Wie bt, zusätzlich wird das geprüfte Bit auf 1 gesetzt. r16,r32 i8,i8 Tabelle 23: Befehle zum Test von Wörtern und Doppelwörtern auf einzelne Bits mit kurzer Erläuterung der Funktionalität und der Bedingungen Testfunktionen Für den Test von Bitsequenzen auf gesetzte Bits stehen der Prozessorbefehl test, der einen logischen Vergleich mit einer Bitmaske durchführt, und eine Reihe von Prozessorbefehlen zur Verfügung, die in einer Bitsequenz nach gesetzten Bits suchen. test op1,op2 Der Operand op1 stellt die Maske dar, op2 ist der zu prüfende Operand. Bit 1 in der Maske gibt an, dass das entsprechende Bit in op2 getestet werden soll. test wirkt wie and, ohne jedoch die Bit-Sequenz op2 zu ändern, d. h. ohne das Ergebnis zu speichern. Das Ergebnis kann über die Zero-flag zf abgefragt werden (jz oder jnz). zf wird gesetzt, wenn kein getestetes Bit gesetzt (=1) war und wird gelöscht, wenn mindestens ein getestetes Bit gesetzt war. Beachten Sie die Parallele zu den Befehlen cmp bzw. sub. Weitere Testbefehle sind in der Tabelle 23 zusammengefasst. 61 r16 r32 r32 btc op1,op2 11.2 op2 r16,m16 r32,m32 r16,m16 r32,m32 r16,m16 r32,m32 r16,m16 r32,m32 r16,m16 r32,m32 r16,m16 r32,m32 r16,m16 r32,m32 r16,m16 r32,m32 Opcode shl/shr Aktion Bit-Sequenz nach links/ rechts verschieben Bemerkung Auffüllen mit 0, Ausgesondertes Bit → cf, Vorzeichenlose Zahlen sal/sar Bit-Sequenz nach links/ rechts verschieben MSB wird restauriert, Ausgesondertes Bit →cf, Vorzeichen-Zahlen, sal≡shl rol/ror Bit-Sequenz nach links/ rechts rotieren Auffüllen mit ausgesondertem Bit, cf wie oben rcl/rcr Bit-Sequenz nach links/ rechts rotieren Bit-Sequenz & cf bilden 9- bzw. 17-Bit-Einheit Tabelle 24: Schiebe- und Rotationsbefehle 11.3 11.3.1 Schiebeoperationen Überblick Die mögliche Arten von Befehlen zum Schieben und Rotieren von Bits Bytes, Wörtern und Doppelwörten sind in Tabelle 24 aufgezählt. Für die Anwendung der Befehle gilt: Form: Opcode AnzahlSchiebungen, BitSequenz Dabei: Bitsequenz: Register und Speichergrößen, Bytes, Wörter und Doppelwörter AnzahlSchiebungen: 1, Register cl, 8 Bit-Festwert 11.3.2 Shift Left- und Shift Right-Operationen Beim einfachen Schieben von Bit-Sequenzen werden freiwerdende Stellen mit Nullen aufgefüllt, das letzte herausgeschobenen Bit wird in dem Carry flag gespeichert. Beispiele: movb shlb $0b11001100,%dl # dx=0xcc=204 $3,%dl # Ergebnis ist dl=01100000b,CF=0 movb shrb $0b11001100,%dl $3,%dl # Ergebnis ist dl=00011001b,CF=1 movw shlw $0xcc,%dx $3,%dx # dx=204 # dx=0x660=1632=23*204 movw shrw $0xcc,%dx $3,%dx # dx=204 # dx=0x19=25=204/23 (Rest 4) andb shrb $0b11000000,%al $3,%al shlb shrb $2,%al $5,%al oder 62 Wichtige Anwendung sind die schnelle Multiplikation und Division von ganzen Zahlen mit bzw. durch ganze Zahlen. Jeder Schiebeschritt (shl bzw. shr) bedeutet Multiplikation mit 2 bzw. Division durch 2. Die Multiplikation einer in %edx stehenden Zahl mit movl mull $2,%eax %edx benötigt im Pentium-Prozessor 10 und im 80486-Prozessor 40 Takte. Sie kann mit shll $1,%edx in 1 Takt (Pentium) bzw. 2 Takten (80486) ausgeführt werden. Die Multiplikation mit 8 braucht bei Verwendung mehrerer Schiebeoperationen mit shll $3,%edx 1 Takt bzw. 2 Takte. Eine weitere Anwendung ist die Isolation einzelner Bits. 11.3.3 Shift Arithmetic Left/Right (sal, sar) Die Befehle sal und sar dienen der Verarbeitung von vorzeichenbehafteten Zahlen. Der Unterschied zu den gewöhnlichen Shift-Befehlen besteht darin, dass beim Verschieben nach rechts das Vorzeichenbit nach jeder Verschiebung um ein Bit wiederhergestellt wird. Der Links-Schiebebefehl sal ist identisch zu shl. 11.3.4 Rotate-Befehle (rol, ror, rcl, rcr) Die Befehle rol, ror arbeiten wie die Befehle shl, shr (auch hinsichtlich des carry-flag) mit dem Unterschied, dass die freiwerdenden Bitstellen nicht mit Nullen, sondern mit den auf der anderen Seite herausgeschobenen Bits aufgefüllt werden. Die Rotate-Befehle können benutzt werden, um Teilbereiche innerhalb eines Bytes oder eines Wortes auszutauschen, z. B. um die rechte und linke Bithälfte zu vertauschen. Beispiele: movl rolb rolw $0b10110010,%al $4,%al # al = 00101011 und cf=1 $8,%ax # al und ah vertauscht Bei den Befehlen rcl, rcr (rotate with carry) bilden die Bitsequenz (Byte, Wort oder Doppelwort) mit dem carry-flag eine 9-Bit-, 17-Bit- oder 33-BitEinheit, wobei das carry flag Bit Nr. 8, 16 bzw. 32 darstellt. Beim Rotieren werden die Bits durch das carry-flag hindurchgeschoben. 63 12 Stringverarbeitung Strings (Zeichenketten) sind zusammenhängende Folgen (Tabellen) von Bytes, Wörtern oder Doppelwörtern. Strings können als Zeichenketten mit den bisher bekannt gewordenen Prozessorbefehlen verarbeitet werden. Zum einfacheren und schnelleren Umgang mit Strings verfügt der Intelprozessor über spezielle Befehle. Diese Befehle zur Stringverarbeitung erzeugen einen effizienteren Code für die Programmierung von Stringmanipulationen. Sie lassen sich in zwei Gruppen aufteilen: • Einfache Stringbefehle ohne Wiederholung • Wiederholungsbefehle Ein einfacher Stringbefehl kann durch ein sogenanntes Präfix zu einem Wiederholungsbefehl umgewandelt werden. • Übersetzungsbefehl xlat. Eine gewisse Sonderposition nimmt die Funktion xlat ein, die eine Übersetzung von Bytes mit Hilfe einer Tabelle gestattet. 12.1 Einfache Stringbefehle Die einfachen Stringbefehle sind in Tabelle 25 zusammengestellt. Für sie gilt: • Die genannten Stringbefehle werden ohne Operanden benutzt. • Alle einfachen Befehle zur Stringverarbeitung beziehen sich nur auf ein Stringelement (Byte, Wort oder Doppelwort). Der gewünschte Datenblock wird durch das OpCode-Suffix ausgewählt. • Die Adressen der referenzierten Bytes, Wörter oder Doppelwörter der Strings werden in den Registern esi (Quellstring) und edi (Zielstring) erwartet. • Nach der Operation werden esi und edi inkrementiert, wenn das directionflag df = 0 ist, und dekrementiert, wenn das direction-flag df = 1 ist. Das direction-flag wird mit std gesetzt und mit cld gelöscht. 12.2 Wiederholungsbefehle Neben der bekannten Schleifenbildung (siehe Abschnitt 8.3) gibt es eine besonders einfache Variante durch das Wiederholungspräfix rep. Der Wiederholungsbefehl rep kann nur bei Wiederholung einer einzelnen Befehlszeile benutzt werden und verwendet das Register ecx als Wiederholungszähler. Er bildet eine sogenannte Hardwareschleife. Dies bedeutet, dass der in der Schleife auszuführende Befehl nur einmal, und nicht bei jedem Schleifendurchlauf, von der Fetch-Unit des Prozessors in die execute Unit geladen werden muss. Die Befehlssequenz rep movsb 64 Funktion movsb movsw movsl cmpsb cmpsw cmpsl scasb scasw scasl lodsb lodsw lodsl stosb stosw stosl Aktion Kopiere ein Byte von Quellstring (Index esi) nach Zielstring (Index edi) Kopiere ein Wort von Quellstring (Index esi) nach Zielstring (Index edi) Kopiere ein Doppelwort von Quellstring (Index esi) nach Zielstring (Index edi) Vergleiche die Bytes von Quellstring (Index esi) und Zielstring (Index edi) Vergleiche die Wörter von Quellstring (Index esi) und Zielstring (Index edi) Vergleiche die Doppelwörter von Quellstring (Index esi) und Zielstring (Index edi) Suche nach dem Zeichen al in Zielstring (Index edi) Suche nach dem Wort ax in Zielstring (Index edi) Suche nach dem Doppelwort eax in Zielstring (Index edi) Lade das Byte aus Quellstring (Index esi) nach al Lade das Wort aus Quellstring (Index esi) nach ax Lade das Doppelwort aus Quellstring (Index edi) nach eax Speichere das Byte in al nach Zielstring (Index edi) Speichere das Wort in ax nach Zielstring (Index edi) Speichere das Doppelwort in eax nach Zielstring (Index edi) Tabelle 25: Liste der einfachen Stringbefehle. Die Adressen der betrachteten Bytes, Wörter oder Doppelwörter müssen zuvor in die Register esi bzw. edi gespeichert werden. ist durch einen Ablaufplan in Abbildung 7 beschrieben. Die Schleife muss in 2 Zeilen geschrieben werden. 1. Der Inhalt von Register ecx wird auf 0 überprüft. Wenn ecx 6= 0, wird der nachfolgende Befehl ausgeführt, nämlich ecx dekrementieren esi und edi inkrementieren (df=0) oder dekrementieren (df=1) (a) ein Byte wird im Arbeitssspeicher von Adresse esi nach Adresse edi kopiert, Byte (esi) nach Byte (edi) kopieren (b) esi und edi werden inkrementiert (df = 0) bzw. dekrementiert (df =1 ), ecx = 0 ? (c) ecx wird dekrementiert, 2. die Schleife wird verlassen, wenn der Inhalt von ecx Null ist. Im folgenden Beispiel werden 35 Byte einer Zeichenkette, die ab der Adresse SOURCE abgelegt ist, an eine andere Stelle des Arbeitsspeichers ab der Adresse DESTINATION kopiert: cld Abbildung 7: Ablaufplan des Wiederholungsbefehls rep mit movsb als Schleifenkörper # Vorwärtskopieren 65 leal leal movl rep movsb SOURCE,%esi # Anfangsadresse des Quellstrings DESTINATION,%edi # Anfangsadresse des Zielstrings $35,%ecx # 35 Bytes sollen kopiert werden Einige Wiederholungsbefehle (repe, repne, repz, repnz) enthalten eine zweite Abbruchbedingung, die für die Vergleichsoperationen cmps und scas verwendet werden kann. repe,repz Wiederholung, solange ecx 6= 0 und bei Gleichheit im Vergleich repne,renpz Wiederholung, solange ecx 6= 0 und bei Ungleichheit im Vergleich Weitere Anwendungen für Stringbefehle mit Wiederholung sind Vergleich von Texten (cmps), Suche nach bestimmten Zeichen (scas), Ersetzen von bestimmten Zeichen (lods, stos) und Verschieben von Feldern im Arbeitsspeicher (movs). Die Harwareschleife rep wird sinnvoll nur mit den Befehlen movs und stos eingesetzt, die erweiterten Hardwareschleifen repe/repz und repne/repnz nur mit den Befehlen sca und cmps (Warum wohl?). Aufgabe: Wie kann ein Text von 250 Zeichen um 15 Byte in Vorwärtsrichtung verschoben werden, ohne Teile des Textes vor dem Kopieren zu zerstören? 12.3 Übersetzungsbefehl xlat Die Wirkungsweise von xlat sei am Beispiel der hexadezimalen Darstellung einer Zahl zwischen 0 und 16 erläutert. Die Zahl steht (binär verschlüsselt) an der Adresse ZHL. Zunächst wird eine Übersetzungstabelle definiert, im Beispiel: HEXTAB: .ascii ’’0123456789abcdef’’ Diese Übersetzungstabelle besteht aus 16 Zeichen und ordnet jeder Zahl (Index) von 0 bis 15 ein Zeichen zu, z. B. der Zahl 11 das Zeichen b oder der Zahl 4 das Zeichen 4. Die Zahl (Index) muss jeweils im Register al stehen, die Anfangsadresse der Übersetzungstabelle wird im Register ebx erwartet. Wenn die Register al und ebx vorbelegt sind, kann der Befehl xlat gegeben werden. leal HEXTAB,%ebx movb ZHL,%al xlat /* Ausgabe des Zeichens */ . . . Ist der Inhalt von al 0b00001101, so wird durch xlat das 14te Zeichen der Übersetzungstabelle, also d nach al kopiert. Zur Ausgabe größerer Zahlen in hexadezimaler Form müssen diese in Quadrupel von Bits (nibble) aufgelöst werden. Dazu eignen sich die Befehle aus Abschnitt 11 (Bitmasken, Schiebeoperationen). Die nibbles werden in der gezeigten Weise in Zeichen umgesetzt. Die hexadezimale Ausgabe von Zahlen ist deswegen besonders einfach und schnell, da keine Rechenoperationen nötig sind. Wichtige Anwendungen sind neben der Ausgabe von (binär) im Rechner gespeicherten Zahlen in hexadezimaler Form die Modifizierung von Zeichensätzen. 66 13 Hochsprachenprogramme Assemblerbefehle oder Assemblermodule werden in Programmen verwendet, um für häufig durchlaufene, zeitkritische Teile einen optimalen Maschinencode zu erzwingen, um eine bequeme Schnittstellenprogrammierung zu ermöglichen oder um Prozessorbefehle auszunutzen, die von den Compilern nicht unterstützt werden. Letzteres gewinnt durch zunehmende Bedeutung von Multimedia-Anwendungen zunehmend an Bedeutung und führte zur Erweiterung der Befehlssätze von Prozessoren. Bei Intel-Prozessoren läuft dies unter der allgemeinen Bezeichnung SIMD (Single Instruction - Multiple Data) mit den Ausführungen MMX (MultiMedia eXtension) und SSE2 (Streaming SIMD Extension 2), bei AMDProzessoren unter 3DNow!. Assemblerbefehle können in zweierlei Weise in Hochsprachenprogrammen benutzt werden. 1. Assemblerbefehle können direkt in den Hochsprachen-Programmcode eingetragen werden. Dort werden sie von einem Inline-Assembler verarbeitet, der in den Hochsprachen-Compiler integriert ist und die wichtigsten Assemblerbefehle kennt. 2. Assemblerbefehle können in genuinen Assembler-Prozeduren (siehe Abschnitt 9) zu Modulen zusammengefasst für bestimmte Teilaufgaben eingesetzt werden. Die Prozeduren werden zu Objektmodulen assembliert und durch den Linker zu anderen z. B. aus Hochsprachen-Quelldateien stammenden Objekt-Modulen gebunden. Die Bedingungen, unter denen der Einbau von Assemblerbefehlen vorgenommen wird, ist stark von der verwendeten Hochsprachen und dem benutzten Compiler abhängig. Für die Betrachtungen in diesem Manuskript ist der GNU C-Compiler gcc (damit ist im Folgenden der C++ Compiler g++ eingeschlossen) zugrunde gelegt. 13.1 Inline-Assemblerbefehle Die Verwendung von Assemblerteilen in Hochsprachenprogrammen in Form von Assemblerprozeduren ist wegen des Overheads bei Prozeduraufruf und Prozedurbeendigung nur sinnvoll, wenn größere Programmteile in Assembler formuliert werden. Für kleinere Programmteile können Assemblerbefehle direkt in den Hochsprachen-Code integriert werden. Eine Assemblerzeile hat die folgende Form: asm (feld1 : feld2 : feld3 : feld4 ) Die Felder werden durch Doppelpunkte getrennt. Besondere Beachtung verdienen die Schnittstellen zwischen dem Hochsprachenteilen und dem Assemblerteil. Die Schnittstellen regeln die Übergabe von Größen, die im Hochsprachenteil deklariert wurden, in den Assemblerteil (Eingabe) und umgekehrt (Ausgabe). Dort können die Hochsprachen-Variablen bestimmten Registern zugeordnet werden. Im Fall der Hochsprache C/C++ ergibt sich folgende Form der Einbindung von Assemblerbefehlen in ein C-Programm: 67 feld1 feld2 feld3 feld4 Im ersten Feld, dem Befehlsfeld, steht in Hochkomma der Assemblerbefehl, wobei Register durch doppelte %-Zeichen eingeleitet werden. Als Platzhalter für die Hochsprachenvariable wird %0 verwendet. Das zweite Feld, das Ausgabefeld, enthält einen Ausgabeparameter (Ausgabe aus Sicht des Assemblerteils). Der Datentyp wird mit d (ganze Zahlen) oder f (Gleitkommazahlen) gekennzeichnet. Das Zeichen = steht bei der Ausgabe. In der Klammer steht der Variablenname. Das dritte Feld ist das Eingabefeld. Es enthält analog zu feld2 einen Eingabeparameter. Im das vierte Feld kann das veränderte Register geschrieben werden (mit nur einem %-Zeichen). int main () { . . . . . // C-programmzeilen asm("movl %0, %%edx":/* keine Ausgabe */:"d "(C_VAR1):"%edx"); //Übergabe der C-Variablen C_VAR1 ins Register edx asm("movl %0, %%ecx":/* keine Ausgabe */:"d "(C_VAR2):"%ecx"); //Übergabe der C-Variablen C_VAR2 ins Register ecdx asm(" Assemblerbefehle "); asm(" Assemblerbefehle "); . . . . . // Weitere Assemblerbefehle asm("movl %%eax,%0":/*"=d "(C_VAR1):/* keine Eingabe */:/* keine Änderungen */); //Übergabe des Registers in die C-Variablen C_VAR1 . . . . . } // C-programmzeilen Im folgenden Beispielprogramm zur Berechnung der Fakultät wird eine vereinfachte Schreibweise benutzt. /* Beispielprogramm für Einbettung von Assemblerzeilen */ /* in ein C-Programm zur Berechnung der Fakultät */ #include<iostream.h> int main () { int n, su; cout << "Anzahl? "; cin >> n; /* Übergabe der Variablen n in Register ecx */ asm("movl %0, %%ecx::"d "(n):"%ecx"); asm("movl $1, %eax"); asm("movl $0, %esi"); asm("LB:"); 68 asm("incl %esi"); asm("mull %esi"); asm("cmpl %ecx, %esi"); asm("jne LB"); /* Übergabe des Registers eax in die Variable su */ asm("movl %%eax, %0":"=d "(su)); cout << "Fakultät: n! = " << su << endl; } Dieses Beispiel soll noch in einer anderen Syntax dargestellt werden: /* Beispielprogramm für Einbettung von Assemblerzeilen */ /* in ein C-Programm zur Berechnung der Fakultät */ #include<iostream.h> int main () { int n, su; cout << "Anzahl? "; cin >> n; asm( "movl $1, %eax;" "movl $0, %esi;" "LB:;" "incl %esi;" "mull %esi;" "cmpl %ecx, %esi;" "jne LB;" /* Übergabe des Registers eax in die Variable su */ /* Übergabe der Variablen n in Register ecx */ :"=a (su):"=c "(n)); cout << "Fakultät: n! = " << su << endl; } Diese Schreibweise enthält in feld1 sämtliche Assemblerzeilen (durch Semikolon abgeschlossen und in Gänsefüßchen eingeschlossen) dahinter das Ausgabefeld und das Eingabefeld. Das optionale feld4 ist weggelassen. Durch die Buchstaben in den Ein-/Ausgabefeldern wird das betroffene Register festgelegt. Folgende Zuordnung gilt: Buchstabe a b c d S D Register eax ebx ecx edx esi edi In einem weiteren Beispiel werden mehrere C-Variable in die Register des Assemblerteils übergeben. 69 /* Beispielprogramm für Einbettung von Assemblerzeilen */ /* in ein C-Programm */ /* Einbringen von mehreren C-Variablen */ #include<iostream.h> int main() { int n1=3, n2=5, n3=7; int su; /* Übernahme der Variablen n1, n2, n3 in die Register eax, ebx, ecx asm("movl %0,%%eax"::"d "(n1):"%eax"); asm("movl %0,%%ebx"::"d "(n2):"%ebx"); asm("movl %0,%\ecx"::"d "(n3):"%ecx"); asm("addl %ecx, %ebx"); asm("addl %ebx, %eax"); /* Übergabe des Registers eax in die Variable su */ asm("movl %%eax,%0":"=d "(su)); cout << "Summe (3+5+7) = " << su << endl; } 13.2 Verbindung von Assembler- und Hochsprachenmodulen Aus Sicht des Prozessors ist das zu verarbeitende Modul (Prozedur, Programm) unabhängig von der Art der Entstehung. Die Art der Entstehung (Assemblierung, Compilierung) hängt von der Art der zu erstellenden Software und den Vorstellungen des Programmierers ab. Grundsätzlich entstehen im ersten Bearbeitungsschritt sogenannte Objektdateien mit Objektcode, die durch einen weiteren Schritt (“Linker”) miteinander verbunden werden müssen, um in gewünschter Weise zusammenarbeiten zu können (siehe Abschnitt 4.3). Im allgemeinen erfolgt die Erstellung der Objektdateien (Prozeduren) unabhängig voneinander. Sollen auf verschiedene Weise erstellte Objektdateien zu einer ausführbaren Datei zusammengefasst werden, ist wegen unterschiedlicher Konventionen (Aufruf, Speichermodell, Parameterübergabe, Datentypen, ... ) eine Vorbereitung der Hochsprachenprozeduren ebenso wie der Assemblerprozeduren nötig. Im Falle des GNU C-Compilers, in dem die Assemblierung in erster Linie als Zwischenstufe zur Objektdatei zu verstehen ist und weniger als eigenständige Methode, Objektcode zu erzeugen, liegen besondere Verhältnisse vor, die weiter unten beschrieben werden. Der Compiler regelt gegebenenfalls durch Optionen gesteuert, in welche Segmentstruktur das Maschinenprogramm eingebettet werden soll. So ordnet z. B. Turbo Pascal unter DOS jeder aufgenommener Unit ein eigenes Codesegment zu und gestattet nur ein Datensegment. Das von der Assemblerprozedur gewählte Speichermodell muss diese Speicherstruktur unterstützen. Die Aufrufkonventionen regeln die Art der Parameterablage auf dem Stack und die Stackbereinigung beim Rücksprung in die rufende Prozedur. Beim Aufruf einer Assemblerprozedur aus einem Pascal- Programm werden die Parameter in der Reihenfolge des Auftretens in der Parameterliste auf den Stack gebracht, beim C-Compiler in umgekehrter Reihenfolge. Danach wird die Rücksprung- 70 Parameter- bzw. Ergebnistyp C Turbo Pascal char, char, boolean, byte, shortint int,short,short integer,word int,signed,unsigned long,long int longint,comp Adresse Adresse Länge Parameter byte 1 Wort Ergebnisregister al word 1 Wort ax long long 2 Wörter 2 Wörter eax eax Tabelle 26: Übergabe des Funktionswertes bei Hochsprachen-Funktionen adresse gesichert. Die Stackbereinigung in einer Assemblerprozedur wird bei Aufruf aus C-Modulen vom rufenden Programm durchgeführt, bei Pascal muss die Stackbereinigung in jedem Modul vor dem Rücksprung vorgenommen werden. Im Gegensatz zu manchen anderen Hochsprachen kennt C nur Funktionen. Der Funktionswert wird von der rufenden Prozedur im Register eax erwartet. Wird also im C-Modul ein Funktionswert angesprochen, so muss in der Assemblerprozedur das Register eax ( bzw. al, ax, dx:ax, edx:eax je nach Funktionstyp) belegt werden. Die Übergabe eines Funktionswertes wird bei Pascal ebenso durch das Register eax vorgenommen. Die Parameterübergabe erfolgt - wie bei anderen Hochsprachen - durch Wertübergabe (“call by value”) oder durch Adressübergabe (“call by reference”). Auch Nicht-Basisdatentypen können - weniger empfehlenswert - durch call by value übergeben werden. Sie werden dabei im Stackframe der rufenden Prozedur abgelegt. 13.3 GNU-Konventionen Der GNU C-Compiler erstellt aus einer C-Quelldatei in mehreren Zwischenstufen die ausführbare Datei. In einer ersten Zwischenstufe wird der C-Code in einen Assembler-Code umgewandelt. Mit der Option -S kann z. B. der Umwandlungsvorgang bei der Stufe des Assembler-Code angehalten werden. Dabei erzeugt der C-Compiler lediglich Assembler-Code. Die Verbindung von unterschiedlichen Modulen ist deswegen ziemlich einfach, da der GNU C-Compiler in der Lage ist, unterschiedliche Module anhand der Erweiterung des Dateinamens zu erkennen und an die anderen Module anzubinden. Dabei sind C-Quelldateien mit Erweiterung .c, AssemblerQuelldateien mit der Erweiterung .s und Objektdateien mit der Erweiterung .o zu versehen. Soll ein Assembler-Modul als Programm (siehe Abschnitt 9.1) verwendet werden, muss lediglich die Standard-Startadresse start: durch die für C-Programme übliche Startadresse main ersetzt werden. Beispiel: Ein C-Programm prgtest.c benutzt eine Assemblerprozedur calcul.s und eine in eigener Datei vorliegende C-Funktion errhndl.c. Die Module sind so gestaltet, dass sie aufeinander abgestimmt sind (siehe 13.2). Dann erzeugt folgende Kommandozeile eine ausführbare Datei prgtest.x: gcc -o prgtest.x prgtest.c calcul.s errhndl.c 71 14 14.1 Programmierung des Koprozessors Ganze Zahlen Im Koprozessor existieren die in Tabelle 27 aufgeführten Formate für ganze Zahlen. Sie werden in ähnlicher Weise wie beim Hauptprozessor definiert (Direktiven) und verwendet (OpSize-Suffices). Das 64-Bit-Ganzzahlenformat existiert nur in der internen Darstellung des Koprozessors. Direktiven Binärzahlen .word .long, .int .quad gepackte BCD-Zahlen (2 Dezimalziffern pro Byte) Anzahl der Bits 16 32 64 80 OpSizeSuffix s l q Tabelle 27: Datenformate des Koprozessore für ganze Zahlen 14.2 14.2.1 Gleitkommazahlen Mantisse und Exponent Bei sehr großen und sehr kleinen Zahlen ist eine Darstellung als ganze Zahlen insbesondere in dualer Form nicht praktizierbar. Solche Zahlen werden als Gleitkommazahlen, also durch Angabe von Mantisse und Exponent angegeben. Die Entfernung zwischen Mond und Erde ist ungefähr rEM = 384.400.000m = 0b10110111010010111101010000000m, als Gleitkommazahl 3, 844 · 10 8 . In den folgenden Darstellungen sind die ersten beiden Zeilen in dezimaler, die anderen in binär/hexadezimaler Schreibweise: Schreibweise 3, 844 · 108 0, 3844·9 1, 0110111010010111101010000000 · 211100 101101110, 10010111101010000000 · 210100 14.2.2 Mantisse 3, 844 0, 3844 1, 6e97a80 16e, 97a80 Exponent 8 9 0 × 1c 0 × 14 Lage des Dezimalpunktes Die Lage des “Dezimalpunktes” wird in der Regel so gewählt, dass die Mantisse einen Wert zwischen 1 (incl.) und dem Basis-Wert annimmt. Bei dualer Darstellung wird der Dezimalpunkt so gesetzt, dass die Ziffer vor dem Dezimalpunkt eine 1 ist. 14.2.3 Darstellung im Koprozessor 80 ×87 Zur Definition einer Gleitkommazahl im Rechner (Prozessor, Arbeitsspeicher) werden Vorzeichen, Mantisse und Exponent benötigt. Der Koprozessor 80 ×87 verwendet die duale Darstellung mit der 1 vor dem Komma. Mit dieser impliziten Festlegung kann ein Bit gespart und damit die Genauigkeit der Darstellung erhöht werden. Gespeichert werden also 72 Direktive OpSize-Suffix Anzahl der Bits Vorzeichen Exponent Mantisse Vorkommateil Exponentenbias Genauigkeit (Dezimalst.) Bereich (Zehnerpot.) Short Real Long Real .float, .single s 32 1 8 23 0 128 7 ±38 .double 10-Byte Real .tfloat l 64 1 11 52 0 1024 16 ±308 t 80 1 15 63 1 16384 19 ±4932 Tabelle 28: Typen von Gleitkommazahlen im Koprozessor 80 ×86 (IEEEFließkommaformate). Die 10-Byte-Real-Gleitkommazahl wird nur intern benutzt. V o rz e ic h e n b it E x p o n e n t 6 3 6 2 M a n tis s e 5 2 5 1 3 2 3 1 0 Abbildung 8: Darstellung der Long Real Gleitkommazahl • das Vorzeichen, • die Mantisse ohne die 1 vor dem Komma und • der Exponent Der Exponent wird in der Exzessdarstellung gespeichert, also mit einem Bias versehen, um negative Exponenten darzustellen (normalisierter Exponent). Dies ist in Tabelle 28 und Abbildung 8 veranschaulicht. Beispiel: Der genaue Wert für den Abstand Erde / Mond beträgt rEM = 384.428.150m = (0b)10110111010011000100001110110m = (0b)1, 0110111010011000100001110110 · 2 11100 m. Diese Zahl soll als Short Real Gleitkommazahl im IEEE-Fließkommaformat geschrieben werden. Die Mantisse wird implizit mit einer “1” als Vorkommazahl geschrieben, der Dezimalteil besteht aus 23 Bit. Zum Exponenten 1 1100 wird der Bias 1000 0000 (=0×80) addiert. Bit 31 enthält das Vorzeichen (0 für +). Damit besteht die Zahl in der Internen Darstellung aus folgenden Bits: 0100 1101 1011 0111 0100 1111 0100 0011. Bei Vergleich der Mantissen werden Sie feststellen, dass nicht alle Bits der genannten Zahl in der REAL4-Darstellung enthalten sind. Wie groß ist die Abweichung? Schreiben Sie die im Beispiel benutzte Zahl als Short Real- und als Long Real-Gleitkommazahl in hexadezimaler Darstellung! 73 7 9 S T S T S T S T S T S T S T S T (1 (2 (3 (4 (5 (6 (7 ) ) ) ) ) 6 4 6 3 0 ) en n t p o E x ) sse i t n M a V o rz e ic h e n b it Abbildung 9: Datenregister des Koprozessors 80 ×87 14.3 Die Architektur des 80 ×87-Koprozessors Der Koprozessor 80 ×87 ist ein eigenständiger Prozessor mit • Zugriff auf den Arbeitsspeicher • eigenen Daten- und Steuerungsregistern • eigenem Befehlssatz für den Datentransfer aber ohne direkten Datenaustausch mit der CPU (Prozessor). Die in Abbildung 9 dargestellten Datenregister haben das tfloat-Format (80 Bit). Da extern nur die 32- bzw. 64-Bit-Formate benutzt werden, führen die Datentransferbefehle eine Formatkonvertierung durch. Die Register sind als Stack organisiert, d. h. es gibt Befehle mit push- und pop-Funktionen. Mit dem Befehl fld memvar (fld bedeutet floating point number load) wird beispielsweise die Speichervariable memvar aus dem Arbeitsspeicher in das Datenregister st (=top of stack) geladen. Der vorige Stackinhalt wird nach unten geschoben. Der Koprozessor verfügt über 7 weitere 16-Bit-Register, die als Steuerregister bzw. Umgebungsvariable dienen. Diese Register enthalten Informationen zur Steuerung, zum Status sowie den aktuellen Stand der Befehls- und Operandenzeiger zur Bearbeitung von Ausnahmezuständen. 14.4 Befehle, Formate Die Syntax der Prozessorbefehle des 80 ×87 ist aufgebaut aus dem Präfix f (für floating point processor), dem eigentlichen Befehlsteil (z. B. add für Addieren oder ld für Laden) und den Operanden. Operanden können explizit (bis zu 2) oder implizit definiert werden. Die Tabellen 29 und 30 geben einen Überblick über die wichtigsten Befehlstypen mit Beispielen. Aus der großen Zahl der Befehle des Koprozessors sind einige Beispiel herausgegriffen, die für Laden und Speichern, Arithmetik und Programmsteuerung zur Verfügung stehen. 74 Befehlstyp Synthax A B C Stack Arbeitsspeicher Register D Register Pop fbefehl fbefehl memory fbefehl st(x),st fbefehl st(x) fbefehlp st(x),st implizite explizite Operanden st,st(1) st memvar st,st(x) st,st(x) st(x),st Beispiel fadd fadd memvar fadd st(3),st fadd st,st(3) faddp Tabelle 29: Befehlstypen des Prozessors 80 ×87 A fld1 st 1 st(1) * fldpi 3,14.. 1 faddp 4,14 * B st st(1) Lädt 1 nach st, der vorherige Inhalt von st wird nach st(1) weitergeschoben (soviel wie push!) Lädt π nach st, die 1 wird nach st(1) weitergeschoben st(st+st(1), der vorherige Inhalt von st wird “weggepopped” MEM1 MEM2 im Datensegment: MEM1: .float 1.0 MEM2: .float 2.0 MEM1→st MEM2→st dadurch MEM1→st(1) st←st+MEM1 st→MEM1 und pop fld MEM1 fld MEM2 1,0 2,0 * 1.0 1,0 1,0 2,0 2,0 fadd MEM1 fstp MEM1 fst MEM2 3,0 1,0 1.0 1,0 * * 1,0 3,0 3,0 2,0 2,0 1,0 C st 1.0 1.0 st(1) 2.0 2.0 st(2) 3.0 3.0 4.0 3.0 3.0 Anfangszustand Addition, Ergebnis im ersten Operand, st(1) Addition, Ergebnis in st 3.0 4.0 3.0 Vertauschung st(1)/st st 1.0 2.0 st(1) 2.0 4.0 st(2) 3.0 fadd %st(1),%st fadd %st,%st(2) fxch %st(1) D faddp %st(2),%st Anfangszustand Addition und POP Tabelle 30: Beispiele zu den Befehlstypen aus Tabelle 29 75 1 5 8 c 3 c 2 c 1 c 0 7 sf 0 z f o f p f c f Abbildung 10: Inhalte von Statuswort (High Byte, oben) und flag-Register (Low Byte, unten). Die Pfeile deuten die Übertragungen aus dem Statuswort des Koprozessors in das flag-Register der CPU an. 1. Laden und Speichern Laden und Speichern von reellen Zahlen, bei der Endung p mit pop fild, fist, fistp dto. für Integerzahlen fldz, fld1, fldpi, fldl2e, fldl2t, fldlg2, fldln2 Laden von Festwerten (0, 1, π,log2 e,log2 10,log2 ln2) fxch Vertauschen der Inhalte von st und st(1) fld, fst, fstp 2. Arithmetik • fadd, fsub, fsubr, fmul, fdiv, fdivr (Endung r bedeutet umgekehrte Reihenfolge der Operanden). Diese Befehle gibt es außer für reelle Zahlen (z. B. fadd), für Integerzahlen (entsprechend fiadd) und als Befehle mit zusätzlichem “pop” (entsprechend faddp oder fiaddp). • fabs, fchs, frndint (Runden zu Integer), fsqrt, fscale (st←st*2st(1)), fprem (Rest von st/st(1)←st). Die Befehle werden alle ohne Operanden verwendet und sind implizit auf das Register st bezogen. 3. Programmsteuerung Da der Koprozessor nicht über Sprungbefehle verfügt, muss die Programmsteuerung durch die CPU vorgenommen werden. Dabei wird das Statuswort (über den Arbeitsspeicher) zur CPU übertragen und dort ausgewertet (siehe Abbildung 10): fstw mem16 fwait movw sahf mem16,%ax # Überträgt das Statuswort in den Arbeitsspeicher, Adresse mem16 # CPU wartet Übertragung ab # Inhalt von ah (= High Byte des Statuswortes) wird in die Bits 0 bis 7 des flag-Registers übertragen 76 Aktion Daten aus dem Arbeitspeicher in KoprozessorRegister laden Timing CPU agiert vor Koprozessor Koordinierung Koprozessor wartet bis CPU den Speicherzugriff beendet hat nicht nötig (CPU kann andere Arbeiten durchführen) Programmierer muss sicherstellen, dass CPU erst zugreift, wenn der Koprozessor fertig ist Bearbeitung der Daten im Koprozessor Daten aus den KoprozessorRegistern in den Arbeitsspeicher speichern Koprozessor agiert vor CPU Lösung Von Assembler bzw. CPU organisiert Befehle fwait wait, Tabelle 31: Koordinierung des Zusammenwirkens von Hauptprozessor und Koprozessor 14.5 Koordinierung des Speicherzugriffes von CPU und Koprozessor Beim Zusammenwirken der CPU (80 ×86) mit dem Koprozessor sind folgende Punkte zu beachten: • Die Prozessoren arbeiten simultan. • Die Dateneingabe und Datenausgabe für den Koprozessor ist nur über die CPU möglich. • Die Register sind nicht gegenseitig zugänglich, Datenaustausch ist nur über den Arbeitsspeicher möglich. Der Ablauf bei der Bearbeitung von Daten im Koprozessor, der sich damit ergibt, werde an Hand des folgenden Beispiels erläutert: Ein Doppelwort in eax wird über Speichervariable var32 in den Koprozessor gebracht, dort bearbeitet und in eax zurückgespeichert: /* CPU vor dem Koprozessor aktiv */ movw %eax,var32 # CPU→Memory fildl var32 # Memory→Koprozessor (st) /* Koprozessor wartet von selbst, bis die CPU fertig ist */ /* und führt dann den Transfer Arbeitsspeicher/Koprozessor aus */ . . . Bearbeitung im Koprozessor . . . /* Inhalt von ST wird als Integer-Zahl im Arbeitsspeicher in var32 gespeichert */ fist var32 # Koprozessor (st)→Memory 77 /* CPU wird gezwungen, zu warten bis der Koprozessor mit der Übertragung in den Arbeitspeicher fertig ist */ fwait /* CPU setzt ihre Tätigkeit fort */ movl var32,%eax # Memory→CPU 14.6 Erzeugung von Gleitkommazahlen Zur Erzeugung von Gleitkommazahlen kann man sich die Eigenschaft nutzbar machen, dass bei Laden von ganzen Zahlen aus dem Arbeitsspeicher in die Register des Koprozessors eine Umwandlung in die interne 80-Bit-Darstellung (10-Byte Real) stattfindet. Dabei können Gleitkommazahlen bei der Eingabe vom Prozessor in ganze Zahlen aufgeteilt und im Koprozessor wieder zusammengesetzt werden. Einfacher ist es, für die Eingabe von Gleitkommazahlen Prozeduren bzw. Funktionen in einer Hochsprache (zweckmäßigerweise in C) zu benutzen (siehe Abschnitt 13.2). 78 15 Input/Output Ports Neben dem Datenaustausch mit dem Arbeitsspeicher kann der Prozessor Daten an periphere Geräte über Input/Output Ports (I/O Ports) übermitteln oder von ihnen empfangen. I/O Ports dienen der Kommunikation (Steuerung, Datenaustausch) mit peripheren Geräten oder Schaltkreisen wie z. B. Drucker, Soundkarte, Grafikkarte, Tastatur, ISDN-Karte, Monitor etc. Sie können als Input Ports oder als Output Ports oder als bidirektionale Ports verwendet werden. 15.1 Adressraum der I/O Ports Die I/O Ports bestehen aus einzeln adressierbaren 8-Bit Speicherplätzen in einem Adressraum, der physikalisch dem Hauptspeicher angehört, logisch aber vom Arbeitsspeicher getrennt ist und mit speziellen Befehlen angesteuert wird. Der Adressraum umfasst 64 kByte und erstreckt sich von 0000 bis ffff. Der Bereich von 00f8 bis 00ff ist reserviert. Mehrere Bytes können gemeinsam als Wörter oder Doppelwörter adressiert werden. Die zu einem bestimmten peripheren Gerät oder Schaltkreis gehörige I/O Port-Adresse wird von dem peripheren Gerät oder Schaltkreis angefordert. Häufig ist im peripheren Gerät oder Schaltkreis die I/O Port-Adresse in bestimmten Grenzen einstellbar. Die I/O Ports stellen so etwas wie Briefkästen und Postfächer im Verkehr zwischen peripherem Gerät und Prozessor dar. In der nachfolgenden Tabelle ist die Zuordnung der I/O Ports am Beispiel eines PC veranschaulicht. Aus ihr Tabelle wird verständlich, dass für den Zugriff auf I/O Ports eine Adminstratorberechtigung erforderlich ist. 0000h 0020h 0040h 0060h 0061h 0064h 0070h 0080h 0094h 00A0h 00C0h 00F0h 0170h 0170h 0lF0h 0lF0h 0200h 0220h 0240h 0274h 02F8h 0330h 0376h - 000Fh 0021h 0043h 0060h 0061h 0064h 0071h 0090h 009Fh 00A1h 00DEh 00F0h 0177h 0177h 01F7h 0lF7h 0207h 022Fh 025Fh 0277h 02FFh 0331h 0276h DMA - Controller Programmierbarer Interrupt Controller (PIC) Systemzeitgeber Standard (101/102 Tasten) oder Microsoft Natural Keyboard Systemlautsprecher Standard (101/102 Tasten) oder Microsoft Natural Keyboard CMOS-/Echtzeitsystemuhr DMA-Controller DMA-Controller Programmierbarer Controller DMA-Controller Numerischer Coprozessor Zweiter IDE Controller (Dual FIFO) Intel 82371SB PCI-Bus Master IDE Controller Erster IDE Controller (Dual FIFO) Intel 82371SB PCI-Bus Master IDE Controller Gameport-Joystick Creative Sound Blaster 16 Plug and Play AVM ISDN - Controller A1 E/A-Read Data-Anschluss für ISA Plug&Play Enumerator COM-Anschluss (COM2) Creative Sound Blaster 16 Plug and Play Zweiter IDE Controller (Dual FIFO) 79 0276h 0J7Sh 0388h 0JB0h 0300h 0JF2h 0JFGh 0JF6h 03F8h 04D0h 0778h 0CF8h D800h E000h ES00h E800h E808h ES08h - 0376h 037Fh 03SBh 03BBh 0SDFh 03F5h 03F6h 03F6h 03FFh 04Dlh 077Fh 0CFFh DSlFh E0FFh ES07h ES07h E80Fh ES0Fh Intel 823715B PCI - Bus Master IDE Controller ECP - Druckeranschluss (LPTl) Creative Sound Blaster 16 Plug and Play Matrox MGA Millenniun PowerDesk Matrox MGA Millennium PowerDesk Standard - Diskettenlaufwerk-Controller Erster IDE Controller (Dual FIFO) Intel 823715B PCI - Bus Master IDE Controller COM-Anschluss (COM1) PCI - Bus ECP - Druckeranschluss (LPT1) PCI-Bus Realtek RTL8029 PCI Ethernet Adaptec AIC - 7880 PCI SCSI Controller Erster IDE Controller (Dual FIFO) Intel 822715B PCI - Bus Master IDE Controller Intel 82371SB PCI-Bus Master IDE Controller Zweiter IDE Controller (Dual FIFO) 15.2 Adressierung der I/O Ports Die Adressierung der I/O Ports erfolgt mit spezifischen I/O-Befehlen unter besonderen Schutzmechanismen. Daneben besteht die Möglichkeit, durch Spiegelung des I/O Port Bereichs in den Arbeitsspeicher (memory mapped I/O) mit den üblichen Befehlen und den üblichen Schutzmechanismen auf die I/O Ports zuzugreifen. Nur bei Benutzung des I/O Port Adressbereichs ist jedoch garantiert, dass vor Beginn der Ausführung des folgenden Befehls der vorhergehende abgeschlossen ist. Zur Adressierung der I/O Ports werden die folgenden Befehle benutzt. • Kopieren vom Port (Portadresse PORT ADR) in das Register %reg (Lesen aus Sicht des Prozessors): in PORT ADR,%reg • Kopieren aus dem Register %reg zum Port (Portadresse PORT ADR) (Schreiben aus Sicht des Prozessors): out %reg, PORT ADR Liegt die I/O Port Adresse zwischen 0x0 und 0xff, kann sie als Festwert angegeben werden, sonst über das Register dx. %reg steht für eines der Register al, ax oder eax, je nachdem ob im I/O Port-Bereich ein Byte, ein Wort oder ein Doppelwort ab der angegebenen Adresse PORT ADR belegt oder gelesen werden sollen. Die Register ax oder eax enthalten die auszugebenden oder einzulesenden Daten oder Steuerwerte. Ähnlich den Stringbefehlen movs, cmps, ... gibt es die Befehle ins und outs, die in Verbindung mit dem Wiederholungspräfix rep eine schnelle Übertragung von Datenketten zwischen I/O Ports und Arbeitsspeicher ermöglichen. 15.3 Schutzmechanismen beim Zugriff auf die I/O Ports Unter Linux arbeitet der INTEL-Prozessor im protected mode, in dem ein Multi-Tasking-Betrieb möglich ist. Zum Schutz der unterschiedlich genutzten Speicherbereiche existiert eine hierarchische Struktur für Speicherzugriffe, die 80 Funktion Eingabe ioperm Setzt die I/Oport access permission bits iopl Ändert die ioplBits inm eflags Register eax ebx ecx edx eax ebx 65hex = 101 Portadresse Anzahl Byte ab Portadresse 1=Zugriff, 0=kein Zugriff 6ehex = 110 neuer Wert der iopl-Bits (0 bis 3) Rückgabe Erfolg Fehler 0 <0 0 <0 Tabelle 32: SystemCalls für den Zugriff auf den I/O Adressraum. Für beide SystemCalls ist root-Berechtigung nötig. durch Privilegierungsniveaus (PL privilege levels) ausgedrückt wird. In Abbildung 11 ist das 4-stufige Schema des 80 ×86 Prozessors dargestellt. Programme dürfen generell nur auf Segmente (und die darin befindlichen Prozeduren, Daten o. ä.) mit geringerem oder gleichem Privilegierungsniveau als sie selbst zugreifen. Der Zugriff auf den I/O Adressraum, über den die I/O Ports erreicht werden können, ist durch die beiden Bits 12 und 13 im eflags Register (IOPL I/O privilege level) geschützt. Er ist üblicherweise nur dem PL 0 (Kernel) und dem PL 1 (Gerätetreiber) erlaubt. Weniger privilegierte Gerätetreiber werden abgewiesen, Anwendungsprogramme müssen Ein- und Ausgabe durch Systemaufrufe realisieren. Die Befehle in, ins, out, outs, cli und sti (siehe Abschnitte 13.2.1 und 13.2.2) sind I/O-sensitive Befehle. Das bedeutet, dass sie nur dann ausgeführt werden, wenn das Privilegierungsniveau des Programms, das diese Befehle benutzt, nicht höher als das IOPL ist. Um mit Hilfe des I/O Adressraums auf I/O Ports zugreifen zu können, müssen zuerst mit dem SystemCall 101 (ioperm) die I/O-port access permis- Abbildung 11: Konzept sion bits gesetzt werden, die einen bestimmten Bereich des I/O der Privilegierungsstufen Adressraums freigeben. Soll der über die I/O Adressen 0hex bis im Prozessor i386 3ffhex hinausgehende Bereich angesprochen werden, ist noch nötig, den SystemCall 110 (iopl) zu benutzen. 15.4 Beispiel: Tonausgabe mit dem Systemlautsprecher über I/O Ports Um die Tonausgabe über den Systemlautsprecher zu verstehen, sind einige Vorbemerkungen erforderlich. Im Beispiel wird der Lautsprecher direkt über den Zähler #2 des Timer Chips PIT 8253/8254 angesteuert. Der Timer Chip wird über die Ports 42hex, 43hex, 61hex gesteuert. Durch den Port 43hex, der dem Steuerregister des Timer Chips zugeordnet ist, wird einer der 3 Zähler (hier Zähler #2) für Einstellungen ausgewählt. Durch Port 42hex wird das Zählerwort an den Zähler #2 des Timer Chips übermittelt, mit dem die Frequenz 1193,18kHz . Zufestgelegt wird. Die gewünschte Frequenz ergibt sich als f = Zaehlerwort 81 P o rt B (6 1 h e x ) B it 7 ----- B it 2 B it 1 B it 0 Z ä h le r 0 Z ä h le r 1 C L K Z ä h le r 2 V e rs tä rk e r G a tte r O U T P IT 8 2 5 3 /8 2 5 4 Abbildung 12: Steuerung des Zählers #2 und des Gatters durch Port 61hex. Die Auswahl und Einstellung des Zählers #2 erfolgt über die Ports 43hex und 42hex. erst wird das niederwertige Byte, dann das höherwertige Byte an Port 42hex übermittelt. Schließlich müssen der Zähler #2 durch Bit 0 aktiviert und der Verstärker mit Bit 1 des Port 61hex angeschlossen werden. Insgesamt sind folgende Schritte erforderlich: A) Die verwendeten Ports 42hex, 43hex, 61hex werden mit dem SystemCall ioperm freigeschaltet. B) Steuerwort zur Auswahl des Zählers #2 und für Einstellungen (Modus, Anzahl der Zählerbytes, ... ) an Port 43hex C) Zählerwort zur Festlegung der Frequenz wird übermittelt, zuerst das niederwertige, dann das höherwertige Byte des Zählerworts: Port 42hex D) Einschalten des Lautsprechers: Zähler #2 wird aktiviert und der Zähler wird mit Verstärker und Lautsprecher verbunden: Bits 0 und 1 in Port 42hex setzen (siehe Abbildung 12). Ausschalten analog durch Löschen der Bits E) Sperren der Ports durch 0 in edx. Geschieht automatisch wenn der Prozess beendet ist. .set ioperm,101 .text start: .globl start /* Systemcall #101 (ioperm): Ab Port #61hex (Systemlautsprecher) wird 1 Byte freigegeben. */ movl $ioperm,%eax 82 movl movl movl int $0x61,%ebx $0x01,%ecx $1,%edx $0×80 /* Systemcall #101 (ioperm): Ab Port #42hex (Systemzeitgeber) werden 2 Byte freigegeben. Im I/O Adressraum sind nun die Ports 61hex, 42hex und 43hex freigegeben. */ movl $ioperm,%eax movl $0x42,%ebx movl $0x02,%ecx movl $1,%edx int $0×80 /* Systemcall #101 (ioperm): Port #80hex wird freigegeben. Dies wird für Festlegung der Dauer des Tons benötigt. */ movl $ioperm,%eax movl $0×80,%ebx movl $0x01,%ecx movl $1,%edx int $0×80 /*Steuerwort (jedes Bit hat eine bestimmte Bedeutung) an Port 43hex niederwertiges Bit des Zählerworts an Port 42hex höherwertiges Bit des Zählerworts an Port 42hex */ go on: movb $0b10110110,%al outb %al,$0x43 movb $0x4b,%al outb %al,$0x42 movb $0x5,%al outb %al,$0x42 /* In Port 61hex werden die Bits 0 und 1 auf 1 gesetzt, damit wird Zähler #2 aktiviert und mit dem Verstärker verbunden (Einschalten des Lautsprechers) */ inb $0x61,%al or $0b11,%al outb %al,$0x61 /* Wartezeit zur Festlegung der Tondauer */ cld movl $0x6ffff,%ecx wait m2: inb $0×80,%al loop wait m2 /* Ausschalten des Lautsprechers durch Löschen der Bits 0,1 in Port 61hex */ inb $0x61,%al 83 and $0b11111100,%al outb %al,$0x61 /* Ports sperren */ movl $ioperm,%eax movl $0x61,%ebx movl $0x01,%ecx movl $0,%edx int $0×80 /* Ports sperren */ movl $ioperm,%eax movl $0x42,%ebx movl $0x02,%ecx movl $0,%edx int $0×80 /* Ports sperren */ movl $ioperm,%eax movl $0×80,%ebx movl $0x01,%ecx movl $0,%edx int $0×80 movl int 15.5 $1,%eax $0×80 Zusammenfassung Die Programmierung von Ports setzt genaue Kenntnisse der betroffenen peripheren Geräte voraus. Das periphere Gerät bestimmt die zugeordneten Ports (z. B. durch Jumper auswählbar) und die Art der zu übermittelnden Informationen. Dennoch lassen sich einige allgemeine Aussagen zur Programmierung von Ports machen. • Aus der Gerätebeschreibung sind die Adressen der benötigten Ports und die Daten zu entnehmen, die zur Erreichung des gewünschten Zwecks zu übermitteln sind. • Häufig werden unterschiedliche Ports zur Steuerung und zur Übermittlung der Betriebsdaten benutzt. • Die Ports sind vor Inbetriebnahme mit dem SystemCall ioperm zugänglich zu machen. Dazu ist root-Berechtigung nötig. • In manchen Fällen ist das periphere Gerät nach Übermittlung der Steuerungsund Betriebsdaten zu aktivieren (Start) und gegebenenfalls zu deaktivieren (Stop). • Die Ports sollten nach Ende der Benutzung wieder gesperrt werden. 84 16 16.1 80 386 Dependent Features AT&T Syntax versus Intel Syntax In order to maintain compatibility with the output of ‘gcc’, ‘as’ supports AT&T System V/386 assembler syntax. This is quite different from Intel syntax. We mention these differences because almost all 80386 documents used only Intel syntax. Notable differences between the two syntaxes are: • AT&T immediate operands are preceded by $; Intel immediate operands are undelimited (Intel ‘push 4’ is AT&T ‘pushl $4’). AT&T register operands are preceded by ‘%’; Intel register operands are undelimited. AT&T absolute (as opposed to PC relative) jump/call operands are prefixed by ‘*’; they are undelimited in Intel syntax. • AT&T and Intel syntax use the opposite order for source and destination operands. Intel ‘add eax, 4’ is ‘addl $4, %eax’. The ‘source, dest’ convention is maintained for compatibility with previous Unix assemblers. • In AT&T syntax the size of memory operands is determined from the last character of the opcode name. Opcode suffixes of ‘b’, ‘w’, and ‘l’ specify byte (8-bit), word (16-bit), and long (32-bit) memory references. Intel syntax accomplishes this by prefixes memory operands (not the opcodes themselves) with ‘byte ptr’, ‘word ptr’, and ‘dword ptr’. Thus, Intel ‘mov al, byte ptr FOO’ is ‘movb FOO, %al’ in AT&T syntax. • Immediate form long jumps and calls are ‘lcall/ljmp $SECTION, $OFFSET’ in AT&T syntax; the Intel syntax is ‘call/jmp far SECTION:OFFSET’. Also, the far return instruction is ‘lret $STACK-ADJUST’ in AT&T syntax; Intel syntax is ‘ret far STACK-ADJUST’. • The AT&T assembler does not provide support for multiple section programs. Unix style systems expect all programs to be single sections. 16.2 Opcode Naming Opcode names are suffixed with one character modifiers which specify the size of operands. The letters ‘b’, ‘w’, and ‘l’ specify byte, word, and long operands. If no suffix is specified by an instruction and it contains no memory operands then ‘as’ tries to fill in the missing suffix based on the destination register operand (the last one by convention). Thus, ‘mov %ax, %bx’ is equivalent to ‘movw %ax, %bx’; also, ‘mov $1, %bx’ is equivalent to ‘movw $1, %bx’. Note that this is incompatible with the AT&T Unix assembler which assumes that a missing opcode suffix implies long operand size. (This incompatibility does not affect compiler output since compilers always explicitly specify the opcode suffix.) Almost all opcodes have the same names in AT&T and Intel format. There are a few exceptions. The sign extend and zero extend instructions need two sizes to specify them. They need a size to sign/zero extend from and a size to zero extend to. This is accomplished by using two opcode suffixes in AT&T syntax. Base names for sign extend and zero extend are ‘movs...’ and ‘movz...’ in AT&T syntax (‘movsx’ and ‘movzx’ in Intel syntax). The opcode suffixes are 85 tacked on to this base name, the from suffix before the to suffix. Thus, ‘movsbl %al, %edx’ is AT&T syntax for ’move sign extend from %al to %edx’. Possible suffixes, thus, are ‘bl’ (from byte to long), ‘bw’ (from byte to word), and ‘wl’ (from word to long). The Intel-syntax conversion instructions • • • • ‘cbw’ sign-extend byte in ‘%al’ to word in ‘%ax’, ‘cwde’ sign-extend word in ‘%ax’ to long in ‘%eax’, ‘cwd’ sign-extend word in ‘%ax’ to long in ‘%dx:%ax’, ‘cdq’ sign-extend dword in ‘%eax’ to quad in ‘%edx:%eax’, are called ‘cbtw’, ‘cwtl’, ‘cwtd’, and ‘cltd’ in AT&T naming. ‘as’ accepts either naming for these instructions. Far call/jump instructions are ‘lcall’ and ‘ljmp’ in AT&T syntax,but are ‘call far’ and ‘jump far’ in Intel convention. 16.3 Register Naming Register operands are always prefixes with ‘%’. The 80386 registers consist of • the 8 32-bit registers ‘%eax’ (the accumulator), ‘%ebx’, ‘%ecx’,‘%edx’, ‘%edi’, ‘%esi’, ‘%ebp’ (the frame pointer), and ‘%esp’(the stack pointer). • the 8 16-bit low-ends of these: ‘%ax’, ‘%bx’, ‘%cx’, ‘%dx’, ‘%di’, ‘%si’, ‘%bp’, and ‘%sp’. • the 8 8-bit registers: ‘%ah’, ‘%al’, ‘%bh’, ‘%bl’, ‘%ch’, ‘%cl’, ‘%dh’, and ‘%dl’ (These are the high-bytes and low-bytes of ‘%ax’, ‘%bx’, ‘%cx’, and ‘%dx’) • the 6 section registers ‘%cs’ (code section), ‘%ds’ (data section), ‘%ss’ (stack section), ‘%es’, ‘%fs’, and ‘%gs’. • the 3 processor control registers ‘%cr0’, ‘%cr2’, and ‘%cr3’. • the 6 debug registers ‘%db0’, ‘%db1’, ‘%db2’, ‘%db3’, ‘%db6’, and ‘%db7’. • the 2 test registers ‘%tr6’ and ‘%tr7’. • the 8 floating point register stack ‘%st’ or equivalently ‘%st(0)’, ‘%st(1)’, ‘%st(2)’, ‘%st(3)’, ‘%st(4)’, ‘%st(5)’, ‘%st(6)’, and ‘%st(7)’. 16.4 Opcode Prefixes Opcode prefixes are used to modify the following opcode. They are used to repeat string instructions, to provide section overrides, to perform bus lock operations, and to give operand and address size (16-bit operands are specified in an instruction by prefixing what would normally be 32-bit operands with a ’operand size’ opcode prefix). Opcode prefixes are usually given as single-line instructions with no operands, and must directly precede the instruction they act upon. For example, the ‘scas’ (scan string) instruction is repeated with: repne scas 86 Here is a list of opcode prefixes: • Section override prefixes ‘cs’, ‘ds’, ‘ss’, ‘es’, ‘fs’, ‘gs’. These are automatically added by specifying using the SECTION:MEMORY-OPERAND form for memory references. • Operand/Address size prefixes ‘data16’ and ‘addr16’ change 32-bit operands/addresses into 16-bit operands/addresses. Note that 16-bit addressing modes (i.e. 8086 and 80286 addressing modes) are not supported (yet). • The bus lock prefix ‘lock’ inhibits interrupts during execution of the instruction it precedes. (This is only valid with certain instructions; see a 80386 manual for details). • The wait for coprocessor prefix ‘wait’ waits for the coprocessor to complete the current instruction. This should never be needed for the 80386/80387 combination. • The ‘rep’, ‘repe’, and ‘repne’ prefixes are added to string instructions to make them repeat ‘%ecx’ times. 16.5 Memory References An Intel syntax indirect memory reference of the form SECTION:[BASE + INDEX*SCALE + DISP] is translated into the AT&T syntax section:disp(base, index, scale), where BASE and INDEX are the optional 32-bit base and index registers, DISP is the optional displacement, and SCALE, taking the values 1, 2, and 8, multiplies INDEX to calculate the address of the operand. If no SCALE is specified, SCALE is taken to be 1. SECTION specifies the optional section register for the memory operand, and may override the default section register (see a 80386 manual for section register defaults). Note that section overrides in AT&T syntax must have be preceded by a ‘%’. If you specify a section override which coincides with the default section register, ‘as’ does not output any section register override prefixes to assemble the given instruction. Thus, section overrides can be specified to emphasize which section register is used for a given memory operand. Here are some examples of Intel and AT&T style memory references: AT&T: ‘-4(%ebp)’, Intel: ‘[ebp - 4]’ base is ‘%ebp’; DISP is ‘-4’. SECTION is missing, and the default section is used (‘%ss’ for addressing with ‘%ebp’ as the base register). INDEX, SCALE are both missing. AT&T: ‘foo(,%eax,4)’, Intel: ‘[foo + eax*4]’ INDEX is ‘%eax’ (scaled by a SCALE 4); DISP is ‘foo’. All other fields are missing. The section register here defaults to ‘%ds’. AT&T: ‘foo(,1)’; Intel ‘[foo]’ 87 This uses the value pointed to by ‘foo’ as a memory operand. Note that BASE and INDEX are both missing, but there is only one ‘,’. This is a syntactic exception. AT&T: ‘%gs:foo’; Intel ‘gs:foo’ This selects the contents of the variable ‘foo’ with section register SECTION being ‘%gs’. Absolute (as opposed to PC relative) call and jump operands must be prefixed with ‘*’. If no ‘*’ is specified, ‘as’ always chooses PC relative addressing for jump/call labels. Any instruction that has a memory operand must specify its size (byte, word, or long) with an opcode suffix (‘b’, ‘w’, or ‘l’, respectively). 16.6 Handling of Jump Instructions Jump instructions are always optimized to use the smallest possible displacements. This is accomplished by using byte (8-bit) displacement jumps whenever the target is sufficiently close. If a byte displacement is insufficient a long (32bit) displacement is used. We do not support word (16-bit) displacement jumps (i.e. prefixing the jump instruction with the ‘addr16’ opcode prefix), since the 80386 insists upon masking ‘%eip’ to 16 bits after the word displacement is added. Note that the ‘jcxz’, ‘jecxz’, ‘loop’, ‘loopz’, ‘loope’, ‘loopnz’ and ‘loopne’ instructions only come in byte displacements, so that if you use these instructions (‘gcc’ does not use them) you may get an error message (and incorrect code). The AT&T 80386 assembler tries to get around this problem by expanding ‘jcxz foo’ to jcxz cx zero jmp cx nonzero cx zero: jmp foo cx nonzero: 16.7 Floating Point All 80387 floating point types except packed BCD are supported. (BCD support may be added without much difficulty). These data types are 16-, 32-, and 64bit integers, and single (32-bit), double (64-bit), and extended (80-bit) precision floating point. Each supported type has an opcode suffix and a constructor associated with it. Opcode suffixes specify operand’s data types. Constructors build these data types into memory. • Floating point constructors are ‘.float’ or ‘.single’, ‘.double’, and ‘.tfloat’ for 32-, 64-, and 80-bit formats. These correspond to opcode suffixes ‘s’, ‘l’, and ‘t’. ‘t’ stands for temporary real, and that the 80387 only supports this format via the ‘fldt’ (load temporary real to stack top) and ‘fstpt’ (store temporary real and pop stack) instructions. • Integer constructors are ‘.word’, ‘.long’ or ‘.int’, and ‘.quad’ for the 16-, 32-, and 64-bit integer formats. The corresponding opcode suffixes are ‘s’ (single), ‘l’ (long), and ‘q’ (quad). As with the temporary real format the 64-bit ‘q’ format is only present in the ‘fildq’ (load quad integer to stack top) and ‘fistpq’ (store quad integer and pop stack) instructions. 88 Register to register operations do not require opcode suffixes, so that ‘fst %st, %st(1)’ is equivalent to ‘fstl %st, %st(1)’. Since the 80387 automatically synchronizes with the 80386 ‘fwait’ instructions are almost never needed (this is not the case for the and 8086/8087 combinations). Therefore, ‘as’ suppresses the ‘fwait’ instruction whenever it is implicitly selected by one of the ‘fn...’ instructions. For example, ‘fsave’ and ‘fnsave’ are treated identically. In general, all the ‘fn...’ instructions are made equivalent to ‘f...’ instructions. If ‘fwait’ is desired it must be explicitly coded. 16.8 Writing 16-bit Code While as normally writes only “pure” 32-bit i386 code, it has limited support for writing code to run in real mode or in 16-bit protected mode code segments. To do this, insert a ‘.code16’ directive before the assembly language instructions to be run in 16-bit mode. You can switch GAS back to writing normal 32-bit code with the ‘.code32’ directive. GAS understands exactly the same assembly language syntax in 16-bit mode as in 32-bit mode. The function of any given instruction is exactly the same regardless of mode, as long as the resulting object code is executed in the mode for which GAS wrote it. So, for example, the ‘ret’ mnemonic produces a 32-bit return instruction regardless of whether it is to be run in 16-bit or 32-bit mode. (If GAS is in 16-bit mode, it will add an operand size prefix to the instruction to force it to be a 32-bit return.) This means, for one thing, that you can use GNU CC to write code to be run in real mode or 16-bit protected mode. Just insert the statement ‘asm(“.code16”);’ at the beginning of your C source file, and while GNU CC will still be generating 32-bit code, GAS will automatically add all the necessary size prefixes to make that code run in 16-bit mode. Of course, since GNU CC only writes small-model code (it doesn’t know how to attach segment selectors to pointers like native ×86 compilers do), any 16-bit code you write with GNU CC will essentially be limited to a 64K address space. Also, there will be a code size and performance penalty due to all the extra address and operand size prefixes GAS has to add to the instructions. Note that placing GAS in 16-bit mode does not mean that the resulting code will necessarily run on a 16-bit pre-80386 processor. To write code that runs on such a processor, you would have to refrain from using any 32bit constructs which require GAS to output address or operand size prefixes. At the moment this would be rather difficult, because GAS currently supports only 32-bit addressing modes: when writing 16-bit code, it always outputs address size prefixes for any instruction that uses a non-register addressing mode. So you can write code that runs on 16-bit processors, but only if that code never references memory. 16.9 Notes There is some trickery concerning the ‘mul’ and ‘imul’ instructions that deserves mention. The 16-, 32-, and 64-bit expanding multiplies (base opcode ‘0xf6’; extension 4 for ‘mul’ and 5 for ‘imul’) can be output only in the one operand form. Thus, ‘imul %ebx, %eax’ does not select the expanding multiply; the expanding multiply would clobber the ‘%edx’ register, and this would confuse ‘gcc’ output. Use ‘imul %ebx’ to get the 64-bit product in ‘%edx:%eax’. We have added a two operand form of ‘imul’ when the first operand is an immediate 89 mode expression and the second operand is a register. This is just a shorthand, so that, multiplying ‘%eax’ by 69, for example, can be done with ‘imul $69, %eax’ rather than ‘imul $69, %eax, %eax’. 90 17 Direktiven Direktiven (auch Pseudooperationen genannt) sind Instruktionen an den Assembler, keine Instruktionen für den Prozessor. Sie geben dem Programmierer Kontrolle über die Arbeitsweise des Assemblers und beginnen alle mit dem Punkt (.). Üblicherweise werden Kleinbuchstaben verwendet. • .code32 erzeugt 32-bit Code • .text SUBSECTION übersetzt die folgenden Anweisungen an das Ende der betreffenden SUBSECTION (SUBSECTION ist ein absoluter Ausdruck, wird er weggelassen, so wird autom. 0 ergänzt). Dieser Abschnitt enthält Prozessorbefehle und Konstanten; Bytes in diesem Abschnitt können während der Programmausführung nicht überschrieben werden! • .global SYMBOL bzw. .globl SYMBOL macht das SYMBOL sichtbar für den Linker ld. Damit kann der Wert des SYMBOLs für andere Programme zugänglich gemacht werden, die mit ihm zusammengebunden werden. • .data SUBSECTION wie .text, nur sind Bytes in diesem Abschnitt während der Programmausführung änderbar • .ascii “STRING” erwartet 0, eine oder mehrere Zeichenketten, die durch Kommata getrennt sind. Diese werden in fortlaufende Adressen abgelegt. • .asciz “STRING” wie .ascii, jedoch wird am Ende noch eine binäre Null angehängt. • .long EXPRESSION erwartet 0, eine oder mehrere EXPRESSIONs, die durch Kommata getrennt sind. Der Wert des Ausdrucks wird in jeweils 4 bytes abgelegt. • .int EXPRESSION identisch mit .long • .space SIZE, FILL oder .skip SIZE, FILL reserviert SIZE Bytes und Initialisiert sie mit dem Wert FILL. • .fill REPEAT, SIZE, VALUE erzeugt REPEAT Kopien von SIZE Bytes mit dem Wert VALUE. REPEAT, SIZE und VALUE sind absolute Ausdrücke. SIZE und VALUE sind optional. • .if ABSOLUTE.EXPRESSION markiert einen Programmteil, der nur dann als Teil des Quellprogramms betrachtet wird, wenn ABSOLUTE.EXPRESSION ungleich Null ist. • .endif markiert das Ende von .if • .else markiert den Alternativzweig von .if; markiert den Beginn von Anweisungen, die assembliert werden, wenn die Bedingung für das vorangegangene .if falsch war • .macro und .endm gestatten die Definition von Macros, die AssemblerAusgaben (textuelle Substitution) erzeugen. .macro MACRONAME bzw. .macro MACRONAME MACROARGS Falls Argumente erforderlich sind, 91 werden diese hinter dem Namen durch Kommata oder Leerstellen voneinander getrennt. Defaultwerte können durch = WERT unmittelbar hinter dem Namen mitgegeben werden. Innerhalb des Macros werden die Argumente durch Voranstellen von \ vor den Argumentnamen ausgewertet. Beispiel: .macro RPT from=0, to=3 .long \from .if \to-\from RPT “(\from+1)” \to .endif .endm RPT erzeugt: .long .long .long .long 0 1 2 3 RPT 2 7 oder RPT 2,7 erzeugt eine entsprechende Ausgabe .long 2 bis .long 7. RPT 2,7 ist gleichwertig mit RPT to=7, from=2. RPT ,7 dagegen beginnt mit dem Defaultwert 0 und endet bei 7. • .set NAME,EXPRESSION .equ NAME,EXPRESSION .equiv NAME,EXPRESSION setzt den Namen NAME dem Ausruck EXPRESSION gleich. Mit .set WRITE,4 kann z. B. bei Benutzung des Interrupts 0×80 zur Ausgabe einer Zeichenkette geschrieben werden: movl $WRITE, %eax. • .byte EXPRESSIONS oder .word EXPRESSIONS oder .long EXPRESSIONS erwartet 0 oder mehr Ausdrücke, getrennt durch Kommata. Für jeden Ausdruck wird die entsprechende Anzahl von Bytes fortlaufend reserviert und mit den Ausdrücken belegt. • .include ’PFAD/DATEI’ Die angegeben Datei wird assembliert und an dieser Stelle eingefügt, dann wird der Rest der Quelldatei assembliert. • .org NEUER LOCATION COUNTER, FILL erhöht den Location Counter auf den angegebenen Wert und füllt mit FILL auf. Überschreiten der Segmentgrenzen ist nicht möglich. Im folgenden ist eine vollständige Liste der Direktiven aufgeführt, die unabhängig von der speziellen Rechnerkonfiguration für den GNU Assembler zur Verfügung stehen. Abort ABORT Align App-File Ascii Asciz Balign Byte Comm Data .abort .ABORT .align ABS-EXPR,ABS-EXPR .app-file STRING .ascii “STRING” .asciz “STRING” .balign ABS-EXPR,ABS-EXPR .byte EXPRESSIONS .comm SYMBOL,LENGTH .data SUBSECTION 92 Def Desc Dim Double Eject Else Endef Endif Equ Equiv Err Extern File Fill Float Global hword Ident If Include Int Irp Irpc Lcomm Lflags Line Ln Linkonce List Long Macro MRI Nolist Octa Org P2align Psize Quad Rept Sbttl Scl Section Set Short Single Size Skip Space Stab String .def NAME .desc SYMBOL,ABS-EXPRESSION .dim .double FLONUMS .eject .else .endef .endif .equ SYMBOL,EXPRESSION .equiv SYMBOL,EXPRESSION .err .extern .file STRING .fill REPEAT,SIZE,VALUE .float FLONUMS .global SYMBOL, .globl SYMBOL .hword EXPRESSIONS .ident .if ABSOLUTE EXPRESSION .include “FILE” .int EXPRESSIONS .irp SYMBOL,VALUES .irpc SYMBOL,VALUES .lcomm SYMBOL , LENGTH .lflags .line LINE-NUMBER .ln LINE-NUMBER .linkonce [TYPE] .list .long EXPRESSIONS .macro NAME ARGS .mri VAL .nolist .octa BIGNUMS .org NEW-LC,FILL .p2align ABS-EXPR,ABS-EXPR .psize LINES,COLUMNS .quad BIGNUMS .rept COUNT .sbttl “SUBHEADING” .scl CLASS .section NAME, SUBSECTION .set SYMBOL, EXPRESSION .short EXPRESSIONS .single FLONUMS .size .skip SIZE,FILL .space SIZE,FILL .stabd, .stabn, .stabs .string “STR” 93 Symver Tag Text Title Type Val Word .symver NAME,NAME2@NODENAME .tag STRUCTNAME .text SUBSECTION .title “HEADING” .type INT .val ADDR .word EXPRESSIONS 94 18 Systemfunktionen Systemfunktionen sind Prozeduren, die vom Betriebssystem dem Programmierer zur Verfügung gestellt werden. Sie sind als Software-Interrupts (siehe Abschnitt ?? programmiert. Für den Benutzer ist zunächst nur wichtig, dass sie wie Prozeduren benutzt werden können, lediglich die Art des Aufrufs und die Parameterübergabe weicht von der bei Prozeduren üblichen Art ab. Statt call PROZNAME lautet der Aufruf int NUMMER, die Parameterübergabe findet nur über die Register eax, ebx, ecx, edx statt. Die Tabelle 33 enthält die für den wichtigen Software-Interrupt 0×80 vorhandenen Funktionen. Der Software-Interrupt 0×80 ermöglicht den Zugang zu einer Sammlung von Funktionen, die einzeln nummeriert sind (siehe Tabelle 33). Die Nummer der gewünschten Funktion muss im Register eax stehen. Wenn z. B. 8 im Register eax enthalten ist (movl $8,%eax) und der Interrupt 80hex ausgelöst wird (int $0×80) wird die Prozedur creat (Erzeugen einer Datei) gestartet. In vielen Fällen sind zusätzliche Informationen nötig. Welche nötig sind, kann den Manpages entnommen werden. Beispiel: Sie wollen die Prozedur write (Ausgabe einer Kette von Bytes) benutzen. Mit man write und/oder man 2 write erhalten Sie die Beschreibung der Prozedur write in der für C-Programmierer gedachten Schreibweise: size t write (int fd, const void *buf, size t count) ↑ ↑ ↑ ↑ eax ebx ecx edx Die gezeigte Zuordnung gilt in der angegebenen Reihenfolge für alle SystemCalls. Eax enthält immer die Information über die Prozedur (z. B. 4 für write). Die nachfolgenden Parameter in der Klammer werden in dieser Reihenfolge auf die Register ebx, ecx und edx bezogen. Rückgabewerte, die über die Ausführung der Funktion informieren, werden in eax zurückgegeben. In manchen Fällen werden nicht alle Register benötigt. Im genannten Fall (write) enthalten die Register den Adressaten der Ausgabe (Monitor, Drucker, Datei (ebx), die Anfangsadresse des Puffers (ecx)(der Puffer enthält die auszugebenden Bytes) und die Länge des Puffers (edx). Der Rückgabewert in eax lautet bei aufgetretenen Fehlern -1, ansonsten enthält eax die Anzahl der tatsächlich ausgegebenen Bytes. 95 Name setup exit fork read write open close waitpid creat link unlink execve chdir time mknod chmod chown break oldstat lseek getpid mount umount setuid getuid stime ptrace alarm oldfstat pause utime stty gtty access nice ftime sync kill rename mkdir rmdir Nr 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Name dup pipe times prof brk setgid getgid signal geteuid getegid acct phys lock ioctl fcntl mpx setpgid ulimit oldolduname umask chroot ustat dup2 getppid getpgrp setsid sigaction sgetmask ssetmask setreuid setregid sigsuspend sigpending sethostname setrlimit getrlimit getrusage gettimeofday settimeofday getgroups setgroups Nr 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 Name select munmap truncate ftruncate fchmod fchown symlink oldlstat readlink uselib swapon reboot readdir mmap getpriority setpriority profil statfs fstatfs ioperm socketcall syslog setitimer getitimer stat lstat fstat olduname iopl vhangup idle vm86 wait4 swapoff sysinfo ipc fsync sigreturn clone setdomainname uname Nr 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 Name modify ldt adjtimex mprotect sigprocmask create module init module delete module get kernel syms quotactl getpgid fchdir bdflush sysfs personality afs syscall setfsuid setfsgid llseek getdents newselect flock msync readv writev getsid fdatasync sysctl mlock munlock mlockall munlockall sched setparam sched getparam sched setscheduler sched getscheduler sched yield sched get priority max sched get priority min sched rr get interval nanosleep mremap Tabelle 33: Liste der SystemCalls unter dem Interrupt 0×80 96 Nr 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 Literatur [1] ELSNER, D., FENLASON J. & friends: The GNU Assembler, 1994 http://www.gnu.org/manual/gas-2.9.1/html mono/as.html/ http://www.gnu.org/manual/gas/html mono/as.html, http://www.gnu.org/manual/gas-2.9.1/html node/as toc.html, http://www.gnu.org/manual/gas/html node/as toc.html, http://www.gnu.org/manual/gas-2.9.1/html chapter/as toc.html, http://www.gnu.org/manual/gas/html chapter/as toc.html, (Stand August 2001) /usr/info/as.info.gz, man as [2] http://developer.intel.com/design/PentiumIII/manuals/index.htm [3] WOHAK, B., MAURUS, R.: 80×86/Pentuim Assembler Programmierung unter DOS und Windows. Bonn; Albany: IWT Verlag GmbH 1995 [4] BRADLEY, D.: Programmieren in Assembler. München: Carl Hanser Verlag 1986 [5] BRORS, I.: Maschinensprache des IBM-PC/AT und kompatibler in der Praxis. Heidelberg: Hüthig Verlag 1988 [6] HAHN, H.: Assembler Inside & Out. Berkeley: McGraw Hill 1992 [7] LINK, W.: Assembler-Programmierung. Poing: Francis-Verlag 1995 [8] MATTHES, W.: Intel’s i486. Aachen: Elektor-Verlag 1992 [9] MORGAN, D.: Numerical Methods Real-Time and Embedded Systems Programming. San Mateo: M&T Publ., Prentice Hall 1992 [10] NORTON, P., SOCHA, J.: Peter Norton’s Assemblerbuch. Haar b. Mchn.: Markt & Technik Verlag 1988 [11] PODSCHUN, T.E.: Das Assembler-Buch. Bonn: Addison-Wesley 1994 [12] THIES, K.-D.: 80486 Systemsoftware-Entwicklung. München: Hanser Verlag 1992 [13] WENDLER, F.: C und Assembler in der Systemprogrammierung. Würzburg: Vogel-Verlag 1992 Motorola [14] FORD, W., TOPP, W.: MC68000 Assembly Language and Systems Programming. Lexington: Heath and Co. 1988 [15] SKINNER, T.P.: Assembly Language Programming for the 68000 Family. New York: Wiley 1988 Sonstige [16] MESSMER, H.-P: PC-Hardware. Bonn: Addison Wesley 1998 [17] BREUER H., München: dtv-Atlas zur Informatik, Tafeln und Texte (Band 2490) Deutscher Taschenbuch Verlag 97 [18] SCHNEIDER U., WERNER D., München Wien: Taschenbuch der Informatik, Fachbuchverlag Leipzig im Carl Hanser Verlag 2000 [19] JACOBSON, E.: Einführung in die Prozessdatenverarbeitung. München, Wien: Carl Hanser Verlag 1996 [20] DUMBACHER, B.: Grundlagen der Informatik, Manuskript zur Vorlesung [21] FLIK, T., LIEBIG, H.: Mikroprozessor-Technik. Berlin: Springer-Verlag 1994 [22] KAMMERER, P.: Von Pascal zu Assembler. Braunschweig: Vieweg Verlag 1998 [23] SCHREINER, A.-T.: Systemprogrammierung in UNIX. Stuttgart: Teubner Verlag 1984 98 Anhang: Tabelle der Ascii-Zeichencodes S C H L Ü S S E L B 6 B 5 0 B 3 B 2 B 1 B 4 B 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 0 1 1 A 1 3 1 1 B 1 0 1 4 0 1 C 1 1 1 5 0 1 1 9 1 0 D 1 0 1 6 1 1 7 1 1 1 0 7 8 0 1 A C K 1 0 0 0 6 1 E 1 7 F E O T E N Q 6 1 0 1 5 0 1 0 4 5 1 E T X 4 1 1 0 3 0 0 2 0 0 1 0 2 1 1 1 1 2 2 2 1 2 2 3 3 1 3 2 4 S T X 3 1 0 1 2 B E L B S H T L F V T F F C R S O S I 0 0 S O H 1 2 1 1 1 0 0 0 N U L 1 1 0 0 0 0 0 0 1 4 2 5 1 5 2 6 1 6 2 7 1 7 3 0 1 8 3 1 1 9 3 2 1 A 3 3 1 B 3 4 1 C 3 5 1 D 3 6 1 E 3 7 1 F D L E D C 1 D C 2 D C 3 D C 4 N A K S Y N E T B C A N E M S U B E S C F S G S R S U S 0 1 4 0 1 6 2 0 4 1 1 7 2 1 4 2 1 8 2 2 4 3 1 9 2 3 4 4 2 0 2 4 4 5 2 1 2 5 4 6 2 2 2 6 4 7 2 3 2 7 5 0 2 4 2 8 5 1 2 5 2 9 5 2 2 6 2 A 5 3 2 7 2 B 5 4 2 8 2 C 5 5 2 9 2 D 5 6 3 0 2 E 5 7 3 1 2 F 0 1 1 0 S P ! " # $ % & ´ ( ) * + , . / 6 0 3 2 3 0 6 1 3 3 3 1 6 2 3 4 3 2 6 3 3 5 3 3 6 4 3 6 3 4 6 5 3 7 3 5 6 6 3 8 3 6 6 7 3 9 3 7 7 0 4 0 3 8 7 1 4 1 3 9 7 2 4 2 3 A 7 3 4 3 3 B 7 4 4 4 3 C 7 5 4 5 3 D 7 6 4 6 3 E 7 7 4 7 3 F 99 1 2 3 4 5 6 7 8 9 : ; = ? > 0 1 8 0 0 < 1 4 8 4 0 8 1 4 9 4 1 8 2 5 0 4 2 8 3 5 1 4 3 8 4 5 2 4 4 8 5 5 3 4 5 8 6 5 4 4 6 8 7 5 5 4 7 9 0 5 6 4 8 9 1 5 7 4 9 9 2 5 8 4 A 9 3 5 9 4 B 9 4 6 0 4 C 9 5 6 1 4 D 9 6 6 2 4 E 9 7 6 3 4 F o c ta l 2 5 h e x 1 5 N A K 1 0 0 1 0 0 @ A B C D E F G H I J K L M O N 6 4 5 0 1 0 1 6 5 5 1 1 0 2 6 6 5 2 1 0 3 6 7 5 3 1 0 4 6 8 5 4 1 0 5 6 9 5 5 1 0 6 7 0 5 6 1 0 7 7 1 5 7 1 1 0 7 2 5 8 1 1 1 7 3 5 9 1 1 2 7 4 5 A 1 1 3 7 5 5 B 1 1 4 7 6 5 C 1 1 5 7 7 5 D 1 1 6 7 8 5 E 1 1 7 7 9 5 F 2 1 d e c im a l 1 1 1 2 0 P Q R S T U V W X Y Z [ \ ] ^ _ 8 0 6 0 8 1 6 1 1 2 2 8 2 6 2 1 2 3 8 3 6 3 1 2 4 8 4 6 4 1 2 5 8 5 6 5 1 2 6 8 6 6 6 1 2 7 8 7 6 7 1 3 0 8 8 6 8 1 3 1 8 9 6 9 1 3 2 9 0 6 A 1 3 3 9 1 6 B 1 3 4 9 2 6 C 1 3 5 9 3 6 D 1 3 6 9 4 6 E 1 3 7 9 5 6 F 1 1 1 4 0 ` a b c d e f g h i j k l m o n 1 0 9 6 7 0 1 4 1 9 7 7 1 1 4 2 9 8 7 2 1 4 3 9 9 7 3 1 4 4 1 0 0 7 4 1 4 5 1 0 1 7 5 1 4 6 1 0 2 7 6 1 4 7 1 0 3 7 7 1 5 0 1 0 4 7 8 1 5 1 1 0 5 7 9 1 5 2 1 0 6 7 A 1 5 3 1 0 7 7 B 1 5 4 1 0 8 7 C 1 5 5 1 0 9 7 D 1 5 6 1 1 0 7 E 1 5 7 1 1 1 7 F 1 p 1 1 2 q 1 1 3 r 1 1 4 s 1 1 5 t 1 1 6 u 1 1 7 v 1 1 8 w 1 1 9 x 1 2 0 y 1 2 1 z 1 2 2 { | } ~ D E L 1 2 3 1 2 4 1 2 5 1 2 6 1 2 7