Assembler-Programmierung für x86-Prozessoren/ Einleitung


Maschinensprache

Bearbeiten

Ein 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

Bearbeiten

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

 
Abb. 1 – Übersetzung eines Assemblerprogramms in Maschinensprache

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

Bearbeiten

Assemblersprachen 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?

Bearbeiten

Angesichts 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:

  1. Treiberprogrammierung (wobei hier auch häufig C benutzt wird).
  2. Optimierung von (zeit-) kritischen Stellen in Programmen.
  3. Benutzung von Befehlen, die der Hochsprachen-Compiler (noch) nicht kennt.
  4. Der Programmierer hat volle Kontrolle über den erzeugten Code.
  5. Bei der Fehlersuche mit einem Debugger sind Assemblerkenntnisse sehr zu empfehlen.
  6. Kennenlernen der Arbeitsweise von Prozessor und restlichem Computersystem.
  7. 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?

Bearbeiten

Um 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:

Welche Vorkenntnisse werden benötigt?

Bearbeiten

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