Assembler-Programmierung für x86-Prozessoren/ Einleitung
Maschinensprache
BearbeitenEin Prozessor ist dazu gemacht, Programme zu verarbeiten. Dafür muss er Befehle entgegen nehmen und ausführen. Die Befehle, die ein Prozessor unterstützt, sind vom jeweiligen Hersteller festgelegt. Ihr Aufbau hängt zusätzlich noch von dem internen Aufbau des Prozessors ab. Dieser Befehlssatz ist die Maschinensprache.
Die einzige Codierung, die ein digitaler Prozessor direkt versteht, besteht aus Binärzahlen. Die Maschinensprache wird deshalb in Form binärer Zahlen gespeichert und verarbeitet. Darum bezeichnet man ausführbare Dateien im Englischen auch als "Binaries".
Als Beispielprogramm sehen Sie die ersten 8 Byte des 1. Sektors jeder Festplatte, den Beginn des so genannten „Urladeprogramms“. In der ersten Spalte steht die Speicheradresse in hexadezimaler Schreibweise, in der zweiten Spalte der Inhalt des Speicherplatzes, und zwar in binärer Darstellung:
Adr
Code
0000
11111010
0001
00110011
0002
11000000
0003
10001110
0004
11010000
0005
10111100
0006
00000000
0007
01111100
Diese Programmdarstellung ist sehr unpraktisch. Die Kolonnen von Einsen und Nullen sind unübersichtlich, fehleranfällig bei der Eingabe und nehmen unverhältnismäßig viel Platz auf dem Papier ein. Deshalb ist es üblich, die gleichen Daten in hexadezimaler Darstellung aufzulisten. Dabei kann man jeweils vier Stellen einer Binärzahl zu einer hexadezimalen Ziffer zusammenfassen. Damit besteht die Darstellung eines Bytes mit acht Binärstellen nur noch aus zwei hexadezimalen Ziffern.
Beispiel gefällig? Sie sehen hier vom gleichen Programm den Anfang, und zwar nicht nur acht Byte, sondern die ersten 32 Byte:
Adr | Code (Hexadezimal) | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0000 | FA | 33 | C0 | 8E | D0 | BC | 00 | 7C | 8B | F4 | 50 | 07 | 50 | 1F | FB | FC |
0010 | BF | 00 | 06 | B9 | 00 | 01 | F2 | A5 | EA | 1D | 06 | 00 | 00 | BE | BE | 07 |
Weil die hexadezimale Schreibweise kompakter ist, hat es sich eingebürgert, wie in diesem Beispiel, in jede Zeile den Inhalt von 16 Speicherplätzen zu schreiben. Die Spalte links zeigt daher die Nummern jedes 16ten Speicherplatzes, und zwar in hexadezimaler Darstellung. Hexadezimal 0010 ist der 17te Speicherplatz; er ist in diesem Beispiel mit BF gefüllt. Anmerkung: Hexadezimal 0010 entspricht der Dezimalzahl 0016. Häufig kennzeichnet man zur besseren Unterscheidung Hexadezimalzahlen mit "h" oder tiefgestellter "(16)" am Ende oder einem führenden "0x" (Beispiel: 0x0010 = 0010h = 001016). Dezimalzahlen erhalten stattdessen eine tiefgestellte "(10)" oder ein "d" (Beispiel: 001610 = 0016d)
Der Computer versteht, durch die binäre Codierung, nur Einsen und Nullen als Befehle. Daher ist es selbst für Spezialisten extrem schwierig ein Programm in diesem Code zu verstehen oder gar zu entwerfen. Zudem haben x86-Befehle variable Längen, von einem Byte bis zu 32 Byte für einen Befehl! Weil aber nicht nur die Befehle, sondern auch alle Daten binär codiert sind, sieht man den Bytefolgen nicht einmal an, was was ist - es gibt keinen erkennbaren Unterschied zwischen Programm- und Datencode.
Vom Maschinencode zum Assembler
BearbeitenUm die Programmierung von Computern zu vereinfachen, kam bald die Idee auf, die Befehle in einem für Menschen besser geeigneten Textformat zu schreiben. Die Übersetzung in den Maschinencode ließ man von einem Programm erledigen - dem Assembler. Das Besondere dabei ist, dass sich ein Assemblerbefehl normalerweise eins zu eins in einen Maschinenbefehl übersetzen lässt.
Der erste Befehl „FA“ des Beispielprogramms oben ist clear interrupt-flag, auf Deutsch „Interrupt-Flag löschen“ - damit werden Unterbrechungsanforderungen anderer Hardwareeinheiten ignoriert. Aus den Anfangsbuchstaben wird die Abkürzung cli
gebildet, die für einen Programmierer leichter zu merken ist als das hexadezimale FA bzw. das binäre 11111010. Solche Abkürzungen werden mnemonische Bezeichnungen, kurz: Mnemonics, genannt.
Woher kommen diese Abkürzungen? Der Hersteller des Prozessors, zum Beispiel Intel, liefert zu jedem neuen Prozessor ein umfangreiches Handbuch mit. Darin ist für jeden Befehl ein langer Name, ein Kurzname (Mnemonic) und eine Beschreibung des Befehls aufgeführt. Die Mnemonics sind möglichst kurz, um den Schreibaufwand beim Programmieren gering zu halten. So wird beispielsweise das englische Wort move (bewegen, transportieren) zu mov
verkürzt, subtract (subtrahieren) zu sub
usw.
Mit Mnemonics und lesbaren Registerbezeichnungen geschrieben sieht das Beispiel von oben so aus:
0000 FA CLI 0001 33C0 XOR AX,AX 0003 8ED0 MOV SS,AX 0005 BC007C MOV SP,7C00 0008 8BF4 MOV SI,SP 000A 50 PUSH AX 000B 07 POP ES 000C 50 PUSH AX 000D 1F POP DS 000E FB STI 000F FC CLD 0010 BF0006 MOV DI,0600 0013 B90001 MOV CX,0100 0016 F2 REPNZ 0017 A5 MOVSW 0018 EA1D060000 JMP 0000:061D 001D BEBE07 MOV SI,07BE
Der Befehl CLI
schaltet also Interrupts ab, der darauf folgende Befehl XOR
führt eine bitweise Exclusiv-Oder Verknüpfung durch. In diesem Falle führt das dazu, dass der Inhalt des AX-Registers auf den Wert "0" gesetzt wird. MOV SS, AX
bedeutet „Kopiere den Inhalt des Registers AX nach Register SS“.
Als Register bezeichnet man die wenigen Speicherplätze, die im Prozessorkern eingebaut sind. Sie werden später erläutert.
Das vollständige Urladerprogramm finden Sie hier.
Moderne Assembler-Programme erleichtern dem Programmierer die Arbeit noch weiter: Sie verstehen nämlich auch symbolische Bezeichnungen, Kommentare und Organisationsanweisungen. Der Programmierer kann sie verwenden, um seinen Code besser zu strukturieren und damit den Überblick zu behalten. Unser Beispiel könnte damit so aussehen:
ORG 07C00 ; Startadresse des Programms festlegen
AA_MBR SEGMENT CODE
STRT: CLI ; alle INTR sperren
XOR AX,AX
MOV SS,AX
MOV SP,Segment AA_MBR ; Sektor von 07C00 umkopieren
; Sektor mit 512 Byte ab 07C00h umkopieren nach 00600h
MOV SI,SP
...
JMP STEP2 ; Sprung nach 0000:061D
; Partitionstabelle wird nach aktiver Partition abgesucht
ORG 0000:0600
STEP2: MOV SI,OFFSET PAR_TAB
...
AA_MBR ENDS
END
Die ORG-Anweisung ganz am Anfang legt fest, ab welcher Speicheradresse das Programm während der späteren Ausführung im Arbeitsspeicher untergebracht werden soll. Sie wird nicht übersetzt, sondern stellt eine Steueranweisung für den Assembler dar. AA_MBR, STRT und STEP2 sind Sprungmarken, AA_MBR ist außerdem der Programmname. PAR_TAB ist die symbolische Bezeichnung der Adresse des Beginns der Partitionstabelle. Die Namen der Sprungmarken, des Programmnamens und der Adressbezeichnung hier wählt der Programmierer nach Belieben; sie könnten auch anders aussehen als in diesem Beispiel.
Das klingt kompliziert, erleichtert das Programmieren jedoch enorm (darauf gehen wir im Laufe des Buches noch genauer ein).
Ein Programm, das einen solchen Quelltext in Maschinensprache übersetzt, nennt man Assemblerprogramm oder kurz Assembler. Das englische Wort to assemble bedeutet „zusammenfügen, zusammenbauen“.
Einordnung von Assembler gegenüber anderen Programmiersprachen
BearbeitenAssemblersprachen zählen zur zweiten Generation der Programmiersprachen wenn der reine Maschinencode der ersten Generation entspricht. Da sie so stark von der Maschinensprache und der Prozessorarchitektur abhängen, muss man, wenn man von Assemblersprache spricht, eigentlich auch immer angeben, auf welche Prozessorfamilie man sich bezieht. Das bedeutet aber auch, dass ein Assembler-Programm, dass für eine bestimmte Prozessorfamilie geschrieben wurde, für eine andere Architektur komplett neu geschrieben werden muss.
Zur dritten Generation der Programmiersprachen zählen so genannte höhere Programmiersprachen wie Pascal oder C. Diese Sprachen sind leichter verständlich und nicht mehr auf ein bestimmtes Computersystem beschränkt, dabei jedoch noch so hardwarenah, dass man C z.B. auch gerne als "Hochsprachen-Assembler" bezeichnet. Im Optimalfall kann man den selben Quelltext für verschiedene Zielsysteme kompilieren, ohne dass man ihn ändern muss. In der Praxis sind häufig trotzdem noch kleinere Anpassungen erforderlich.
Höhere Programmiersprachen nehmen dem Programmierer darüber hinaus noch Dinge wie die Speicherverwaltung ab und bieten weitere Annehmlichkeiten wie mächtigere Sprachkonstrukte. Beispiele dafür sind z.B. Java, C#, Basic, usw. Dafür sind sie in der Ausführung meist deutlich langsamer als in Assembler oder C geschriebene Programme. Außerdem ist die direkte Hardwareprogrammierung oft nicht möglich. Pascal dagegen ist ein Hybrid, da es eingebetteten Assembler anbietet, aber auch noch höhere Abstraktionen als C bietet (z.B. automatische Speicherverwaltung für Strings/Zeichenketten).
Wofür benötigt man heute noch Assembler?
BearbeitenAngesichts der Begrenzung von Assemblersprachen auf eine Prozessorfamilie und der Unverständlichkeit des Codes liegt die Frage nahe: Wofür verwendet man heute noch Assembler, wo es doch komfortable Hochsprachen wie Java, C# und andere gibt?
Einige Gründe für die Verwendung von Assemblersprache:
- Treiberprogrammierung (wobei hier auch häufig C benutzt wird).
- Optimierung von (zeit-) kritischen Stellen in Programmen.
- Benutzung von Befehlen, die der Hochsprachen-Compiler (noch) nicht kennt.
- Der Programmierer hat volle Kontrolle über den erzeugten Code.
- Bei der Fehlersuche mit einem Debugger sind Assemblerkenntnisse sehr zu empfehlen.
- Kennenlernen der Arbeitsweise von Prozessor und restlichem Computersystem.
- Programmierung von eingebetteten Systemen.
Im Gegensatz zur weit verbreiteten Meinung ist ein Betriebssystem normalerweise nicht in Assembler, sondern weitgehend in einer Hochsprache geschrieben. Das liegt daran, dass Assembler nur dort benötigt wird, wo der direkte Hardwarezugriff unbedingt nötig ist, z.B. wenn für einen Zweck ein ganz bestimmtes Prozessorregister benutzt werden muss. Das betrifft z.B. kleine Teile der Speicherverwaltung oder den Bootloader des Betriebssystems.
Was wird für die Assemblerprogrammierung benötigt?
BearbeitenUm Assemblerquellcode zu übersetzen wird natürlich ein entsprechender Assembler benötigt. Hier verwenden wir NASM, den Netwide Assembler. Er kann unter DOS, Windows, Linux und OSX verwendet werden, ist freie Software und damit kostenlos und wird aktiv weiterentwickelt.
Darüber hinaus wird ein Texteditor benötigt, vorzugsweise einer, der Syntax-Highlighting für Assembler im Intel-Format unterstützt. Unter Windows würde ich Notepad++ empfehlen. Notepad++ ist ebenfalls freie Software und unterstützt Syntax-Highlighting für alle möglichen Programmiersprachen.
Unter Linux ist die Auswahl an guten Editoren sehr viel größer. Vim und Emacs sind selbstverständlich nutzbar, Nano und GEdit ebenfalls. Dazu alles Andere, was unformatierten Text speichern kann.
Den NASM gibt es hier:
Notepad++ ist hier zu finden:
Unter Linux ist es am einfachsten, einen beliebigen Editor über den jeweiligen Paketmanager zu installieren.
Weitere Informationen findet man unter anderem hier:
- The Art of Assembly
- USENET – comp.lang.asm.x86
Welche Vorkenntnisse werden benötigt?
BearbeitenIn diesem Buch wird davon ausgegangen, dass bereits Grundkenntnisse in einer imperativen Programmiersprache vorhanden sind. Es wird hier nicht mehr erklärt, was eine Schleife oder eine Auswahlanweisung ist. Außerdem sollten Sie den Umgang mit Linux und/oder Windows beherrschen.