Wikipedia · einfach zusammengefasst · Stand
Maschinensprache
Mit einem Assembler: Assemblersprachen formulieren die Prozessorbefehle des Maschinencodes als Mnemonics in einer einfachen Syntax. Dieser Quelltext wird …
Inhalt6 Abschnitte
Grundbegriff und Bedeutung
Maschinensprache ist eine Programmiersprache, deren formale Sprachelemente unmittelbar die Instruktionen des Prozessors festlegen. Wegen ihrer Nähe zur Hardware wird sie auch als „Programmiersprache eines Computers“ bezeichnet. Welche Befehle es gibt und wie sie aufgebaut sind, hängt vom Prozessortyp ab und ist im Befehlssatz definiert. Dargestellt wird Maschinensprache meistens als Binärcode, vereinfacht auch als Hexadezimalzahlen.
Ein Maschinenbefehl ist eine Anweisung an den Prozessor, eine Operation auszuführen, etwa eine Addition oder einen Wertevergleich. Jede funktionelle Leistung eines Prozessors entsteht somit durch die Ausführung von Maschinencode. Ein Maschinenprogramm ist eine sinnvolle Folge solcher codierten Befehle und der zugehörigen Daten.
Die Begriffe Maschinencode, Maschinensprache, Binärcode, nativer Code und Programmcode werden teilweise synonym verwendet. Sie können dabei entweder allgemein die Syntax beziehungsweise das verwendete Codeformat bezeichnen, etwa den internen Binärcode einer Zentraleinheit, oder die konkreten Anweisungen eines bestimmten Programms, beispielsweise den Binärcode für „Programm ABC“, den der Computer direkt ausführen kann.
Maschinenprogramme und ihre Erstellung
Maschinenprogramme werden in allen Geräten mit Prozessoren eingesetzt: in Großrechnern, Personal Computern, Smartphones und eingebetteten Systemen, etwa in Waschmaschinen, Radios oder Fahrzeugsteuerungen für ABS und Airbag. Bei PCs liegen sie normalerweise in ausführbaren Dateien, unter Windows häufig mit der Endung „.exe“. Andere Betriebssysteme verwenden teilweise keine Dateiendung oder andere Formate; unter z/OS werden ausführbare Dateien beispielsweise als Lademodul bezeichnet. In eingebetteten Systemen und Mikrocontrollern können Maschinenprogramme dauerhaft im ROM liegen, etwa als Bootloader.
Menschen können Maschinenprogramme mit einem Hex-Editor oder einem Maschinencode-Monitor betrachten, grundsätzlich auch erstellen und verändern. Praktisch werden sie jedoch meist aus Quelltext erzeugt. Ein Assembler übersetzt Assemblersprache, ein Compiler übersetzt eine höhere Programmiersprache in ausführbaren Maschinencode. Bei interpretierten Programmen werden Maschinenbefehle beim Programmstart oder während der Laufzeit erzeugt.
Mögliche Wege der Programmerstellung sind:
- Die direkte Eingabe von Binärcode ist äußerst umständlich und fehleranfällig und seit den 1950er Jahren unüblich.
- Ein Hex-Editor kann Zahlenwerte und Opcodes, also codierte Befehlsnummern, eingeben; auch dieses Verfahren ist fehleranfällig.
- In der Assemblersprache werden Prozessorbefehle durch Mnemonics und eine einfache Syntax formuliert. Der Assembler wandelt diesen Quelltext in Maschinencode um.
- Ein Programm kann in einer Hochsprache geschrieben und durch einen Compiler übersetzt werden. Häufig entsteht dabei zunächst Objektcode.
- Eine Hochsprache kann nach der Übersetzung in einen Zwischencode oder direkt durch einen Interpreter ausgeführt werden. Java verwendet beispielsweise Bytecode, der von einem Interpreter ausgeführt wird. .NET-Sprachen wie C# werden in eine Intermediate Language übersetzt, die innerhalb der CLR zur Laufzeit von einem JIT-Compiler in die jeweilige Maschinensprache übersetzt wird.
- Bei der Installation von Software, einschließlich des Betriebssystems, liegt das Programm häufig bereits als Maschinencode für die jeweilige Plattform vor; eine eigene Kompilierung durch den Nutzer entfällt.
Ein Disassembler kann Maschinencode in ein Assemblerformat zurückübersetzen. Die Rückübersetzung in eine höhere Programmiersprache durch einen Decompiler ist dagegen stark eingeschränkt.
Abgrenzung zur Assemblersprache
Maschinencode besteht aus einer Folge von Bytes, die Befehle und Daten darstellen. Er ist für Menschen schwer lesbar. In der Assemblersprache werden Maschinenbefehle durch verständlichere Abkürzungen, sogenannte Mnemonics, ersetzt. Operationscodes, Quell- und Zielfelder sowie weitere Angaben können mit symbolischen Bezeichnern wie MOVE, PLZ oder LAENGE und gegebenenfalls mit Zahlenwerten für Längen oder Registernummern notiert werden.
Ein Assemblerprogramm liegt meist als Textdatei vor, das Maschinenprogramm dagegen in der Regel als Binärdatei. Ein Assemblerbefehl entspricht normalerweise genau einem Maschinenbefehl; bei Makroassemblern kann eine Anweisung allerdings mehrere Maschinenbefehle erzeugen. Die textuelle Programmierung mit anschließender Übersetzung ist deshalb deutlich einfacher und schneller als das direkte Codieren im Maschinencode.
Assembler erlauben unterschiedliche Darstellungen von Zeichen und Zahlen, darunter Text, dezimal, hexadezimal, oktal und binär. Im EBCDIC-Code bedeuten die Angaben „A“, „X'C1'“ und „B'11000001'“ dasselbe. Im Maschinencode steht dafür „X'C1'“; bei dualen Operationen entspricht dies dem Wert +193, bei Zeichenoperationen dem Zeichen „A“.
Datenfelder können im Assembler benannt, in verschiedenen Formaten deklariert und mit Konstanten vorbelegt werden. Der erzeugte Maschinencode reserviert den benötigten Speicherplatz, ersetzt symbolische Adressen durch numerische Adressen und setzt die Feldlängen ein. Auch Befehle und Speicherorte können symbolisch bezeichnet werden. Dadurch muss der Programmierer die numerischen Adressen nicht selbst kennen. Bei Änderungen am Programm werden die tatsächlichen Adressen vom Assembler oder Linker angepasst. Das ermöglicht auch den Aufruf von Unterprogrammen, deren endgültige Adresse erst später eingesetzt wird.
Ein Assemblerprogramm behandelt normalerweise eine definierte Aufgabe und kann durch Linken mit den Ergebnissen weiterer Assemblierungen, beispielsweise Objektmodulen, zu einem Maschinenprogramm verbunden werden. Kommentare und zusätzliche Dokumentation bleiben im Maschinenprogramm in der Regel nicht erhalten. Die meisten dieser Vorteile gelten in ähnlicher Weise auch für höhere Programmiersprachen, die gegenüber der Assemblersprache zusätzliche Leistungsmerkmale besitzen.
Beispiel einer Übersetzung aus C
Der folgende C-Code berechnet die Summe von 2 und 3 und gibt das Ergebnis zurück:
int main() { int a = 2; int b = 3; int c = a + b; return c; }
Eine mögliche Übersetzung für einen x86-Prozessor lautet auszugsweise:
- „55; 48 89 E5; 48 83 EC 12“ entspricht „push rbp; mov rbp, rsp; sub rsp, 12“. Dabei wird das Register RBP auf dem Stack gesichert, auf den Wert des Stackpointers RSP gesetzt und Speicherplatz für die Variablen vorbereitet.
- „C7 45 FC 02“ entspricht „mov DWORD PTR [rbp-4], 2“ und setzt die Variable a auf 2.
- „C7 45 F8 03“ entspricht „mov DWORD PTR [rbp-8], 3“ und setzt b auf 3.
- „8B 45 F8; 8B 55 FC; 01 D0; 89 45 F4“ lädt b in EAX und a in EDX, addiert EDX zu EAX und speichert das Ergebnis als c.
- „8B 45 F4“ lädt c in EAX. In einem optimierten Programm könnte diese Anweisung entfallen, weil EAX den Wert bereits enthält.
- „48 83 E5; 5D; C3“ entspricht dem Wiederherstellen des Stackzustands und „ret“. Danach wird an die Aufrufstelle von main zurückgesprungen; EAX enthält den Rückgabewert.
Ein Compiler kann daraus zusammen mit weiteren Informationen eine ausführbare Datei erzeugen. Der Lader des Betriebssystems lädt den Maschinencode in den Arbeitsspeicher. Anschließend ruft die Laufzeitumgebung main() auf, und die CPU arbeitet die Maschinenbefehle ab.
IBM-Rechner und OS/390
Bei IBM-Rechnern mit OS/390 entsteht der Maschinencode beim Assemblieren oder Kompilieren. Der Linkage Editor stellt ihn, gegebenenfalls zusammen mit weiteren Modulen, als ausführbares Programm in einer Programmbibliothek bereit. Zur Ausführung wird das Programm in den Hauptspeicher geladen. Befehle und Daten können dabei gemischt gespeichert werden, wie es bei der Von-Neumann-Architektur möglich ist; dies unterscheidet sie beispielsweise von der Harvard-Architektur.
Der Wert „12“ kann im Speicher hexadezimal und in minimaler Länge unterschiedlich dargestellt werden:
- „F1F2“: Text oder ungepackte Zahl.
- „012C“: gepackt positiv; je Zahl wird ein Halbbyte gespeichert, am Ende steht ein Vorzeichen-Halbbyte.
- „012D“: gepackt negativ.
- „0C“: binär positiv, entsprechend B'00001100'.
Längere Felder können führende Nullen oder bei Text nachfolgende Leerstellen enthalten. Für jedes Datenfeld ist eine Adresse festgelegt, an der es entsprechend seiner Länge und seines Formats beginnt.
Ein Befehlsbeispiel ist „C5.1C.92A4.8C2B“: C5 ist der Opcode für CLC (Compare logical character), also einen Zeichenvergleich. 1C gibt die Länge minus 1 an; bei 00 würde ein Byte verglichen, hier sind es 29 Bytes. 92A4 und 8C2B bezeichnen die beiden Operanden über Basisregister und Distanz. In Assemblersprache kann dies als „CLC FELDA(29),FELDB“ erscheinen.
„47.80.B654“ ist ein bedingter Sprungbefehl BC (Branch on Condition). Die Bedingung 8 bedeutet hier „gleich“ und entspricht dem Mnemonic BE (Branch on Equal). B bezeichnet das Basisregister, 654 die Distanz. Enthält B den Wert 6C4410, wird zu Adresse 6C4A64 verzweigt. In einer Hochsprache könnte derselbe Ablauf als „IF Feld_A = Feld_B then GOTO XXX“ formuliert werden. Bei erfüllter Bedingung wird zu XXX gesprungen, andernfalls mit dem folgenden Code fortgefahren.
Die Befehle können unterschiedlich lang sein. Das Steuerwerk erkennt ihre Länge an den ersten beiden Bits des Befehlscodes und stellt das Befehlszählregister entsprechend weiter. Adressen werden durch ein oder zwei Registerangaben und gegebenenfalls eine Distanz dargestellt. Beim Programmstart lädt das Betriebssystem ein Register mit der Ladeadresse des Programms. Von dort aus werden Basisregister verwendet, damit relative Adressen die tatsächlichen Speicherstellen ansprechen.
Systemfunktionen wie Ein- und Ausgabe, Datum- und Uhrzeitabfragen, Tastatureingaben oder das Laden von Unterprogrammen werden über den Befehl SVC (Supervisor Call) angefordert. Im zweiten Byte steht die Funktion; weitere Parameter werden über eine festgelegte Datenschnittstelle übergeben. „X'05 08'“ steht beispielsweise für LOAD. Die entsprechenden Betriebssystembefehle führen die Funktion aus und kehren danach zum Befehl hinter dem SVC zurück.
Typische Befehlsarten und Prozessormerkmale
Maschinenbefehle greifen häufig über festgelegte Register auf Speicherpositionen und Operanden zu. Ergebnisse und Zusatzinformationen gibt der Prozessor über Register oder Flags im Statusregister zurück. Flags sind Statusbits, die beispielsweise anzeigen, ob ein Wertebereich überschritten wurde. Befehlslängen sowie die Größe von Quell- und Zieloperanden hängen von der Architektur ab. Der Additionsbefehl ADC (add with carry) kann etwa durch Carry- und Overflow-Flags ein Überschreiten des gültigen Wertebereichs signalisieren.
Der Befehlsvorrat und die Verfügbarkeit einzelner Befehle unterscheiden sich je nach Prozessortyp und Prozessorgeneration. SHL und SHR verschieben einen Registerwert nach links oder rechts und waren bereits beim 8086 vorhanden. SHLD und SHRD füllen die entstehenden Leerstellen zusätzlich aus einem anderen Integerwert auf und wurden erst ab dem 80386 implementiert.
Befehle können unterschiedlich mächtig sein. CMP vergleicht zwei Werte hinsichtlich <, > oder =, XCHG vertauscht zwei Operanden, und CMPXCHG verbindet Vergleich und bedingten Austausch. BT prüft ein einzelnes Bit; BTC invertiert es, BTR löscht es und BTS setzt es abhängig vom Ergebnis der Prüfung.
Grundlegende Kategorien sind:
- Arithmetische Operationen: ADD, ADC, SUB, SBB, DIV, MUL, INC und DEC.
- Logische Operationen: AND, OR, XOR und NOT verknüpfen Bitfelder.
- Bit-orientierte Operationen: BSF und BSR lesen einzelne Bits aus; SHL, SHR, RCL, RCR, ROL und ROR verschieben sie; BT und BTC beziehungsweise BTR manipulieren sie.
- Speicheroperationen: MOV, MOVSX, MOVZX und XCHG übertragen Daten zwischen Registern und Speicher; BSWAP verändert die Byte-Reihenfolge innerhalb eines Registers.
- Vergleichsoperationen: CMP und TEST vergleichen Werte.
- Kombinierte Befehle: XADD und CMPXCHG verbinden Vergleich, Arithmetik und Datenaustausch.
- Steueroperationen: Verzweigungen beeinflussen den Programmablauf.
- Datenkonvertierung: Befehle wie CBW, CVTLB und CVTSD2SI wandeln Darstellungen um, gegebenenfalls mit Informationsverlust.
RISC-Prozessoren (Reduced instruction set computer) besitzen einen weniger mächtigen Befehlssatz; einzelne Befehle können typischerweise in einem Taktzyklus ausgeführt werden. CISC-Prozessoren (Complex instruction set computer), zu denen heute fast ausschließlich x86-kompatible CPUs zählen, dekodieren komplexe Befehle intern zur schnelleren Ausführung in eine RISC-ähnliche Mikrocontrollersprache. Jeder Befehl benötigt eine in Datenblättern angegebene Anzahl von Taktzyklen. In extrem zeitkritischen Anwendungen kann dieses Wissen dazu dienen, einen langsamen Befehl durch mehrere insgesamt effizientere Befehle zu ersetzen. Besonders bei CISC-Prozessoren sind Befehle zumindest teilweise durch interne Mikroprogramme realisiert.