Assemblersprache

Aus besserwiki.de
Assemblersprache
Motorola 6800 Assembly Language.png
Typische sekundäre Ausgabe eines Assemblers - zeigt die ursprüngliche Assemblersprache (rechts) für den Motorola MC6800 und die assemblierte Form
ParadigmaImperativ, unstrukturiert
Erstes Erscheinen1949; vor 74 Jahren
TypisierungsdisziplinKeine

In der Computerprogrammierung ist Assemblersprache (oder Assemblersprache oder symbolischer Maschinencode) jede Programmiersprache auf niedriger Ebene mit einer sehr starken Übereinstimmung zwischen den Anweisungen in der Sprache und den Maschinencodeanweisungen der Architektur. Assemblersprache hat in der Regel eine Anweisung pro Maschinenbefehl (1:1), aber auch Konstanten, Kommentare, Assembler-Direktiven, symbolische Bezeichnungen von z.B. Speicherplätzen, Registern und Makros werden im Allgemeinen unterstützt.

Assembler-Code wird von einem Hilfsprogramm, dem so genannten Assembler, in ausführbaren Maschinencode umgewandelt. Der Begriff "Assembler" wird im Allgemeinen Wilkes, Wheeler und Gill zugeschrieben, die in ihrem 1951 erschienenen Buch The Preparation of Programs for an Electronic Digital Computer (Die Erstellung von Programmen für einen elektronischen Digitalcomputer) den Begriff als "ein Programm, das ein anderes, aus mehreren Abschnitten bestehendes Programm zu einem einzigen Programm zusammensetzt" bezeichneten. Der Konvertierungsprozess wird als Assembler bezeichnet, so wie das Assemblieren des Quellcodes. Der Rechenschritt, den ein Assembler bei der Verarbeitung eines Programms durchführt, wird Assemblierungszeit genannt.

Da die Assemblierung von den Maschinencode-Anweisungen abhängt, ist jede Assemblersprache spezifisch für eine bestimmte Computerarchitektur.

Manchmal gibt es mehr als einen Assembler für dieselbe Architektur, und manchmal ist ein Assembler spezifisch für ein Betriebssystem oder für bestimmte Betriebssysteme. Die meisten Assemblersprachen bieten keine spezielle Syntax für Betriebssystemaufrufe, und die meisten Assemblersprachen können universell mit jedem Betriebssystem verwendet werden, da die Sprache Zugang zu allen echten Fähigkeiten des Prozessors bietet, auf denen letztlich alle Systemaufrufmechanismen beruhen. Im Gegensatz zu Assembler-Sprachen sind die meisten höheren Programmiersprachen in der Regel über mehrere Architekturen hinweg portabel, müssen aber interpretiert oder kompiliert werden, was sehr viel komplizierter ist als das Assemblieren.

In den ersten Jahrzehnten der Computertechnik war es üblich, dass sowohl die Systemprogrammierung als auch die Anwendungsprogrammierung vollständig in Assemblersprache stattfand. Obwohl sie für manche Zwecke immer noch unersetzlich ist, wird der Großteil der Programmierung heute in höheren interpretierten und kompilierten Sprachen durchgeführt. In "No Silver Bullet" fasst Fred Brooks die Auswirkungen der Abkehr von der Assembler-Programmierung zusammen: "Der stärkste Impuls für die Produktivität, Zuverlässigkeit und Einfachheit von Software war sicherlich die zunehmende Verwendung von Hochsprachen für die Programmierung. Die meisten Beobachter schreiben dieser Entwicklung mindestens eine Verfünffachung der Produktivität und einen gleichzeitigen Gewinn an Zuverlässigkeit, Einfachheit und Verständlichkeit zu."

Heute ist es typisch, kleine Mengen Assembler-Code innerhalb größerer Systeme zu verwenden, die in einer höheren Sprache implementiert sind, sei es aus Leistungsgründen oder um direkt mit der Hardware auf eine Weise zu interagieren, die von der höheren Sprache nicht unterstützt wird. So sind zum Beispiel nur knapp 2 % des Quellcodes von Version 4.9 des Linux-Kernels in Assembler geschrieben; mehr als 97 % sind in C geschrieben.

Eine Assemblersprache, kurz auch Assembler genannt (von englisch to assemble ‚zusammenfügen‘), ist eine Programmiersprache, die auf den Befehlsvorrat eines bestimmten Computertyps (d. h. dessen Prozessorarchitektur) ausgerichtet ist.

Assemblersprachen bezeichnet man deshalb als maschinenorientierte Programmiersprachen und – als Nachfolger der direkten Programmierung mit Zahlencodes – als Programmiersprachen der zweiten Generation: Anstelle eines Binärcodes der Maschinensprache können Befehle und deren Operanden durch leichter verständliche mnemonische Symbole in Textform (z. B. „MOVE“), Operanden z. T. als symbolische Adresse (z. B. „PLZ“), notiert und dargestellt werden.

Der Quelltext eines Assemblerprogramms wird mit Hilfe einer Übersetzungssoftware (Assembler oder Assemblierer) in Maschinencode übersetzt. Dagegen übersetzt in höheren Programmiersprachen (Hochsprachen, dritte Generation) ein sogenannter Compiler abstraktere (komplexere, nicht auf den Prozessor-Befehlssatz begrenzte) Befehle in den Maschinencode der gegebenen Zielarchitektur – oder in eine Zwischensprache.

Umgangssprachlich werden die Ausdrücke „Maschinensprache“ und „Assembler(sprache)“ häufig synonym verwendet.

Assembler-Syntax

Die Assemblersprache verwendet eine Mnemonik, um z. B. jeden Low-Level-Maschinenbefehl oder Opcode, jede Direktive, typischerweise auch jedes Architekturregister, Flag usw. darzustellen. Einige der Mnemonics können eingebaut sein, andere sind benutzerdefiniert. Viele Operationen erfordern einen oder mehrere Operanden, um eine vollständige Anweisung zu bilden. Die meisten Assembler erlauben benannte Konstanten, Register und Labels für Programm- und Speicherplätze und können Ausdrücke für Operanden berechnen. Auf diese Weise werden Programmierer von langwierigen, sich wiederholenden Berechnungen befreit, und Assemblerprogramme sind viel lesbarer als Maschinencode. Je nach Architektur können diese Elemente auch für bestimmte Anweisungen oder Adressierungsmodi kombiniert werden, indem Offsets oder andere Daten sowie feste Adressen verwendet werden. Viele Assembler bieten zusätzliche Mechanismen zur Erleichterung der Programmentwicklung, zur Steuerung des Assemblierungsprozesses und zur Unterstützung der Fehlersuche.

Einige sind spaltenorientiert, mit bestimmten Feldern in bestimmten Spalten; dies war bei Maschinen, die in den 1950er und frühen 1960er Jahren Lochkarten verwendeten, sehr verbreitet. Einige Assembler haben eine Freiform-Syntax, bei der die Felder durch Begrenzungszeichen getrennt sind, z. B. durch Satzzeichen oder Leerzeichen. Einige Assembler haben eine gemischte Syntax, bei der z. B. Bezeichnungen in einer bestimmten Spalte stehen und andere Felder durch Begrenzungszeichen getrennt sind; dies wurde in den 1960er Jahren üblicher als eine spaltenorientierte Syntax.

IBM System/360

Alle IBM-Assembler für System/360 haben standardmäßig ein Label in Spalte 1, durch Begrenzungszeichen getrennte Felder in den Spalten 2-71, einen Fortsetzungsindikator in Spalte 72 und eine Sequenznummer in den Spalten 73-80. Die Begrenzungszeichen für Label, Opcode, Operanden und Kommentare sind Leerzeichen, während die einzelnen Operanden durch Kommas und Klammern getrennt sind.

Terminologie

  • Ein Makro-Assembler ist ein Assembler, der eine Makrobefehlseinrichtung enthält, so dass (parametrisierter) Assemblertext durch einen Namen dargestellt werden kann, und dieser Name kann verwendet werden, um den erweiterten Text in anderen Code einzufügen.
    • Offener Code bezieht sich auf jede Assembler-Eingabe außerhalb einer Makrodefinition.
  • Ein Cross-Assembler (siehe auch Cross-Compiler) ist ein Assembler, der auf einem Computer oder Betriebssystem (dem Host-System) eines anderen Typs als dem System ausgeführt wird, auf dem der resultierende Code laufen soll (dem Zielsystem). Cross-Assembling erleichtert die Entwicklung von Programmen für Systeme, die nicht über die Ressourcen zur Unterstützung der Softwareentwicklung verfügen, wie z. B. eingebettete Systeme oder Mikrocontroller. In einem solchen Fall muss der resultierende Objektcode über einen Festspeicher (ROM, EPROM usw.), ein Programmiergerät (wenn der Festspeicher in das Gerät integriert ist, wie bei Mikrocontrollern) oder eine Datenverbindung auf das Zielsystem übertragen werden, wobei entweder eine exakte Bit-für-Bit-Kopie des Objektcodes oder eine textbasierte Darstellung dieses Codes (z. B. Intel Hex oder Motorola S-Record) verwendet wird.
  • Ein High-Level-Assembler ist ein Programm, das Sprachabstraktionen bereitstellt, die häufig mit Hochsprachen assoziiert werden, wie z. B. erweiterte Kontrollstrukturen (IF/THEN/ELSE, DO CASE usw.) und abstrakte Hochdatentypen, einschließlich Strukturen/Datensätze, Unions, Klassen und Mengen.
  • Ein Mikroassembler ist ein Programm, das ein Mikroprogramm, die so genannte Firmware, vorbereitet, um den Low-Level-Betrieb eines Computers zu steuern.
  • Ein Meta-Assembler ist "ein Programm, das die syntaktische und semantische Beschreibung einer Assemblersprache annimmt und einen Assembler für diese Sprache generiert" oder das eine Assembler-Quelldatei zusammen mit einer solchen Beschreibung annimmt und die Quelldatei entsprechend dieser Beschreibung assembliert. Die "Meta-Symbol"-Assembler für die Computer der Serien SDS 9 und SDS Sigma sind Meta-Assembler. Sperry Univac stellte auch einen Meta-Assembler für die UNIVAC 1100/2200 Serie zur Verfügung.
  • Inline-Assembler (oder eingebetteter Assembler) ist Assembler-Code, der in einem Hochsprachenprogramm enthalten ist. Dies wird am häufigsten in Systemprogrammen verwendet, die direkten Zugriff auf die Hardware benötigen.

Schlüsselbegriffe

Assembler

Ein Assemblerprogramm erzeugt Objektcode, indem es Kombinationen von Mnemonics und Syntax für Operationen und Adressierungsmodi in ihre numerischen Entsprechungen übersetzt. Diese Darstellung umfasst in der Regel einen Operationscode ("Opcode") sowie weitere Steuerbits und Daten. Der Assembler berechnet auch konstante Ausdrücke und löst symbolische Namen für Speicherplätze und andere Einheiten auf. Die Verwendung symbolischer Verweise ist ein Hauptmerkmal von Assemblern, das mühsame Berechnungen und manuelle Adressaktualisierungen nach Programmänderungen erspart. Die meisten Assembler verfügen auch über Makrofunktionen zur Durchführung von Textsubstitutionen - z. B. zur Generierung häufiger kurzer Befehlsfolgen als Inline-Anweisungen anstelle von aufgerufenen Unterprogrammen.

Einige Assembler sind auch in der Lage, einige einfache Arten von anweisungssatzspezifischen Optimierungen durchzuführen. Ein konkretes Beispiel hierfür sind die allgegenwärtigen x86-Assembler verschiedener Hersteller. Die meisten dieser Assembler sind in der Lage, Sprungbefehle (lange Sprünge durch kurze oder relative Sprünge) in einer beliebigen Anzahl von Durchläufen zu ersetzen, wenn dies gewünscht wird. Andere können sogar einfache Umordnungen oder Einfügungen von Befehlen vornehmen, wie z. B. einige Assembler für RISC-Architekturen, die bei der Optimierung einer sinnvollen Befehlsplanung helfen können, um die CPU-Pipeline so effizient wie möglich zu nutzen.

Assembler gibt es seit den 1950er Jahren, als erste Stufe über der Maschinensprache und vor den höheren Programmiersprachen wie Fortran, Algol, COBOL und Lisp. Es gab auch mehrere Klassen von Übersetzern und halbautomatischen Codegeneratoren mit ähnlichen Eigenschaften wie Assembler und Hochsprachen, wobei Speedcode vielleicht eines der bekanntesten Beispiele ist.

Es kann mehrere Assembler mit unterschiedlicher Syntax für eine bestimmte CPU oder Befehlssatzarchitektur geben. So könnte beispielsweise ein Befehl zum Hinzufügen von Speicherdaten zu einem Register in einem Prozessor der x86-Familie in der ursprünglichen Intel-Syntax add eax,[ebx] lauten, während dies in der vom GNU Assembler verwendeten AT&T-Syntax addl (%ebx),%eax geschrieben würde. Trotz des unterschiedlichen Aussehens erzeugen die verschiedenen syntaktischen Formen im Allgemeinen den gleichen numerischen Maschinencode. Ein und derselbe Assembler kann auch verschiedene Modi haben, um verschiedene syntaktische Formen sowie deren genaue semantische Interpretationen zu unterstützen (z. B. FASM-Syntax, TASM-Syntax, idealer Modus usw., im speziellen Fall der x86-Assembler-Programmierung).

Anzahl der Durchläufe

Es gibt zwei Arten von Assemblern, die sich danach richten, wie viele Durchläufe durch den Quellcode erforderlich sind (wie oft der Assembler den Quellcode liest), um die Objektdatei zu erzeugen.

  • One-Pass-Assembler lesen den Quellcode einmal durch. Jedes Symbol, das verwendet wird, bevor es definiert ist, erfordert "Errata" am Ende des Objektcodes (oder zumindest nicht früher als an dem Punkt, an dem das Symbol definiert ist), die dem Linker oder Lader sagen, dass er "zurückgehen" und einen Platzhalter überschreiben soll, der dort hinterlassen wurde, wo das noch nicht definierte Symbol verwendet wurde.
  • Multi-Pass-Assembler erstellen in den ersten Durchläufen eine Tabelle mit allen Symbolen und ihren Werten und verwenden die Tabelle dann in späteren Durchläufen zur Codegenerierung.

In beiden Fällen muss der Assembler in der Lage sein, die Größe jeder Anweisung in den ersten Durchläufen zu bestimmen, um die Adressen der nachfolgenden Symbole zu berechnen. Das bedeutet, dass der Assembler, wenn die Größe einer Operation, die sich auf einen später definierten Operanden bezieht, von der Art oder dem Abstand des Operanden abhängt, eine pessimistische Schätzung vornimmt, wenn er zum ersten Mal auf die Operation stößt, und sie erforderlichenfalls mit einer oder mehreren "Keine-Operation"-Anweisungen in einem späteren Durchgang oder den Errata auffüllen. In einem Assembler mit Peephole-Optimierung können die Adressen zwischen den Durchläufen neu berechnet werden, um den pessimistischen Code durch einen Code zu ersetzen, der auf die genaue Entfernung zum Ziel zugeschnitten ist.

Der ursprüngliche Grund für die Verwendung von Assemblern mit nur einem Durchgang war die Speichergröße und die Geschwindigkeit des Assemblierens - oft würde ein zweiter Durchgang das Speichern der Symboltabelle im Speicher (um Vorwärtsreferenzen zu behandeln), das Zurückspulen und erneute Einlesen des Programmquelltextes auf Band oder das erneute Einlesen eines Kartenspiels oder Lochstreifens erfordern. Spätere Computer mit viel größerem Speicher (insbesondere Plattenspeicher) hatten den Platz, um alle notwendigen Verarbeitungen ohne ein solches erneutes Einlesen durchzuführen. Der Vorteil des Multi-Pass-Assemblers besteht darin, dass durch das Fehlen von Errata der Linking-Prozess (oder das Laden des Programms, wenn der Assembler direkt ausführbaren Code erzeugt) schneller geht.

Beispiel: Im folgenden Codeschnipsel wäre ein One-Pass-Assembler in der Lage, die Adresse der Rückwärtsreferenz zu ermitteln BKWD beim Assemblieren der Anweisung S2zu assemblieren, wäre aber nicht in der Lage, die Adresse der Vorwärtsreferenz FWD bei der Assemblierung der Verzweigungsanweisung S1; in der Tat, FWD möglicherweise undefiniert sein. Ein Assembler mit zwei Durchgängen würde beide Adressen in Durchgang 1 ermitteln, so dass sie bei der Codegenerierung in Durchgang 2 bekannt wären.

S1   B    FWD
  ...
FWD   EQU *
  ...
BKWD  EQU *
  ...
S2    B   BKWD 

High-Level-Assembler

Anspruchsvollere High-Level-Assembler bieten Sprachabstraktionen wie:

  • Hochrangige Prozedur-/Funktionsdeklarationen und -aufrufe
  • Erweiterte Kontrollstrukturen (IF/THEN/ELSE, SWITCH)
  • Abstrakte Datentypen auf hohem Niveau, einschließlich Strukturen/Datensätze, Unions, Klassen und Mengen
  • Hochentwickelte Makro-Verarbeitung (obwohl sie auf gewöhnlichen Assemblern seit den späten 1950er Jahren verfügbar ist, z.B. für die IBM 700er und IBM 7000er Serie, und seit den 1960er Jahren für IBM System/360 (S/360), neben anderen Maschinen)
  • Objektorientierte Programmiermerkmale wie Klassen, Objekte, Abstraktion, Polymorphismus und Vererbung

Siehe Sprachdesign unten für weitere Details.

Assemblersprache

Ein in Assemblersprache geschriebenes Programm besteht aus einer Reihe von mnemonischen Prozessoranweisungen und Meta-Anweisungen (auch bekannt als deklarative Operationen, Direktiven, Pseudo-Anweisungen, Pseudo-Operationen und Pseudo-Ops), Kommentaren und Daten. Assembler-Befehle bestehen in der Regel aus einem Opcode-Mnemonik, gefolgt von einem Operanden, der eine Liste von Daten, Argumenten oder Parametern sein kann. Einige Befehle können "implizit" sein, was bedeutet, dass die Daten, mit denen der Befehl operiert, implizit durch den Befehl selbst definiert sind - ein solcher Befehl benötigt keinen Operanden. Die resultierende Anweisung wird von einem Assembler in Maschinensprachbefehle übersetzt, die in den Speicher geladen und ausgeführt werden können.

Der nachstehende Befehl weist beispielsweise einen x86/IA-32-Prozessor an, einen unmittelbaren 8-Bit-Wert in ein Register zu verschieben. Der Binärcode für diesen Befehl ist 10110, gefolgt von einer 3-Bit-Kennung für das zu verwendende Register. Die Kennung für das AL-Register ist 000, so dass der folgende Maschinencode das AL-Register mit den Daten 01100001 lädt.

10110000 01100001

Dieser binäre Computercode kann in hexadezimaler Form wie folgt ausgedrückt werden, um ihn für den Menschen besser lesbar zu machen.

B0 61

Hier bedeutet B0 "Verschiebe eine Kopie des folgenden Wertes in AL, und 61 ist eine hexadezimale Darstellung des Wertes 01100001, der dezimal 97 ist. Die Assemblersprache für die 8086-Familie bietet die Abkürzung MOV (move) für Befehle wie diesen, so dass der obige Maschinencode in Assemblersprache wie folgt geschrieben werden kann, mit einem erklärenden Kommentar, falls erforderlich, nach dem Semikolon. Dies ist viel einfacher zu lesen und zu merken.

MOV AL, 61h ; AL mit 97 dezimal (61 hex) laden <span title="Aus: Englische Wikipedia, Abschnitt &quot;Assembly language&quot;" class="plainlinks">[https://en.wikipedia.org/wiki/Assembly_language#Assembly_language <span style="color:#dddddd">ⓘ</span>]</span>

In einigen Assemblersprachen (einschließlich dieser hier) kann dieselbe Abkürzung, wie z.B. MOV, für eine Familie verwandter Befehle zum Laden, Kopieren und Verschieben von Daten verwendet werden, unabhängig davon, ob es sich um unmittelbare Werte, Werte in Registern oder Speicherplätze handelt, auf die Werte in Registern oder unmittelbare (d.h. direkte) Adressen zeigen. Andere Assembler verwenden möglicherweise separate Opcode-Mnemoniks wie L für "move memory to register", ST für "move register to memory", LR für "move register to register", MVI für "move immediate operand to memory" usw.

Wenn dieselbe Mnemonik für verschiedene Befehle verwendet wird, bedeutet dies, dass die Mnemonik je nach den Operanden, die auf die Mnemonik folgen, mehreren verschiedenen binären Befehlscodes entspricht, ohne Daten (z. B. 61h in diesem Beispiel). Bei den x86/IA-32-CPUs steht die Intel-Assembler-Syntax MOV AL, AH beispielsweise für einen Befehl, der den Inhalt des Registers AH in das Register AL verschiebt. Die hexadezimale Form dieses Befehls lautet:

88 E0

Das erste Byte, 88h, kennzeichnet eine Verschiebung zwischen einem bytegroßen Register und entweder einem anderen Register oder dem Speicher, und das zweite Byte, E0h, ist kodiert (mit drei Bit-Feldern), um anzugeben, dass beide Operanden Register sind, die Quelle AH und das Ziel AL ist.

In einem Fall wie diesem, in dem dieselbe Mnemonik für mehr als einen Binärbefehl stehen kann, bestimmt der Assembler anhand der Operanden, welcher Befehl erzeugt werden soll. Im ersten Beispiel ist der Operand 61h eine gültige hexadezimale numerische Konstante und kein gültiger Registername, so dass nur der Befehl B0 in Frage kommt. Im zweiten Beispiel ist der Operand AH ein gültiger Registername und keine gültige numerische Konstante (hexadezimal, dezimal, oktal oder binär), so dass nur die Anweisung 88 in Frage kommt.

Assembler-Sprachen sind immer so konzipiert, dass diese Art von Eindeutigkeit durch ihre Syntax universell erzwungen wird. In der x86-Assemblersprache von Intel beispielsweise muss eine hexadezimale Konstante mit einer Ziffer beginnen, so dass die hexadezimale Zahl "A" (entspricht der Dezimalzahl 10) als 0Ah oder 0AH und nicht als AH geschrieben wird, damit sie nicht als Name des Registers AH erscheint. (Dieselbe Regel verhindert auch Mehrdeutigkeit bei den Namen der Register BH, CH und DH sowie bei jedem benutzerdefinierten Symbol, das mit dem Buchstaben H endet und ansonsten nur Zeichen enthält, die hexadezimale Ziffern sind, wie etwa das Wort "BEACH").

Um auf das ursprüngliche Beispiel zurückzukommen: Während der x86-Opcode 10110000 (B0) einen 8-Bit-Wert in das AL-Register kopiert, verschiebt 10110001 (B1) ihn in CL und 10110010 (B2) tut dies in DL. Es folgen Assembler-Beispiele für diese Codes.

MOV AL, 1h ; Lädt AL mit dem unmittelbaren Wert 1
MOV CL, 2h ; Lädt CL mit dem unmittelbaren Wert 2
MOV DL, 3h ; DL mit unmittelbarem Wert 3 laden

Die Syntax von MOV kann auch komplexer sein, wie die folgenden Beispiele zeigen.

MOV EAX, [EBX] ; Verschiebe die 4 Bytes im Speicher an der in EBX enthaltenen Adresse nach EAX
MOV [ESI+EAX], CL ; Verschiebt den Inhalt von CL in das Byte an der Adresse ESI+EAX
MOV DS, DX ; Verschiebt den Inhalt von DX in das Segmentregister DS

In jedem Fall wird die MOV-Mnemonik von einem Assembler direkt in einen der Opcodes 88-8C, 8E, A0-A3, B0-BF, C6 oder C7 übersetzt, und der Programmierer muss normalerweise nicht wissen oder sich merken, welcher es ist.

Die Umwandlung von Assemblersprache in Maschinencode ist die Aufgabe eines Assemblers, und die umgekehrte Aufgabe kann zumindest teilweise von einem Disassembler übernommen werden. Im Gegensatz zu Hochsprachen gibt es eine Eins-zu-eins-Entsprechung zwischen vielen einfachen Assembler-Anweisungen und Maschinensprache-Anweisungen. In einigen Fällen kann ein Assembler jedoch Pseudoanweisungen (im Wesentlichen Makros) bereitstellen, die sich zu mehreren Maschinensprachanweisungen erweitern lassen, um häufig benötigte Funktionen bereitzustellen. Zum Beispiel kann ein Assembler für eine Maschine, die keine "Verzweigung, wenn größer oder gleich"-Anweisung hat, eine Pseudoanweisung bereitstellen, die zu den Maschinensprache-Anweisungen "Setzen, wenn kleiner als" und "Verzweigung, wenn Null (auf das Ergebnis der Set-Anweisung)" erweitert wird. Die meisten Assembler mit vollem Funktionsumfang bieten auch eine reichhaltige Makrosprache (siehe unten), die von Anbietern und Programmierern verwendet wird, um komplexere Code- und Datensequenzen zu erzeugen. Da die Informationen über Pseudoanweisungen und Makros, die in der Assemblerumgebung definiert sind, im Objektprogramm nicht vorhanden sind, kann ein Disassembler die Makro- und Pseudoanweisungsaufrufe nicht rekonstruieren, sondern nur die tatsächlichen Maschinenanweisungen disassemblieren, die der Assembler aus diesen abstrakten Assemblerspracheinheiten erzeugt hat. Da Kommentare in der Assembler-Quelldatei vom Assembler ignoriert werden und keine Auswirkungen auf den von ihm erzeugten Objektcode haben, ist ein Disassembler auch nicht in der Lage, Quellkommentare wiederherzustellen.

Jede Computerarchitektur hat ihre eigene Maschinensprache. Computer unterscheiden sich in der Anzahl und Art der von ihnen unterstützten Operationen, in der unterschiedlichen Größe und Anzahl der Register und in der Darstellung der Daten im Speicher. Während die meisten Allzweckcomputer im Wesentlichen die gleichen Funktionen ausführen können, unterscheiden sie sich in der Art und Weise, wie sie dies tun; die entsprechenden Assemblersprachen spiegeln diese Unterschiede wider.

Für einen einzigen Befehlssatz können mehrere Sätze von Mnemonics oder Assembler-Syntaxen existieren, die typischerweise in verschiedenen Assembler-Programmen instanziiert werden. In diesen Fällen ist die am weitesten verbreitete Syntax diejenige, die vom CPU-Hersteller bereitgestellt und in seiner Dokumentation verwendet wird.

Zwei Beispiele für CPUs, die zwei verschiedene Sätze von Mnemonics haben, sind die Intel 8080 Familie und die Intel 8086/8088. Da Intel das Urheberrecht auf seine Assembler-Mnemonics beanspruchte (zumindest auf jeder Seite der in den 1970er und frühen 1980er Jahren veröffentlichten Dokumentation), erfanden einige Unternehmen, die unabhängig voneinander CPUs produzierten, die mit den Intel-Befehlssätzen kompatibel waren, ihre eigenen Mnemonics. Die Zilog Z80-CPU, eine Weiterentwicklung des Intel 8080A, unterstützt alle 8080A-Befehle und viele weitere; Zilog erfand eine völlig neue Assemblersprache, nicht nur für die neuen Befehle, sondern auch für alle 8080A-Befehle. Während Intel beispielsweise die Mnemonik MOV, MVI, LDA, STA, LXI, LDAX, STAX, LHLD und SHLD für verschiedene Datenübertragungsbefehle verwendet, wird in der Z80-Assemblersprache die Mnemonik LD für alle diese Befehle verwendet. Ein ähnlicher Fall sind die NEC V20- und V30-CPUs, verbesserte Kopien des Intel 8086 bzw. 8088. Wie Zilog bei der Z80 erfand auch NEC neue Mnemonics für alle 8086- und 8088-Befehle, um Anschuldigungen wegen Verletzung des Urheberrechts von Intel zu vermeiden. (Es ist fraglich, ob solche Urheberrechte gültig sein können, und spätere CPU-Firmen wie AMD und Cyrix haben Intels x86/IA-32-Befehlsmnemonics exakt wiederveröffentlicht, ohne dafür eine Erlaubnis zu haben oder rechtlich belangt zu werden.) Es ist zweifelhaft, ob in der Praxis viele Leute, die die V20 und V30 programmiert haben, tatsächlich in der Assemblersprache von NEC und nicht in der von Intel geschrieben haben; da zwei Assemblersprachen für dieselbe Befehlssatzarchitektur isomorph sind (ähnlich wie Englisch und Schweinelatein), gibt es keine Verpflichtung, die von einem Hersteller veröffentlichte Assemblersprache für seine Produkte zu verwenden.

Programmbefehle in Maschinensprache bilden sich aus dem Operationscode (Opcode) und meist weiteren, je nach Befehl individuell festgelegten Angaben wie Adressen, im Befehl eingebettete Literale, Längenangaben etc. Da die Zahlenwerte der Opcodes schwierig zu merken sind, verwenden Assemblersprachen leichter merkbare Kürzel, sogenannte mnemonische Symbole (kurz Mnemonics).

Beispiel: Der folgende Befehl in der Maschinensprache von x86-Prozessoren

Mit Computerhilfe kann man das eine in das andere weitgehend eins zu eins übersetzen. Jedoch werden Adressumformungen vorgenommen, so dass man symbolische Adressen benutzen kann. Die Eingabedaten für einen Assembler enthalten neben den eigentlichen Codes/Befehlen (die er in Maschinencode übersetzt) auch Steueranweisungen, die seine Arbeitsweise bestimmen/festlegen, zum Beispiel zur Definition eines Basisregisters.

Häufig werden komplexere Assemblersprachen (Makroassembler) verwendet, um die Programmierarbeit zu erleichtern. Makros sind dabei im Quelltext enthaltene Aufrufe, die vor dem eigentlichen Assemblieren automatisch durch (meist kurze) Folgen von Assemblerbefehlen ersetzt werden. Dabei können einfache, durch Parameter steuerbare Ersetzungen vorgenommen werden. Die Disassemblierung von derart generiertem Code ergibt allerdings den reinen Assemblercode ohne die beim Übersetzen expandierten Makros.

Entwurf der Sprache

Grundlegende Elemente

Es gibt große Unterschiede in der Art und Weise, wie die Autoren von Assemblern Anweisungen kategorisieren und in der Nomenklatur, die sie verwenden. Insbesondere bezeichnen einige alles, was nicht ein Maschinenmnemonikum oder ein erweitertes Mnemonikum ist, als Pseudo-Operation (Pseudo-op). Eine typische Assemblersprache besteht aus 3 Arten von Befehlsanweisungen, die zur Definition von Programmoperationen verwendet werden:

  • Opcode-Mnemonics
  • Daten-Definitionen
  • Assembler-Anweisungen

Opcode-Mnemonics und erweiterte Mnemonics

Anweisungen (Statements) in Assembler sind im Allgemeinen sehr einfach, im Gegensatz zu denen in Hochsprachen. Im Allgemeinen ist eine Mnemonik ein symbolischer Name für eine einzelne ausführbare Maschinensprache-Anweisung (ein Opcode), und es gibt mindestens eine Opcode-Mnemonik für jede Maschinensprache-Anweisung. Jede Anweisung besteht in der Regel aus einer Operation oder einem Opcode und null oder mehr Operanden. Die meisten Befehle beziehen sich auf einen einzelnen Wert oder ein Wertepaar. Bei den Operanden kann es sich um unmittelbare Werte (in der Anweisung selbst kodierte Werte), um in der Anweisung angegebene oder implizierte Register oder um die Adressen von Daten handeln, die sich an anderer Stelle im Speicher befinden. Dies wird durch die zugrunde liegende Prozessorarchitektur bestimmt: der Assembler spiegelt lediglich wider, wie diese Architektur funktioniert. Erweiterte Mnemonics werden oft verwendet, um eine Kombination eines Opcodes mit einem bestimmten Operanden zu spezifizieren, z.B. verwenden die System/360-Assembler B als eine erweiterte Mnemonik für BC mit einer Maske von 15 und NOP ("NO OPeration" - einen Schritt lang nichts tun) für BC mit einer Maske von 0.

Erweiterte Mnemonics werden häufig verwendet, um spezielle Verwendungszwecke von Befehlen zu unterstützen, die oft nicht aus dem Befehlsnamen ersichtlich sind. Viele CPUs verfügen beispielsweise nicht über einen expliziten NOP-Befehl, haben aber Befehle, die für diesen Zweck verwendet werden können. In 8086-CPUs kann die Anweisung xchg ax,ax wird verwendet für nopverwendet, wobei nop ein Pseudo-Opcode ist, um die Anweisung zu kodieren xchg ax,ax. Einige Disassembler erkennen dies und dekodieren den xchg ax,ax Anweisung als nop. In ähnlicher Weise verwenden die IBM-Assembler für System/360 und System/370 die erweiterten Mnemonics NOP und NOPR für BC und BCR mit Nullmasken. Bei der SPARC-Architektur werden diese Befehle als synthetische Befehle bezeichnet.

Einige Assembler unterstützen auch einfache eingebaute Makrobefehle, die zwei oder mehr Maschinenbefehle erzeugen. Bei einigen Z80-Assemblern wird zum Beispiel die Anweisung ld hl,bc erkannt, um Folgendes zu erzeugen ld l,c gefolgt von ld h,b. Diese werden manchmal auch als Pseudo-Opcodes bezeichnet.

Mnemonics sind willkürliche Symbole; 1985 veröffentlichte die IEEE die Norm 694 für einen einheitlichen Satz von Mnemonics, die von allen Assemblern verwendet werden sollen. Der Standard wurde inzwischen zurückgezogen.

Datenanweisungen

Es gibt Anweisungen, die zur Definition von Datenelementen zur Aufnahme von Daten und Variablen verwendet werden. Sie definieren die Art der Daten, die Länge und die Ausrichtung der Daten. Diese Anweisungen können auch festlegen, ob die Daten für externe Programme (separat assemblierte Programme) oder nur für das Programm, in dem der Datenabschnitt definiert ist, verfügbar sind. Einige Assembler bezeichnen diese Befehle als Pseudo-OPs.

Assembler-Anweisungen

Assembler-Direktiven, auch Pseudo-Opcodes, Pseudo-Operationen oder Pseudo-Ops genannt, sind Befehle, die einem Assembler gegeben werden, "um ihn anzuweisen, andere Operationen als Assembler-Befehle durchzuführen". Direktiven beeinflussen die Arbeitsweise des Assemblers und "können den Objektcode, die Symboltabelle, die Listingdatei und die Werte interner Assemblerparameter beeinflussen". Manchmal ist der Begriff Pseudo-Opcode für Direktiven reserviert, die Objektcode erzeugen, wie z.B. solche, die Daten erzeugen.

Die Namen von Pseudo-Ops beginnen oft mit einem Punkt, um sie von Maschinenbefehlen zu unterscheiden. Pseudo-Ops können die Assemblierung des Programms von Parametern abhängig machen, die der Programmierer eingibt, so dass ein Programm auf unterschiedliche Weise assembliert werden kann, beispielsweise für verschiedene Anwendungen. Oder ein Pseudo-Op kann verwendet werden, um die Darstellung eines Programms zu manipulieren, damit es leichter zu lesen und zu warten ist. Eine weitere häufige Verwendung von Pseudo-Ops ist die Reservierung von Speicherbereichen für Laufzeitdaten und die optionale Initialisierung ihres Inhalts auf bekannte Werte.

Symbolische Assembler ermöglichen es dem Programmierer, Speicherplätzen und verschiedenen Konstanten beliebige Namen (Labels oder Symbole) zuzuordnen. In der Regel erhält jede Konstante und Variable einen Namen, so dass die Anweisungen auf diese Speicherplätze namentlich verweisen können, was die Selbstdokumentation des Codes fördert. In ausführbarem Code ist der Name jedes Unterprogramms mit seinem Einstiegspunkt verbunden, so dass jeder Aufruf eines Unterprogramms dessen Namen verwenden kann. Innerhalb von Unterroutinen werden GOTO-Ziele mit Bezeichnungen versehen. Einige Assembler unterstützen lokale Symbole, die sich oft lexikalisch von normalen Symbolen unterscheiden (z.B. die Verwendung von "10$" als GOTO-Ziel).

Einige Assembler, wie z.B. NASM, bieten eine flexible Symbolverwaltung, die es dem Programmierer erlaubt, verschiedene Namensräume zu verwalten, automatisch Offsets innerhalb von Datenstrukturen zu berechnen und Labels zuzuweisen, die sich auf literale Werte oder das Ergebnis einfacher Berechnungen durch den Assembler beziehen. Labels können auch zur Initialisierung von Konstanten und Variablen mit verschiebbaren Adressen verwendet werden.

Assemblersprachen erlauben, wie die meisten anderen Computersprachen auch, das Hinzufügen von Kommentaren zum Programmquellcode, die beim Assemblieren ignoriert werden. Eine umsichtige Kommentierung ist in Assemblerprogrammen unerlässlich, da die Bedeutung und der Zweck einer Folge von binären Maschinenbefehlen schwer zu ermitteln sein kann. Die "rohe" (unkommentierte) Assemblersprache, die von Compilern oder Disassemblern erzeugt wird, ist ziemlich schwierig zu lesen, wenn Änderungen vorgenommen werden müssen.

Makros

Viele Assembler unterstützen vordefinierte Makros, andere wiederum unterstützen vom Programmierer definierte (und immer wieder neu definierbare) Makros, bei denen es sich um Sequenzen von Textzeilen handelt, in die Variablen und Konstanten eingebettet sind. Die Makrodefinition ist meist eine Mischung aus Assembleranweisungen, z.B. Direktiven, symbolischen Maschinenanweisungen und Vorlagen für Assembleranweisungen. Diese Folge von Textzeilen kann Opcodes oder Direktiven enthalten. Sobald ein Makro definiert ist, kann sein Name anstelle eines Mnemonics verwendet werden. Wenn der Assembler eine solche Anweisung verarbeitet, ersetzt er die Anweisung durch die Textzeilen, die mit diesem Makro verbunden sind, und verarbeitet sie dann so, als ob sie in der Quellcodedatei vorhanden wären (einschließlich, in einigen Assemblern, der Erweiterung aller Makros, die im Ersatztext vorhanden sind). Makros in diesem Sinne gehen auf die IBM-Autocoder der 1950er Jahre zurück.

Makro-Assembler haben typischerweise Direktiven, um z.B. Makros zu definieren, Variablen zu definieren, Variablen auf das Ergebnis eines arithmetischen, logischen oder String-Ausdrucks zu setzen, zu iterieren, Code bedingt zu erzeugen. Einige dieser Direktiven können auf die Verwendung innerhalb einer Makrodefinition beschränkt sein, z.B. MEXIT in HLASM, während andere innerhalb von offenem Code (außerhalb von Makrodefinitionen) erlaubt sein können, z.B. AIF und COPY in HLASM.

In der Assemblersprache stellt der Begriff "Makro" ein umfassenderes Konzept dar als in einigen anderen Zusammenhängen, wie z.B. dem Präprozessor in der Programmiersprache C, wo die #define-Direktive typischerweise dazu verwendet wird, kurze einzeilige Makros zu erstellen. Assembler-Makrobefehle können, wie Makros in PL/I und einigen anderen Sprachen, selbst lange "Programme" sein, die durch Interpretation durch den Assembler während der Assemblierung ausgeführt werden.

Da Makros "kurze" Namen haben können, sich aber auf mehrere oder sogar viele Codezeilen ausdehnen, können sie verwendet werden, um Assembler-Programme viel kürzer erscheinen zu lassen und weniger Zeilen Quellcode zu benötigen, wie bei höheren Sprachen. Sie können auch verwendet werden, um Assembler-Programme auf höheren Ebenen zu strukturieren und optional eingebetteten Debugging-Code über Parameter und andere ähnliche Funktionen einzuführen.

Makro-Assembler erlauben es Makros oft, Parameter zu übernehmen. Einige Assembler enthalten recht ausgefeilte Makrosprachen, die solche Hochsprachenelemente wie optionale Parameter, symbolische Variablen, Bedingungen, Stringmanipulation und arithmetische Operationen enthalten, die alle während der Ausführung eines bestimmten Makros verwendet werden können, und die es Makros ermöglichen, Kontext zu speichern oder Informationen auszutauschen. So könnte ein Makro auf der Grundlage der Makroargumente zahlreiche Assembleranweisungen oder Datendefinitionen erzeugen. Auf diese Weise können z. B. rekordähnliche Datenstrukturen oder "ausgerollte" Schleifen erzeugt werden, oder es können ganze Algorithmen auf der Grundlage komplexer Parameter generiert werden. Ein "Sortier"-Makro könnte zum Beispiel die Spezifikation eines komplexen Sortierschlüssels akzeptieren und einen für diesen speziellen Schlüssel ausgearbeiteten Code erzeugen, ohne die Laufzeittests zu benötigen, die für eine allgemeine Prozedur zur Interpretation der Spezifikation erforderlich wären. Eine Organisation, die Assemblersprache verwendet, die mit Hilfe einer solchen Makro-Suite stark erweitert wurde, kann als in einer höheren Sprache arbeitend betrachtet werden, da solche Programmierer nicht mit den konzeptionellen Elementen der niedrigsten Ebene eines Computers arbeiten. Um diesen Punkt zu unterstreichen, wurden Makros verwendet, um eine frühe virtuelle Maschine in SNOBOL4 (1967) zu implementieren, die in der SNOBOL Implementation Language (SIL), einer Assemblersprache für eine virtuelle Maschine, geschrieben wurde. Die Zielmaschine übersetzte diesen Code mit Hilfe eines Makro-Assemblers in ihren eigenen Code. Dies ermöglichte ein für die damalige Zeit hohes Maß an Portabilität.

In der Mainframe-Ära wurden Makros verwendet, um große Softwaresysteme für bestimmte Kunden anzupassen, und sie wurden auch von Kundenpersonal verwendet, um die Anforderungen ihrer Arbeitgeber zu erfüllen, indem sie spezielle Versionen von Hersteller-Betriebssystemen erstellten. Dies geschah beispielsweise durch Systemprogrammierer, die mit IBMs Conversational Monitor System / Virtual Machine (VM/CMS) und mit IBMs "Echtzeit-Transaktionsverarbeitungs"-Zusatzprogrammen Customer Information Control System CICS und ACP/TPF arbeiteten, dem Airline-/Finanzsystem, das in den 70er Jahren entstand und noch heute viele große Computerreservierungssysteme (CRS) und Kreditkartensysteme betreibt.

Es ist auch möglich, ausschließlich die Makroverarbeitungsfähigkeiten eines Assemblers zu nutzen, um Code zu erzeugen, der in völlig anderen Sprachen geschrieben wurde, z. B. um eine Version eines Programms in COBOL zu erzeugen, indem ein reines Makro-Assemblerprogramm verwendet wird, das COBOL-Codezeilen innerhalb von Assemblerzeitoperatoren enthält, die den Assembler anweisen, beliebigen Code zu erzeugen. IBM OS/360 verwendet Makros, um die Systemgenerierung durchzuführen. Der Benutzer legt Optionen fest, indem er eine Reihe von Assembler-Makros kodiert. Das Assemblieren dieser Makros erzeugt einen Jobstream zum Aufbau des Systems, einschließlich der Job-Steuersprache und der Dienstprogramm-Steueranweisungen.

Dies liegt daran, dass das Konzept der "Makroverarbeitung", wie es in den 1960er Jahren erkannt wurde, unabhängig vom Konzept der "Assemblierung" ist, da erstere nach modernen Begriffen eher eine Textverarbeitung als eine Objektcodeerzeugung ist. Das Konzept der Makroverarbeitung tauchte und taucht in der Programmiersprache C auf, die "Präprozessoranweisungen" zum Setzen von Variablen und zur Durchführung bedingter Tests auf deren Werte unterstützt. Im Gegensatz zu bestimmten früheren Makroprozessoren in Assemblern ist der C-Präprozessor nicht Turing-komplett, da er weder Schleifen noch "go to" kann, was es Programmen ermöglicht, Schleifen zu bilden.

Trotz der Mächtigkeit der Makroverarbeitung wurde sie in vielen Hochsprachen nicht mehr verwendet (große Ausnahmen sind C, C++ und PL/I), während sie in Assemblern ein Dauerbrenner bleibt.

Die Substitution von Makroparametern erfolgt strikt nach Namen: Bei der Makroverarbeitung wird der Wert eines Parameters durch seinen Namen ersetzt. Die berühmteste Klasse von Fehlern, die daraus resultierten, war die Verwendung eines Parameters, der selbst ein Ausdruck und kein einfacher Name war, obwohl der Makroautor einen Namen erwartet hatte. In dem Makro:

foo: Makro a
lade a*b 

war beabsichtigt, dass der Aufrufer den Namen einer Variablen angibt und die "globale" Variable oder Konstante b zur Multiplikation von "a" verwendet wird. Wenn foo mit dem Parameter a-c aufgerufen wird, erfolgt die Makroexpansion von load a-c*b. Um mögliche Unklarheiten zu vermeiden, können Benutzer von Makroprozessoren formale Parameter innerhalb von Makrodefinitionen einklammern, oder Aufrufer können die Eingabeparameter einklammern.

Unterstützung für strukturierte Programmierung

Es wurden Makropakete geschrieben, die strukturierte Programmierelemente zur Kodierung des Ausführungsablaufs enthalten. Das früheste Beispiel für diesen Ansatz war der Concept-14-Makrosatz, der ursprünglich von Harlan Mills (März 1970) vorgeschlagen und von Marvin Kessler in der Federal Systems Division von IBM implementiert wurde und der IF/ELSE/ENDIF und ähnliche Kontrollflussblöcke für OS/360-Assembler-Programme bereitstellte. Dies war eine Möglichkeit, die Verwendung von GOTO-Operationen im Assembler-Code zu reduzieren oder zu eliminieren, einer der Hauptfaktoren, die zu Spaghetti-Code in der Assemblersprache führen. Dieser Ansatz war in den frühen 1980er Jahren (in den letzten Tagen der groß angelegten Verwendung von Assemblersprache) weithin akzeptiert. Das High Level Assembler Toolkit von IBM enthält ein solches Makropaket.

Ein kurioses Design war A-natural, ein "stromorientierter" Assembler für 8080/Z80-Prozessoren von Whitesmiths Ltd. (Entwickler des Unix-ähnlichen Idris-Betriebssystems und des angeblich ersten kommerziellen C-Compilers). Die Sprache wurde als Assembler klassifiziert, weil sie mit rohen Maschinenelementen wie Opcodes, Registern und Speicherreferenzen arbeitete; sie enthielt jedoch eine Ausdruckssyntax zur Angabe der Ausführungsreihenfolge. Klammern und andere Spezialsymbole steuerten zusammen mit blockorientierten strukturierten Programmierkonstrukten die Reihenfolge der generierten Anweisungen. A-natural wurde als Objektsprache für einen C-Compiler und nicht für die manuelle Programmierung entwickelt, aber seine logische Syntax hat einige Fans gefunden.

Seit dem Niedergang der Assembler-Entwicklung im großen Stil ist die Nachfrage nach anspruchsvolleren Assemblern offensichtlich gering. Trotzdem werden sie immer noch entwickelt und in Fällen eingesetzt, in denen Ressourcenbeschränkungen oder Besonderheiten in der Architektur des Zielsystems den effektiven Einsatz von höheren Sprachen verhindern.

Assembler mit einer starken Makro-Engine ermöglichen eine strukturierte Programmierung über Makros, wie z. B. das Switch-Makro im Masm32-Paket (dieser Code ist ein vollständiges Programm):

include \masm32\include\masm32rt.inc ; Verwendung der Masm32-Bibliothek <span title="Aus: Englische Wikipedia, Abschnitt &quot;Support for structured programming&quot;" class="plainlinks">[https://en.wikipedia.org/wiki/Assembly_language#Support_for_structured_programming <span style="color:#dddddd">ⓘ</span>]</span>

.code
demomain:
  REPEAT 20
	switch rv(nrandom, 9) ; eine Zahl zwischen 0 und 8 erzeugen
	mov ecx, 7
	Fall 0
		print "fall 0"
	case ecx ; im Gegensatz zu den meisten anderen Programmiersprachen,
		print "case 7" ; der Masm32-Schalter erlaubt "variable Fälle"
	case 1 .. 3
		.wenn eax==1
			drucke "Fall 1"
		.elseif eax==2
			drucke "Fall 2"
		.sonst
			print "Fälle 1 bis 3: andere"
		.endif
	Fall 4, 6, 8
		print "Fälle 4, 6 oder 8"
	Standard
		mov ebx, 19 ; 20 Sterne ausdrucken
		.wiederholen
			print "*"
			dec ebx
		.Until Sign? ; Schleife, bis das Vorzeichenflag gesetzt ist
	endsw
	print chr$(13, 10)
  ENDM
  exit
end demomain

Verwendung von Assembler

Ein Quelltext in Assemblersprache wird auch als Assemblercode bezeichnet. Programme in Assemblersprachen zeichnen sich dadurch aus, dass alle Möglichkeiten des Mikroprozessors genutzt werden können, was heutzutage selten erforderlich ist. Sie finden im Allgemeinen nur noch dann Anwendung, wenn Programme bzw. einzelne Teile davon sehr zeitkritisch sind, z. B. beim Hochleistungsrechnen oder bei Echtzeitsystemen. Ihre Nutzung kann auch dann sinnvoll sein, wenn für die Programme nur sehr wenig Speicherplatz zur Verfügung steht (z. B. in eingebetteten Systemen).

Unter dem Aspekt der Geschwindigkeitsoptimierung kann der Einsatz von Assemblercode auch bei verfügbaren hochoptimierenden Compilern noch seine Berechtigung haben, Vor- und Nachteile sollten aber für die spezifische Anwendung abgewogen werden. Bei komplexer Technik wie Intel Itanium und verschiedenen digitalen Signalprozessoren kann ein Compiler u. U. durchaus besseren Code erzeugen als ein durchschnittlicher Assemblerprogrammierer, da das Ablaufverhalten solcher Architekturen mit komplexen mehrstufigen intelligenten Optimierungen (z. B. Out-of-order execution, Pipeline-Stalls, …) hochgradig nichtlinear ist. Die Geschwindigkeitsoptimierung wird immer komplexer, da zahlreiche Nebenbedingungen eingehalten werden müssen. Dies ist ein gleichermaßen wachsendes Problem sowohl für die immer besser werdenden Compiler der Hochsprachen als auch für Programmierer der Assemblersprache. Für einen optimalen Code wird immer mehr Kontextwissen benötigt (z. B. Cachenutzung, räumliche und zeitliche Lokalität der Speicherzugriffe), welches der Assemblerprogrammierer teilweise (im Gegensatz zum Compiler) durch Laufzeitprofiling des ausgeführten Codes in seinem angestrebten Anwendungsfeld gewinnen kann. Ein Beispiel hierfür ist der SSE-Befehl MOVNTQ, welcher wegen des fehlenden Kontextwissens von Compilern kaum optimal eingesetzt werden kann.

Die Rückwandlung von Maschinencode in Assemblersprache wird Disassemblierung genannt. Der Prozess ist allerdings verlustbehaftet, bei fehlenden Debug-Informationen hochgradig verlustbehaftet, da sich viele Informationen wie ursprüngliche Bezeichner oder Kommentare nicht wiederherstellen, da diese beim Assemblieren nicht in den Maschinencode übernommen wurden oder berechnet wurden.

Historische Perspektive

Assembler waren zu der Zeit, als der speicherprogrammierbare Computer eingeführt wurde, noch nicht verfügbar. Kathleen Booth wird die "Erfindung der Assemblersprache" zugeschrieben, die auf theoretischen Arbeiten beruhte, die sie 1947 während ihrer Arbeit am ARC2 an der Birkbeck University of London begann, nachdem sie von Andrew Booth (ihrem späteren Ehemann) mit dem Mathematiker John von Neumann und dem Physiker Herman Goldstine am Institute for Advanced Study beraten wurde.

Ende 1948 hatte der Electronic Delay Storage Automatic Calculator (EDSAC) einen Assembler (genannt "initial orders") in sein Bootstrap-Programm integriert. Er verwendete Ein-Buchstaben-Mnemonics, die von David Wheeler entwickelt wurden, der von der IEEE Computer Society als Schöpfer des ersten "Assemblers" angesehen wird. In Berichten über den EDSAC wurde der Begriff "Assembler" für den Prozess der Kombination von Feldern zu einem Befehlswort eingeführt. SOAP (Symbolic Optimal Assembly Program) war eine Assemblersprache für den IBM 650 Computer, die 1955 von Stan Poley geschrieben wurde.

Assemblersprachen machen einen Großteil der fehleranfälligen, mühsamen und zeitaufwändigen Programmierung der ersten Generation von Computern überflüssig und befreien die Programmierer von lästigen Aufgaben wie dem Merken von Zahlencodes und der Berechnung von Adressen. Sie waren einst für alle Arten der Programmierung weit verbreitet. In den späten 1950er Jahren wurde ihre Verwendung jedoch im Zuge der Suche nach einer höheren Programmierproduktivität weitgehend von höheren Sprachen verdrängt. Heute wird Assemblersprache immer noch für direkte Hardwaremanipulationen, den Zugriff auf spezielle Prozessorbefehle oder zur Lösung kritischer Leistungsprobleme verwendet. Typische Anwendungen sind Gerätetreiber, eingebettete Low-Level-Systeme und Echtzeitsysteme (siehe § Aktuelle Verwendung).

In der Vergangenheit wurden zahlreiche Programme vollständig in Assembler geschrieben. Der Burroughs MCP (1961) war der erste Computer, für den ein Betriebssystem nicht vollständig in Assembler entwickelt wurde; er wurde in Executive Systems Problem Oriented Language (ESPOL), einem Algol-Dialekt, geschrieben. Viele kommerzielle Anwendungen wurden ebenfalls in Assembler geschrieben, darunter ein großer Teil der IBM-Mainframe-Software, die von großen Unternehmen entwickelt wurde. COBOL, FORTRAN und etwas PL/I verdrängten schließlich einen großen Teil dieser Arbeit, obwohl eine Reihe von großen Organisationen bis in die 1990er Jahre hinein Assembler-Anwendungsinfrastrukturen beibehielten.

Die meisten frühen Mikrocomputer basierten auf handcodierter Assemblersprache, einschließlich der meisten Betriebssysteme und großen Anwendungen. Der Grund dafür war, dass diese Systeme nur über begrenzte Ressourcen verfügten, idiosynkratische Speicher- und Anzeigearchitekturen erforderten und nur begrenzte, fehleranfällige Systemdienste boten. Vielleicht noch wichtiger war, dass es keine erstklassigen Compiler für Hochsprachen gab, die für den Einsatz auf Mikrocomputern geeignet waren. Auch ein psychologischer Faktor mag eine Rolle gespielt haben: Die erste Generation von Mikrocomputer-Programmierern hatte eine hobbymäßige "Draht- und Zangen"-Einstellung.

In einem eher kommerziellen Kontext waren die wichtigsten Gründe für die Verwendung von Assembler minimaler Umfang, minimaler Overhead, höhere Geschwindigkeit und Zuverlässigkeit.

Typische Beispiele für große Assemblerprogramme aus dieser Zeit sind die IBM PC DOS-Betriebssysteme, der Turbo Pascal Compiler und frühe Anwendungen wie das Tabellenkalkulationsprogramm Lotus 1-2-3. Assembler wurde verwendet, um die beste Leistung aus dem Sega Saturn herauszuholen, einer Konsole, für die die Entwicklung und Programmierung von Spielen bekanntermaßen schwierig war. Das Arcade-Spiel NBA Jam von 1993 ist ein weiteres Beispiel.

Assembler war lange Zeit die primäre Entwicklungssprache für viele beliebte Heimcomputer der 1980er und 1990er Jahre (wie den MSX, Sinclair ZX Spectrum, Commodore 64, Commodore Amiga und Atari ST). Dies lag zum großen Teil daran, dass interpretierte BASIC-Dialekte auf diesen Systemen eine unzureichende Ausführungsgeschwindigkeit sowie unzureichende Möglichkeiten boten, um die vorhandene Hardware auf diesen Systemen voll auszunutzen. Einige Systeme verfügen sogar über eine integrierte Entwicklungsumgebung (IDE) mit hochentwickelten Debugging- und Makrofunktionen. Einige Compiler, die für den Radio Shack TRS-80 und seine Nachfolger verfügbar waren, konnten Inline-Assembler-Quellcode mit High-Level-Programmanweisungen kombinieren. Beim Kompilieren erzeugte ein eingebauter Assembler Inline-Maschinencode.

Aktuelle Verwendung

Es gab schon immer Debatten über den Nutzen und die Leistung von Assembler im Vergleich zu Hochsprachen.

Obwohl Assembler für bestimmte Nischenanwendungen wichtig ist (siehe unten), gibt es andere Optimierungswerkzeuge.

Im Juli 2017 rangiert Assembler im TIOBE-Index für die Beliebtheit von Programmiersprachen auf Platz 11, noch vor Visual Basic zum Beispiel. Assembler kann zur Geschwindigkeits- oder Größenoptimierung verwendet werden. Im Falle der Geschwindigkeitsoptimierung wird behauptet, dass moderne optimierende Compiler Hochsprachen in Code umwandeln können, der genauso schnell läuft wie handgeschriebener Assembler, obwohl es auch Gegenbeispiele gibt. Die Komplexität moderner Prozessoren und Speichersubsysteme macht eine wirksame Optimierung für Compiler wie auch für Assembler-Programmierer immer schwieriger. Darüber hinaus hat die zunehmende Prozessorleistung dazu geführt, dass die meisten CPUs die meiste Zeit im Leerlauf arbeiten, wobei Verzögerungen durch vorhersehbare Engpässe wie Cache-Misses, E/A-Operationen und Paging entstehen. Dies hat dazu geführt, dass die reine Code-Ausführungsgeschwindigkeit für viele Programmierer keine Rolle mehr spielt.

Es gibt einige Situationen, in denen sich Entwickler für die Verwendung von Assembler entscheiden können:

  • Beim Schreiben von Code für Systeme mit älteren Prozessoren, die nur begrenzte Hochsprachenoptionen haben, wie der Atari 2600, Commodore 64 und Grafikrechner. Programme für diese Computer aus den 1970er und 1980er Jahren werden oft im Kontext der Demoszene oder Retrogaming-Subkulturen geschrieben.
  • Code, der direkt mit der Hardware interagieren muss, z. B. in Gerätetreibern und Interrupt-Handlern.
  • In einem eingebetteten Prozessor oder DSP erfordern Interrupts mit hoher Wiederholungsrate die kürzeste Anzahl von Zyklen pro Interrupt, z. B. ein Interrupt, der 1000 oder 10000 Mal pro Sekunde auftritt.
  • Programme, die prozessorspezifische Anweisungen verwenden müssen, die nicht in einem Compiler implementiert sind. Ein gängiges Beispiel ist die bitweise Rotationsanweisung, die den Kern vieler Verschlüsselungsalgorithmen bildet, sowie die Abfrage der Parität eines Bytes oder des 4-Bit-Übertrags einer Addition.
  • Es wird eine eigenständige ausführbare Datei von kompakter Größe benötigt, die ohne Rückgriff auf die Laufzeitkomponenten oder Bibliotheken einer Hochsprache ausgeführt werden muss. Zu den Beispielen gehören Firmware für Telefone, Kraftstoff- und Zündsysteme von Kraftfahrzeugen, Klimasteuerungssysteme, Sicherheitssysteme und Sensoren.
  • Programme mit leistungsabhängigen inneren Schleifen, bei denen Assemblersprache Optimierungsmöglichkeiten bietet, die in einer Hochsprache nur schwer zu erreichen sind. Zum Beispiel lineare Algebra mit BLAS oder diskreter Kosinustransformation (z. B. SIMD-Assembly-Version von x264).
  • Programme, die vektorisierte Funktionen für Programme in höheren Sprachen wie C erstellen. In der höheren Sprache wird dies manchmal durch compilerinterne Funktionen unterstützt, die direkt auf SIMD-Mnemonics abgebildet werden, aber dennoch zu einer Eins-zu-Eins-Assembler-Konvertierung führen, die spezifisch für den jeweiligen Vektorprozessor ist.
  • Echtzeitprogramme wie Simulationen, Flugnavigationssysteme und medizinische Geräte. In einem Fly-by-Wire-System beispielsweise müssen Telemetriedaten innerhalb strenger Zeitvorgaben interpretiert und verarbeitet werden. Solche Systeme müssen Quellen für unvorhersehbare Verzögerungen ausschließen, die durch (einige) interpretierte Sprachen, automatische Garbage Collection, Paging-Operationen oder präemptives Multitasking entstehen können. Einige höhere Sprachen enthalten jedoch Laufzeitkomponenten und Betriebssystemschnittstellen, die zu solchen Verzögerungen führen können. Durch die Wahl von Assembler- oder niedrigeren Sprachen für solche Systeme erhalten die Programmierer eine bessere Übersicht und Kontrolle über die Verarbeitungsdetails.
  • Kryptografische Algorithmen, die immer genau die gleiche Zeit für die Ausführung benötigen, um Zeitangriffe zu verhindern.
  • Modifizierung und Erweiterung von Legacy-Code, der für IBM-Großrechner geschrieben wurde.
  • Situationen, in denen eine vollständige Kontrolle über die Umgebung erforderlich ist, in Situationen mit extrem hoher Sicherheit, in denen nichts als selbstverständlich angesehen werden kann.
  • Computerviren, Bootloader, bestimmte Gerätetreiber oder andere Elemente, die sehr nah an der Hardware oder dem Low-Level-Betriebssystem liegen.
  • Befehlssatzsimulatoren zur Überwachung, Nachverfolgung und Fehlersuche, bei denen der zusätzliche Overhead auf ein Minimum beschränkt werden soll.
  • Situationen, in denen keine Hochsprache existiert, auf einem neuen oder speziellen Prozessor, für den kein Cross-Compiler verfügbar ist.
  • Reverse-Engineering und Modifizierung von Programmdateien wie z.B.:
    • Bestehende Binärdateien, die ursprünglich in einer Hochsprache geschrieben worden sein können oder auch nicht, z. B. wenn versucht wird, Programme nachzubilden, für die der Quellcode nicht verfügbar ist oder verloren gegangen ist, oder den Kopierschutz geschützter Software zu knacken.
    • Videospiele (auch als ROM-Hacking bezeichnet), was mit verschiedenen Methoden möglich ist. Die am weitesten verbreitete Methode ist das Ändern von Programmcode auf Assembler-Ebene.

Assembler wird immer noch in den meisten Informatik- und Elektronikstudiengängen gelehrt. Obwohl heute nur wenige Programmierer regelmäßig mit Assembler arbeiten, sind die zugrunde liegenden Konzepte nach wie vor wichtig. Grundlegende Themen wie Binärarithmetik, Speicherzuweisung, Stack-Verarbeitung, Zeichensatzkodierung, Interrupt-Verarbeitung und Compiler-Entwurf lassen sich nur schwer im Detail studieren, wenn man nicht weiß, wie ein Computer auf Hardware-Ebene funktioniert. Da das Verhalten eines Computers im Wesentlichen durch seinen Befehlssatz definiert wird, ist der logische Weg zum Erlernen solcher Konzepte das Studium einer Assemblersprache. Die meisten modernen Computer haben ähnliche Befehlssätze. Daher reicht das Studium einer einzigen Assemblersprache aus, um zu lernen: I) die grundlegenden Konzepte zu erlernen; II) Situationen zu erkennen, in denen die Verwendung von Assemblersprache sinnvoll sein könnte; und III) zu sehen, wie effizient ausführbarer Code aus Hochsprachen erstellt werden kann.

Typische Anwendungen

  • Assembler wird typischerweise im Bootcode eines Systems verwendet, dem Low-Level-Code, der die Systemhardware vor dem Booten des Betriebssystems initialisiert und testet und oft im ROM gespeichert ist. (Das BIOS auf IBM-kompatiblen PC-Systemen und CP/M ist ein Beispiel dafür.)
  • Assembler wird häufig für Low-Level-Code verwendet, z. B. für Betriebssystem-Kernel, die sich nicht auf die Verfügbarkeit bereits vorhandener Systemaufrufe verlassen können, sondern diese für die jeweilige Prozessorarchitektur, auf der das System läuft, implementieren müssen.
  • Einige Compiler übersetzen Hochsprachen zunächst in Assembler, bevor sie sie vollständig kompilieren, so dass der Assemblercode zu Debugging- und Optimierungszwecken angezeigt werden kann.
  • Einige Compiler für relativ einfache Sprachen wie Pascal oder C ermöglichen es dem Programmierer, Assembler direkt in den Quellcode einzubetten (so genanntes Inline-Assembly). Programme, die solche Möglichkeiten nutzen, können dann Abstraktionen konstruieren, die für jede Hardwareplattform eine andere Assemblersprache verwenden. Der portable Code des Systems kann dann diese prozessorspezifischen Komponenten über eine einheitliche Schnittstelle verwenden.
  • Assemblersprache ist nützlich beim Reverse Engineering. Viele Programme werden nur in Form von Maschinencode vertrieben, der sich mit einem Disassembler leicht in Assemblersprache übersetzen lässt, aber schwieriger mit einem Decompiler in eine höhere Sprache zu übersetzen ist. Tools wie der Interactive Disassembler machen zu diesem Zweck ausgiebig Gebrauch von der Disassemblierung. Diese Technik wird von Hackern verwendet, um kommerzielle Software zu knacken, und von Konkurrenten, um Software mit ähnlichen Ergebnissen von konkurrierenden Unternehmen herzustellen.
  • Assemblersprache wird verwendet, um die Ausführungsgeschwindigkeit zu erhöhen, insbesondere bei frühen Personalcomputern mit begrenzter Verarbeitungsleistung und RAM.
  • Assembler können verwendet werden, um aus formatiertem und kommentiertem Quellcode Datenblöcke zu generieren, die von anderem Code verwendet werden können, ohne dass ein Overhead durch Hochsprache entsteht.

Beispielprogramm

Ein sehr einfaches Programm, das zu Demonstrationszwecken häufig benutzte Hallo-Welt-Beispielprogramm, kann zum Beispiel in der Assemblersprache MASM für MS-DOS aus folgendem Assemblercode bestehen:

ASSUME  CS:CODE, DS:DATA        ;- dem Assembler die Zuordnung der Segmentregister zu den Segmenten mitteilen <span title="Aus: Deutsche Wikipedia, Abschnitt &quot;Beispielprogramm&quot;" class="plainlinks">[https://de.wikipedia.org/wiki/Assemblersprache#Beispielprogramm <span style="color:#dddddd">ⓘ</span>]</span>

DATA    SEGMENT                 ;Beginn des Datensegments
Meldung db  "Hallo Welt"        ;- Zeichenkette „Hallo Welt“
        db  13, 10              ;- Neue Zeile
        db  "$"                 ;- Zeichen, das die Textausgabefunktion (INT 21h, Unterfunktion 09h) als Zeichenkettenende versteht
DATA    ENDS                    ;Ende des Datensegments <span title="Aus: Deutsche Wikipedia, Abschnitt &quot;Beispielprogramm&quot;" class="plainlinks">[https://de.wikipedia.org/wiki/Assemblersprache#Beispielprogramm <span style="color:#dddddd">ⓘ</span>]</span>

CODE    SEGMENT                 ;Beginn des Codesegments
Anfang:                         ;- Einsprung-Label fuer den Anfang des Programms
        mov ax, DATA            ;- Adresse des Datensegments in das Register „AX“ laden
        mov ds, ax              ;  In das Segmentregister „DS“ uebertragen (das DS-Register kann nicht direkt mit einem Wert beschrieben werden)
        mov dx, OFFSET Meldung  ;- die zum Datensegment relative Adresse des Textes in das „DX“ Datenregister laden
                                ;  die vollstaendige Adresse von „Meldung“ befindet sich nun im Registerpaar DS:DX
        mov ah, 09h             ;- die Unterfunktion 9 des Betriebssysteminterrupts 21h auswaehlen (Textausgaberoutine)
        int 21h                 ;- den Betriebssysteminterrupt 21h aufrufen (hier erfolgt die Ausgabe des Textes am Schirm)
        mov ax, 4C00h           ;- die Unterfunktion 4Ch (Programmbeendigung) des Betriebssysteminterrupts 21h festlegen
        int 21h                 ;- damit wird die Kontrolle wieder an das Betriebssystem zurueckgegeben (Programmende)
CODE    ENDS                    ;Ende des Codesegments <span title="Aus: Deutsche Wikipedia, Abschnitt &quot;Beispielprogramm&quot;" class="plainlinks">[https://de.wikipedia.org/wiki/Assemblersprache#Beispielprogramm <span style="color:#dddddd">ⓘ</span>]</span>

END     Anfang                  ;- dem Assembler- und Linkprogramm den Programm-Einsprunglabel mitteilen
                                ;- dadurch erhaelt der Befehlszaehler beim Aufruf des Programmes diesen Wert

Vergleichende Gegenüberstellungen für das Hallo-Welt-Programm in unterschiedlichen Assemblerdialekten enthält diese Liste.

In einem Pascal-Quelltext (eine Hochsprache) kann der Programmcode für „Hallo Welt“ dagegen deutlich kürzer sein:

program Hallo(output);
begin
  writeln('Hallo Welt')
end. <span title="Aus: Deutsche Wikipedia, Abschnitt &quot;Beispielprogramm&quot;" class="plainlinks">[https://de.wikipedia.org/wiki/Assemblersprache#Beispielprogramm <span style="color:#dddddd">ⓘ</span>]</span>

Vergleich zur Programmierung in einer Hochsprache

Nachteile

Assemblerprogramme sind sehr hardwarenah geschrieben, da sie direkt die unterschiedlichen Spezifikationen und Befehlssätze der einzelnen Computerarchitekturen (Prozessorarchitektur) abbilden. Daher kann ein Assemblerprogramm im Allgemeinen nicht auf ein anderes Computersystem (andere Prozessorarchitektur) übertragen werden, ohne dass der Quelltext angepasst wird. Das erfordert, abhängig von den Unterschieden der Assemblersprachen, hohen Umstellungsaufwand, unter Umständen ist ein komplettes Neuschreiben des Programmtextes erforderlich. Im Gegensatz dazu muss bei Hochsprachen oft nur ein Compiler für die neue Zielplattform verwendet werden.

Quelltexte in Assemblersprache sind fast immer deutlich länger als in einer Hochsprache, da die Instruktionen weniger komplex sind und deshalb gewisse Funktionen/Operationen mehrere Assemblerbefehle erfordern; z. B. müssen beim logischen Vergleich von Daten (= > < …) ungleiche Datenformate oder -Längen zunächst angeglichen werden. Die dadurch größere Befehlsanzahl erhöht das Risiko, unübersichtlichen, schlecht strukturierten und schlecht wartbaren Programmcode herzustellen.

Vorteile

Nach wie vor dient Assembler zur Mikro-Optimierung von Berechnungen, für die der Hochsprachencompiler nicht ausreichend effizienten Code generiert. In solchen Fällen können Berechnungen effizienter direkt in Assembler programmiert werden. Beispielsweise sind im Bereich des wissenschaftlichen Rechnens die schnellsten Varianten mathematischer Bibliotheken wie BLAS oder bei architekturabhängigen Funktionen wie der C-Standardfunktion memcpy weiterhin die mit Assembler-Code. Auch lassen sich gewisse, sehr systemnahe Operationen unter Umgehung des Betriebssystems (z. B. direktes Schreiben in den Bildschirmspeicher) nicht in allen Hochsprachen ausführen.

Der Nutzen von Assembler liegt auch im Verständnis der Arbeits- und Funktionsweise eines Systems, das durch Konstrukte in Hochsprachen versteckt wird. Auch heute noch wird an vielen Hochschulen Assembler gelehrt, um ein Verständnis für die Rechnerarchitektur und seine Arbeitsweise zu bekommen.