Die Community zu .NET und Classic VB.
Menü

Visual Basic und Assembler - Seite 3

 von 

Kleine Helferlein
Nächste Seite >>
Begriffsbestimmungen
<< Vorherige Seite

Grundzüge der Assemblerprogrammierung  

Es kann an dieser Stelle kein ausführlicher Sprach-Lehrgang erfolgen. Das ist aber auch nicht nötig, denn der 80x86 kennt gerade einmal um die 150 Befehle, die sich z.T. nur geringfügig unterscheiden, und von denen i.d.R. nur ein Bruchteil zum Einsatz kommt. Nichtsdestotrotz werde ich ausführlich auf die 80x86-Register eingehen, weil mir das Thema in den aktuellen Dokumentationen zu kurz kommt, um sie richtig verwenden zu können. Der Befehlssatz kann im Internet nachgeschlagen werden - das letzte Wort haben selbstverständlich die lobenswert übersichtlichen Intel-Dokumentationen, die kostenfrei bei Intel im PDF-Format geladen werden können. Wer noch nie mit Maschinensprache zu tun gehabt hat, sollte sich ruhig Band 1 (Grundlagen) ansehen; wer bereits Erfahrungen sammeln konnte, kann sicher gleich mit Band 2 (Befehlssatz) einsteigen. Intel Handbücher

Programmumgebung

Assemblerprogramme haben kein eigenes Dateiformat. Sie können mit jedem x-beliebigen ASCII-Zeileneditor geschrieben werden, ja, dürfen keinerlei Sonderzeichen enthalten. Um überhaupt Programmcode zu erzeugen, bedarf es aber eines gewissen Rahmens (environment), damit der Assembler weiß, was er tun soll. Deshalb beginnt jede Assembler-Datei mit sog. Assembler-Direktiven, denen stets ein Punkt vorangeht, was impliziert, dass selbst definierte Bezeichner niemals mit einem Punkt beginnen dürfen.

Im folgenden wird der Rahmen für eine ausführbare Datei im portable executable file format PE-Format beschrieben. Wie so erzeugte EXE-Dateien in den VB-Code übernommen werden können, steht weiter unten unter "Kleine Helferlein".

Auswahl des Prozessors/Befehlsumfanges

Zunächst wird festgelegt, welcher Befehlssatz verwendet werden soll. Die Assembler-Direktive .386 regelt, dass keine Befehle benutzt werden können, die es erst ab dem 80486 gibt. Entsprechend hebt die Direktive .586 jede Beschränkung auf.

16-Bit- oder 32-Bit-Umgebung

Dann ist zu bestimmen, ob im 16-Bit-Modus oder im 32-Bit-Modus gearbeitet werden soll und in welcher Reihenfolge Parameter an Funktionen übergeben werden. Die Direktive .Model FLAT [, STDCALL] regelt beides im Sinne der W32-Nutzung.

Kennzeichnung des Programmanfanges

Jeder Code-Block muss durch .Code zur Unterscheidung von z.B. Datenblöcken (.Data) gekennzeichnet sein.

Kennzeichnung des ersten ausführbaren Befehls

Da auch innerhalb eines Codeblockes Daten, z.B. Texte, gespeichert werden können, braucht der Assembler einen Hinweis darauf, an welcher Stelle genau die erste ausführbare Befehlszeile innerhalb des Codes steht. Wir könnten dies frei gestalten, doch vereinfacht es die Sache, das Programm direkt mit der zu erstellenden Funktion zu beginnen und ggf. benötigte Texte an das Ende zu packen, um beim Aufruf aus VB keinen Versatz (Offset) für den Einsprung berechnen zu müssen. Der Name des Programmeinstieges ist beliebig. Es haben sich ebenso "start" wie "_run" eingebürgert. Der so gekennzeichnete Block muss ausdrücklich mit END <name> beendet werden.

Kennzeichnung der Prozedur

Es ist in diesem Rahmen nicht unbedingt notwendig, jedoch ordentlicher, die ASM-Funktion auch als solche mit dem Pseudo-Befehl <name> PROC NEAR zu kennzeichnen. Prozeduren können near, d.i. im eigenen Adressraum, oder far, d.i. in einem fernen Adressraum, deklariert werden - ihr Ende wird mit <name> ENDP gekennzeichnet. Bei der Verwendung in VB sind Prozeduren stets near, da sie im VB-eigenen Adressraum erzeugt und seitens VB/API auch per call near (s. Befehlssatz) aufgerufen werden.

Zusammengefasst ergibt das diesen Standard-Programmrahmen:

.386
.MODEL FLAT

.CODE
_run:

MyFunction PROC NEAR

; hier steht der Programmcode
; hier steht der Programmcode

MyFunction ENDP

END _run

Bitte beachten

Bei der direkten Einbindung von ASM-Code in VB, wie sie hier vorgenommen wird, darf es nur ein Codesegment, kein Datensegment und keine Verweise auf externe Funktionen (API) in der EXE-Datei geben! Programme werden normalerweise vom Windows-Loader in den Speicher geladen und alle Bezüge entsprechend der Ladeadresse angepasst (relocated). Da wir das Programm selbst an eine willkürliche Adresse laden, werden Bezüge nicht angepasst und verweisen somit auf unbestimmte Adressen. Ein Assemblerprogramm darf beliebig viele Prozeduren enthalten, jedoch ist es schwierig, diese aufzurufen, weil die Startadressen erst beim Assemblieren ermittelt werden. Daher ist es empfehlenswert, für jede ASM-Prozedur eine eigene EXE-Datei anzulegen. Wird der Assembler-Code als DLL eingebunden, gelten diese Beschränkungen selbstverständlich nicht.

Programmcode

Es wurde schon erwähnt, dass Maschinensprache eher spartanisch ausgestattet ist. So gibt es keine Schleifen-Befehle, keinen IF-Befehl etc., sondern nur einzeilige Anweisungen mit stets demselben Aufbau:
Label: Befehl Parameter ; Kommentar

Label (optional) Das Label ist eine Sprungmarke. Es beginnt mit einem Buchstaben oder einem Unterstrich und endet mit einem Doppelpunkt. Genau wie in alten BASIC-Codes kann mit einem Sprungbefehl dorthin verzweigt werden. In einer Near-Prozedur sind alle Sprünge relativ zur aufrufenden Adresse. Der Wert wird vom Assembler während der Assemblierung berechnet.
Befehl (optional> Die Befehle können der Intel-Dokumentation entnommen werden (a.a.O. Band 2). Am häufigsten sind Befehle anzutreffen, die Werte verschieben (MOV für move), vergleichen (CMP für compare) und in Abhängigkeit vom Vergleichsergebnis zu anderen Programmadressen verzweigen (Jcc für jump conditionally).
Parameter siehe unten
Kommentar (optional) Kommentare werden durch spezielle Zeichen gekennzeichnet (MASM benutzt das Semikolon). Der Assembler ignoriert schlicht jeglichen darauf folgenden Text. Werden mehrere aufeinanderfolgende Kommentarzeilen benötigt, können diese mit COMMENT * eingeleitet werden. Für das Sternchen darf jedes Zeichen benutzt werden; es besagt, dass aller Text bis zum nächsten Auftreten dieses Zeichens Kommentar ist.

Parameter

Es gibt nur drei Parameterarten: Register, direkte und indirekte Werte.

Register sind im Prozessor selbst verfügbare Speicherplätze, derer es drei Gruppen gibt:

Generelle Datenregister EAX, EBX, ECX, EDX
Indexregister ESI, EDI, EBP, ESP, EIP
Segmentregister DS, ES, SS, CS, GS, FS
MOV EAX,EBX Kopiert EBX nach EAX
JMP EAX Das Programm verzweigt an die in EAX gegebene Adresse

Der Direkt -Parameter ist eine Ganzzahl, die addiert, subtrahiert oder anderweitig bearbeitet werden kann:

MOV EAX,3 Lädt Wert 3 ins Register EAX
JMP 4711 Das Programm verzweigt an die Adresse 4711
JMP $+9 Das Dollarzeichen steht für die aktuelle Programmadresse. In diesem Fall steht es für die Adresse des Sprungbefehls selbst. Da der natürlich ausgeführt wird, überspringt der Prozessor effektiv 7 Bytes
MOV EAX,Offset abc In VB ausgedrückt: EAX = AddressOf(abc)

Direkt-Parameter können binär, oktal, dezimal oder hexadezimal angegeben werden und müssen zur Unterscheidung von Labels mit einer Ziffer (ggf. einer Null) beginnen:

1111b Binär für 15
17o Oktal für 15
15d Das d für dezimal kann entfallen (Standardeinstellung)
0Fh Hexadezimal für 15 (die führende Null ist unumgänglich)

Indirekt meint: der gegebene Parameter verweist auf eine Speicherstelle. Zur Kennzeichnung des Verweises wird der Parameter in eckigen Klammern geschrieben:

MOV EAX,[3] Lädt den Wert ins Register EAX, der an Speicherstelle 3 steht. Da EAX ein 32-Bit-Register ist, werden die Speicherstellen 3 bis 6 geladen!
MOV EAX,[EBX] Lädt den Wert der Speicherstelle ins Register EAX, die durch den Wert in EBX (bis EBX+3 !) bezeichnet (indiziert) wird.
MOV [3],EAX Hier wird der Vorgang umgekehrt, d.h. der Wert des Registers EAX wird in den Speicherstellen 3 bis 6 abgelegt.
MOV [$+3],EAX Dieserart würde sich das Programm selbst variieren, denn der Wert des Registers EAX würde direkt hinter den Befehl geschrieben.
MOV EAX,[Offset abc] Hier wird EAX mit dem Wert aus der Speicherstelle geladen, die durch das Label abc gekennzeichnet ist.

Grundsätzlich gilt: Für den Prozessor ist jeder Wert schlicht irgendein Integer. D.h. der Programmierer muss selbst darauf achten, ob eine Adresse z.B. einen Stringpointer oder den Offset einer Stringvariablen bezeichnet, ob also etwas dorthin geschrieben werden darf oder nicht. Art und Anzahl der Parameter sind befehlsabhängig. Der Move-Befehl z.B. erwartet zwei Parameter, die bestimmen, welcher Wert wohin verschoben werden soll. Der Jump-Befehl erwartet hingegen nur einen Parameter, nämlich die Zieladresse (bzw. den Offset). Andere Befehle kommen ganz ohne Parameter aus, wie z.B. der Befehl STC. Derzeit gibt es nur ein paar Befehle, die drei Parameter erlauben (SHLD, SHRD, IMUL).

Bitte beachten!

  • Parameter sind niemals optional )
  • Sind zwei Parameter gegeben, dann ist der erste das Ziel (destination, dst), während der zweite Herkunft (source, src) genannt wird. Bei einem Additionsbefehl (add dst,src) heißt das: src wird zu dst addiert und das Ergebnis in dst gespeichert.

Register

Der richtige Umgang mit Registern ist besonders wichtig. Deshalb sei diesem Thema hier ein eigener Abschnitt gewidmet, obwohl es inhaltlich eher zum letzten Punkt, Parameter, gehört.

Registeraufbau

Im Laufe der Prozessorentwicklung ist die Registerbreite, zweimal verdoppelt worden: von 8 auf 16 und von 16 auf 32 Bits. Um Kompatibilität zu gewährleisten, sind die ehemaligen 8-Bit-Register immer noch als solche verfügbar, besser: werden Befehle vom Prozessor so ausgeführt, als wären die Register unverändert geblieben. Tatsächlich handelt es sich aber um die unteren Bits eines 32-Bit-Registers:


Abbildung 1: Bitregister

  • Zunächst fällt auf, dass die Bits von rechts nach links beginnend bei Null gezählt werden.
  • In derselben "umgekehrten" Weise werden auch Daten im Speicher abgelegt: das niederwertige Byte zuerst, das höchstwertige Byte zuletzt (little endian = little end first).
  • Dann wird deutlich, dass nicht alle Register "teilbar" sind. Die Generellen Datenregister können als 8-, 16- oder 32-Bit-Register, die Indexregister als 16- oder 32-Bit-Register angesprochen werden ). Dabei ist wichtig zu beachten, dass sich z.B. der Wert von EAX ändert, wenn der Wert von AL, AH oder AX geändert wird, weil es sich bloß um eine logische Unterteilung ein und desselben physischen Registers handelt! Segmentregister können grundsätzlich nur in ihrer 32-Bit-Form angesprochen werden.
  • Nicht zuletzt vermisst man weitere Register. Die Ausstattung des 80x86 ist wirklich spärlich. In seiner gesamten Entwicklungsgeschichte sind nur 2 Register (FS und GS) hinzugekommen. Allerdings wurden die Adressierungsarten (s.u.) sehr erweitert.
  • Außerdem stehen offensichtlich nur Ganzzahlen (Integer) zur Verfügung. In der Tat kennt der 80x86 keine Gleitkommazahlen. Seit dem 80486 ist der mathematische Co-Prozessor 80x87 aber fester Bestandteil eines jeden Chips. Alle Befehle, die mit einem F anfangen, richten sich an die Gleitkommazahl-Einheit (floating point unit, FP). Diese hat 8 eigene 80 Bit breite Register, auf deren Verwendung hier nicht weiter eingegangen wird.

Namensherleitung

Die Fähigkeit, Berechnungen durchzuführen, war bei dem 8-Bit-Rechner 8080 (Vorgänger des 8086), noch dem arithmetic-logical-, kurz: AL-Register oder Akkumulator vorbehalten. Alle zu bearbeitenden Werte mussten zunächst in dieses Register überführt werden. Um sie aus dem Hauptspeicher (RAM )) laden bzw. dorthin schreiben zu können, verfügte die CPU über ein doppelt breites Register, dessen Inhalt auf eine Speicherstelle wies, man sagt: eine Adresse indizierte, so dass die Kommunikation gewissermaßen "durch" dieses Register vonstatten ging.

Mit der Einführung des 16-Bit-Prozessors 8086 im IBM-PC waren die Möglichkeiten explodiert: Mehrere Register konnten Berechnungen durchführen - sie wurden deshalb Generelle Datenregister genannt. Außerdem standen mit einem mal 640 KB Hauptspeicher statt der bis dato üblichen 64 KB zur Verfügung ), so dass wesentlich mehr Daten im RAM gehalten werden konnten. Um den Datenaustausch zwischen Prozessor und Hauptspeicher zu beschleunigen bzw. einen Flaschenhals zu vermeiden, d.h. nicht alle Generellen Datenregister "durch" ein einziges Register kommunizieren zu lassen, kamen weitere Register hinzu, die nicht befähigt waren, Berechnungen durchzuführen, sondern ausschließlich der Indizierung von Speicher dienten, weshalb sie Indexregister genannt wurden.

Nun waren aber die Indexregister nur 16 Bit breit und konnten daher nur 64 KB adressieren. Dieses Problem wurde durch Einführung der Segmentregister gelöst. Zwecks Geschwindigkeitsoptimierung wurde jedem Indexregister ein eigenes Segmentregister zugeteilt:

DS:SI data segment source index
ES:DI extra segment destination index
SS:BP stack segment base pointer
CS:IP code segment instruction pointer

Siehe auch Intel-Dokumentation, Band 1, Kapitel 5 "Specifying a Segment Selector"

Um von der automatischen Zuordnung abzuweichen, also z.B. über das SI-Register einen Wert aus dem durch ES gekennzeichneten Segment zu laden, musste ein Präfix vorangestellt werden: MOV AL,ES:[SI]. Dadurch war es möglich, einen Wert aus dem Speicher zu laden, zu verarbeiten und anschließend andernorts abzulegen, ohne den Wert eines Indexregisters verändern zu müssen.

Beschränkungen und Besonderheiten

Heute sind alle Register 32 Bit breit. Der mögliche Adressraum wird eher durch die Finanzen denn durch den Prozessor resp. die technische Entwicklung begrenzt. Im Flat-Model weist Windows jedem Prozess automatisch 2 GB Hauptspeicher zu und setzt die Segmentregister CS, SS, DS und ES auf denselben Wert; dieser steht nicht für Paragraphs, sondern wählt einen Speicherselektor aus, der Informationen über Ort und Länge des Speichers sowie über Zugriffsberechtigungen enthält. ) Mit Indexregistern (außer EIP) können inzwischen ebenso Berechnungen durchgeführt werden (ADD ESI,4711 o.ä.), wie Generelle Datenregister als Indexregister fungieren (MOV EAX,[EBX] o.ä.), wobei alle Generellen Datenregister dem DS-Segment zugeordnet sind, wenn kein anders lautendes Präfix gegeben ist. Einige Besonderheiten haben aber die Zeit überdauert. Die meisten beziehen sich auf das EAX-, das Akkumulator-Register, was aus dessen besonderer Stellung zu 8080-Zeiten herrührt. Aber auch alle anderen Datenregister haben noch ihrem ursprünglichen Namen entsprechende Besonderheiten:

Generelles Datenregister EAX (accumulator)
Die nicht-vorzeichenbehaftete Multiplikation (MUL) und die Division (DIV) funktionieren bis heute nur mit AL, AX bzw. EAX. Darum bedürfen diese Befehle auch nur eines Parameters (Multiplikator). Das hat seinen Grund vor allem darin, dass diese Operationen - im 32-Bit-Modus die einzigen 64-Bit-Operationen darstellen. Um dies zu verwirklichen, werden je nach MUL-/DIV-Befehl die Register AH:AL, DX:AX (nicht EAX!) oder EDX:EAX zusammengefasst. Nach einer 32-Bit-Multiplikation (MUL EBX) stehen die höherwertigen 32 Bits des Ergebnisses im EDX-Register, während EAX die niederwertigen Bits enthält. Bei einer Division enthält EAX den ganzzahligen Teil des Ergebnisses, EDX den Rest.

Generelles Datenregister EBX (base)
Der Ein-Byte-Befehl ) XLAT, mit dem AL aus der Speicherstelle [EBX+AL] geladen wird, ist bis heute verfügbar. Er impliziert automatisch die Verwendung der Register AL und EBX und kann nicht auf andere Register angewendet werden. Für Wert-Ersetzungen, die sich im Rahmen von 0 bis 255 befinden wie z.B. RGB-Werte im 24-Bit-Graphikmodus, ist er nach wie vor sinnvoll einsetzbar ActiveVB Tipp 635

Generelles Datenregister EDX (data i/o)
Portdaten (Drucker, Bildschirm usw.) können nur direkt (mit Portadresse < 256) oder über DX (nicht EDX!) ausgegeben/eingelesen werden (z.B. IN AL,DX). Als Destination für den IN-Befehl bzw. Source für den OUT-Befehl sind nur AL und AX erlaubt.

Generelles Datenregister ECX (counter)
Für das Counter-Register wurden einst extra Befehle entwickelt, die den Programmablauf beschleunigten. Der Befehl JECXcc bewirkt einen bedingten Sprung in Abhängigkeit vom Wert des ECX-Registers, ohne dass ein Compare-Befehl voranstehen muss. Beim LOOPcc-Befehl wird ECX dekrementiert und zur angegebenen Adresse verzweigt, wenn es nicht Null UND die in cc gegebene Bedingung erfüllt ist. Mit Einführung des Pentium wurden diese Befehle obsolet, da sie mehr CPU-Takte benötigen als ihre mehrzeiligen Entsprechungen (s. Tabelle in Kapitel II.7). Wichtiger ist, dass das Rechts- oder Linksverschieben von Registern um einen zu berechnenden Wert immer noch nur in Verbindung mit dem CL-Register möglich ist (z.B. SHL EAX,CL) und das Präfix REPcc (s. nächsten Punkt) stets die Verwendung des Counter-Registers ECX einschließt.

Indexregister ESI, EDI (source index, destination index)
Für diese Register stehen nach wie vor besondere Ein-Byte-Befehle14) zum Verschieben bzw. Durchsuchen von Speicherbereichen zur Verfügung:

LODSx AL (LODSB), AX (LODSW) oder EAX (LODSD) wird mit dem Wert aus dem durch DS:ESI indizierten Speicher geladen und ESI anschließend entsprechend um 1, 2 oder 4 erhöht.
STOSx AL (STOSB), AX (STOSW) oder EAX (STOSD) wird in dem durch ES:EDI indizierten Speicher abgelegt und EDI anschließend entsprechend um 1, 2 oder 4 erhöht.
SCASx AL (SCASB), AX (SCASW) oder EAX (SCASD) wird mit dem Wert in dem durch ES:EDI indizierten Speicher verglichen und EDI anschließend um 1, 2 oder 4 erhöht
MOVSx 1 (MOVSB), 2 (MOVSW) oder 4 (MOVSD) Bytes werden von DS:ESI nach ES:EDI kopiert und beide Indexregister anschließend entsprechend erhöht.

In dieser einfachen Form wurden auch diese Befehle durch die Struktur des Pentium obsolet (s. Tabelle in Kapitel II.7). Mit vorangestelltem Präfix (REPcc für repeat conditionally) sind sie jedoch weiterhin von Interesse. REPcc besagt, dass der Befehl ECX mal ausgeführt werden soll. Für die Verarbeitung des REP-Präfixes benötigt der Pentium einige CPU-Takte. A b einem ECX-Wert von etwa 4, d.h. mindestens 4 Such-, Füll- oder Kopieraktionen, ist die REP-Konstruktion jedoch schneller als ihre mehrzeilige Entsprechung. Demnach können große Speicherbereiche mit einem einzigen Befehl in Windeseile (1 Takt pro Durchlauf, das sind bei einem 2 GHz PC theoretisch bis zu 8 Gigabyte pro Sekunde) kopiert werden.

Indexregister ESP (stack pointer)
Stack (Stapel) bezeichnet eine Art Ablage. Mit dem Befehl PUSH können Register auf den Stapel gelegt und mit dem Befehl POP wieder geladen werden. Das funktioniert nach dem Last-In-First-Out-Prinzip (LIFO), d.h. der letzte mit PUSH auf den Stapel gelegte Wert wird mit dem nächsten POP-Befehl geladen, ganz gleich, ob es sich um dasselbe Zielregister handelt oder nicht. Da der 80x86 wenige Register bereitstellt, ist dies eine gute Möglichkeit, Werte zu sichern, danach zu bearbeiten, ggf. irgendwo zu speichern und dann durch Zurückholen vom Stapel wiederherzustellen. Und genau das wird von den Hochsprachen umgesetzt: Jede Prozedur, sei sie nun mit C oder VB kompiliert, beginnt damit, die Register auf dem Stapel abzulegen; dann werden diese den Prozeduranforderungen gemäß benutzt und vor dem Rücksprung zum aufrufenden Programm mit POP-Befehlen wiederhergestellt.

  • Der Stapel ist ein Speicher wie jeder andere auch und lässt sich ganz gewöhnlich durch mov, add o.ä. Befehle manipulieren. Es ist lediglich so, dass bestimmte Befehle (push, pop, call, ret u.a.) automatisch das ESP-Register als Indexregister nutzen.
  • Es können nur 32-Bit-Werte auf dem Stapel abgelegt und von ihm geladen werden!
  • Es können auch Direktwerte (PUSH 3) bzw. Indirektwerte (PUSH [EAX]) übergeben und vom Stapel geladen werden (POP [4711]).
  • Das ESP-Register, der Stapelzeiger, indiziert die Speicherstelle von der der nächste Wert mit POP geladen wird. Weist ESP bei Ausführung eines POP-Befehles z.B. auf die Adresse 41800000h, dann erhält das Zielregister den Wert aus [41800000h] und ESP wird um 4 auf 41800004 erhöht; bei einem PUSH-Befehl verhält es sich genau anders herum, d.h. zunächst wird ESP um 4 auf 417FFFFC erniedrigt und dann der Wert in dieser Adresse abgelegt .


Abbildung 2: Stapel


Abbildung 3: Push

Indexregister EBP (base pointer)
Besonders von Compilern wird der stack exzessiv für die Speicherung lokaler Variablen genutzt. Da der base pointer dem Stacksegment zugeordnet ist (SS:EBP), ist er bzw. war er zu 16-Bit-Zeiten ) besonders geeignet, um auf diese Werte zuzugreifen. Diese Vorgehensweise ist immer noch so verbreitet, dass der Pentium extra Befehle dafür bereitstellt (ENTER und LEAVE). Nach der üblichen Sicherung der Register sieht der Stapel daher wie unten beschrieben aus. Der typische Programmcode ist hier von unten nach oben dargestellt, damit deutlich wird, wie PUSH-Befehle den Stapel belegen..


Abbildung 4: Basepointer

Segmentregister DS, ES, FS, GS, SS
Segmentregister können nicht direkt (z.B. per MOV FS,3), sondern nur indirekt geladen werden: per MOV FS,EAX oder MOV FS,[EAX] oder POP FS

Segment-/Indexregister CS:IP (code segment/instruction pointer)
Codesement und instruction pointer weisen zusammen genommen auf den nächsten auszuführenden Befehl. Beide Register lassen sich nur über Sprungbefehle (CALL, JMP, INT) bzw. Rücksprungbefehle (RET, IRET) setzen.

Empfehlungen

Um Programm- und Computerabstürze zu vermeiden, hier einige Tipps zur Verwendung der Register:

Sichern der Register: Von VB aufgerufener ASM-Code sollte die Register EBP, EBX, ESI und EDI keinesfalls ändern bzw. vor Benutzung auf dem Stapel sichern und vor der Rückkehr zu VB wiederherstellen. Bei Unsicherheit darüber, welche Register zu sichern sind, gilt die Faustregel: Lieber eines zu viel als zu wenig.

Benutzung der Segmentregister Besonders der unerfahrene ASM-Programmierer sollte die Segmentregister möglichst nicht benutzen. Für sie gilt insbesondere das Wiederherstellungsgebot. Im FLAT-Modell führt das Ändern der Register DS, ES oder SS zu einem Speicher-Ausnahmefehler.

Benutzung von SS:ESP Nutzen Sie das ESP-Register ausschließlich als Stackpointer, nicht als Zwischenspeicher, denn dies kann zu unbeabsichtigtem Überschreiben von Speicherinhalten führen! Nutzen Sie den Stapel ausgiebig! z.B. in dem Sie gleich zu Anfang des Programms Platz für benötigte Variablen schaffen (SUB ESP,<anzahl>), aber achten Sie darauf, ESP vor der Rückkehr von Ihrer Funktion wieder auf den ursprünglichen Wert zu setzen (ADD ESP,<anzahl> oder LEA ESP,[EBP+/-<anzahl>]), um die geordnete Rückkehr von Ihrer Funktion zu gewährleisten.

Verwendung des $-Operators und von Offsets Die durch das Dollarzeichen bestimmte Adresse ist fix, d.h. sie wird bei der Assemblierung als Summe aus Programm-Startadresse und aktuellem Offset gebildet. Da in unserem Fall die Programm-Startadresse variabel ist, verwenden Sie anstelle von

MOV EAX,$ o.ä. besser folgenden Trick, wenn Sie die aktuelle Adresse benötigen:
CALL $+5 Verzweigt zur Adresse direkt hinter dem Call-Befehl (nächste Programmzeile) und legt die absolute Rücksprungadresse auf dem Stapel ab
POP EAX Lädt den obersten Wert des Stapels ins EAX-Register, so dass EAX die Adresse der aktuellen Programmzeile enthält

Soll ein Register auf Daten im Programmcode verweisen, muss ähnlich vorgegangen werden (EAX weist in diesem Beispiel auf Daten):


Abbildung 5: Verweise

Adressierung

Die Assembler-Programmierung besteht weitgehend aus dem Laden, Vergleichen/Verändern und Schreiben von Werten. Daher ist besonderes Augenmerk auf die Möglichkeiten zu legen, an Werte heranzukommen, ihre Speicherstelle (Adresse) zu bestimmen, kurz: sie zu adressieren. Der 80x86 hat nur wenige Register, diese können jedoch bei der Adressierung sehr flexibel, da kombiniert eingesetzt werden; im Einzelnen bis zu zwei Register und ein Offset, wobei das letztgenannte Register zusätzlich die Angabe eines Multiplikators erlaubt - besonders nützlich bei der Arbeit mit Wertetabellen:


Abbildung 6: Adressierung

  • reg bezeichnet ein 32-Bit-Register
  • x ist 2, 4 oder 8
  • Offset ist ein beliebiger vorzeichenbehafteter Integer
  • Die Kombination von Registern und Offsets bezeichnet stets eine Speicherstelle. Für den Fall, dass ein Register eine Adresse beinhaltet und ein anderes Register relativ zu dieser gesetzt werden soll, kann der Befehl LEA (load effective address) in derselben Weise wie der MOV (move) Befehl verwendet werden. Der Befehl LEA EAX,[ESI+3] lädt EAX mit dem Wert ESI+3 (nicht aus der Speicherstelle [ESI+3], wie die eckigen Klammern vermuten lassen - hier ist die Syntax ein wenig inkonsequent).
  • Die Möglichkeit, Multiplikatoren vorzugeben, kann ebenfalls genutzt werden, um einfache Multiplikationen mit Registern durchzuführen (hier wird EAX mit 10 multipliziert):SHL EAX,1 LEA EAX,[4*EAX+EAX].

Flags

Ein besonders wichtiges Register des 80x86, das Zustands- oder Flagregister, blieb bislang unerwähnt. Die Bits dieses Registers spiegeln diverse Zustände wider, die z.B. durch mathematische Operationen ausgelöst werden: Ergibt etwa die Addition zweier Werte einen Übertrag, d.h. war das Zielregister zu klein, um das Ergebnis aufzunehmen, so ist nach der Befehlsausführung das Übertagsflag (Carry-Flag oder Borrow-Flag) gesetzt, sprich: Bit 0 des Flagregisters ist 1; kommt Null heraus, so ist das Nullflag (Zero-Flag) gesetzt usw. Der Zustand eines Flags (1 oder 0) bleibt bis zur Ausführung eines Befehles erhalten, der ihn explizit (z.B. STC = set carry flag => das Übertragsflag wird ausdrücklich gesetzt) oder implizit (z.B. ADD EAX,3 => das Übertragsflag wird vom Additionsbefehl beeinflusst) ändert.

Die wichtigsten Flags


Abbildung 7: Flags

Etliche Befehle des 80x86 implizieren die Abfrage eines bestimmten Flags, zumeist des Carry-Flags, und dessen Status beeinflusst die Befehlsausführung bzw. das Resultat:


Abbildung 8: Befehle

Flags sichern, wiederherstellen und manipulieren

Einige Befehle ermöglichen das Sichern des Statusregisters:

PUSHF Sichert das gesamte Register auf dem Stack
LAHF Sichert 8 Bits des Statusregisters im Register AH
SETcc reg8 Setzt ein 8-Bit-Register entsprechend der Bedingung cc auf 0 oder 1

Andere dienen dazu, das Statusregister zu setzen/wiederherzustellen:

POPF Lädt das gesamte Register vom Stack
SAHF Setzt 8 Bits des Statusregisters entsprechend AH

Da es sich beim Flag-Register um ein "normales" Register handelt, bei dem lediglich den einzelnen Bits besondere Bedeutung zukommt, können die Stati prinzipiell auch "manuell" verändert werden:


Abbildung 9: Flags II

Befehlsschleifen-Konstruktionen

Flags und bedingte Sprungbefehle ermöglichen IF-Abfragen und For-Next-Konstruktionen:


Abbildung 10: Schleifen

MASM stellt für derartige Konstruktionen Direktiven und Pseudocodes zur Verfügung, die einerseits praktisch sind, andererseits den eingangs für Compiler beschriebenen Nachteil mit sich bringen, dass durch stupide Umsetzung überflüssige Codezeilen entstehen Direktiven

Empfehlungen, Hinweise

  • Flags sind äußerst wichtig zur Steuerung von Programmabläufen. Informieren Sie sich daher vor Verwendung eines Befehls genau darüber, welchen Einfluss er auf das Flagregister hat. Der Befehl ADD ECX,1 z.B. beeinflusst Sign-, Zero-, Parity- und Carryflag, während durch den Befehl INC ECX das Carryflag nicht gesetzt wird und der Befehl LEA ECX,[ECX+1] das Flagregister überhaupt nicht verändert. Alle drei Befehle haben aber hinsichtlich ECX dieselbe Wirkung: ECX = ECX+1.
  • 80x86-Befehle sind bis auf IMUL nicht vorzeichenbehaftet. Wenn Sie zum Wert 7FFFFFFFh eins addieren, wird nicht - wie bei VB - ein Überlauffehler gemeldet, sondern das Ergebnis ist 80000000h und das Vorzeichenflag gesetzt. Dasselbe gilt, wenn Sie eins zu 0FFFFFFFFh addieren: Das Ergebnis ist Null, das Vorzeichenflag gelöscht, das Carryflag gesetzt.
  • Obgleich 8/16-Bit-Register bloß den unteren Teil eines 32-Bit-Registers abbilden, werden die Flags bei ihrer Verwendung doch so gesetzt, als wären sie physisch verfügbar, d.h. ADD AL,255 setzt das Carryflag, wenn AL vor der Ausführung ungleich Null ist.
  • Sollten Sie beim Kopieren von Daten das Direction-Flag setzen müssen (STD), vergessen Sie nicht, es wieder zu löschen (CLD), denn als gewissermaßen ungeschriebenes Gesetz gilt: Das Directionflag ist nicht (!) gesetzt.
  • Probieren Sie nur Befehle aus, wenn Sie die Wirkung der betroffenen Flags kennen. "Spielen" Sie z.B. nicht mit den Befehlen CLI und STI, die die Ausführung maskierbarer Interrupts erlauben bzw. verhindern.
  • MASM bietet die Möglichkeit, Pseudo-Labels (@@) zu benutzen und per JMP @B (jump to begin -> springe zum Anfang) bzw. JMP @F (jump to finish -> springe zum Ende) dorthin zu verzweigen. Enthält das Programm mehrere Loops, rate ich dazu, diese Möglichkeit zu missachten und stattdessen aussagekräftige Labels (Loop1:, Loop2: usw.) zu benutzen, da dies die Lesbarkeit des Codes erheblicht verbessert.

Optimierung

Das Thema Optimierung ist seit Einführung des Pentium umfangreicher und komplizierter geworden, da im Pentium mehrere Einheiten doppelt vorhanden sind, was erstmalig parallele Befehlsbearbeitung (Lesen, Interpretieren, Ausführen) ermöglichte. Dadurch benötigen die meisten Befehle nur einen CPU-Taktzyklus. Allerdings sind das Zusammenspiel der Einheiten sowie Ausnahmeregelungen schwer nachvollziehbar. Als Faustregeln gelten:

  • Möglichst kurze Befehle (also z.B. INC ECX anstelle von ADD ECX,00000001)
  • Möglichst nicht direkt hintereinander dasselbe Register als Ziel und Herkunft benutzen, da dies zu "Strafzyklen" führt, denn genau wie in einer Datenbank in der Mehrplatzumgebung muss der Prozessor darauf achten, dass die parallele Bearbeitung von Befehlen nicht das Ergebnis verfälscht; zu diesem Zweck wird die parallele Verarbeitung mitunter unterbrochen.
  • Kurze Programmtexte sind nicht notwendig schneller als lange. Vor allem laufen etliche "Langversionen" eingebauter 80x86-Befehle, also ihre mehrzeiligen Entsprechungen, schneller als diese. Einige Beispiele:


Abbildung 11: Eingebaute Befehle

Beim letzten Beispiel sind linker und rechter Code jeweils einzeilig. Dennoch ist der rechts ausgewiesene schneller, da er parallel zu anderen Befehlen ablaufen kann, was für den NOT-Befehl nicht gilt. Siehe zum Thema Optimierung:
P5-Optimierung I
P5-Optimierung II
CPU-Takte

Fehlerbearbeitung (debugging)

Jedes Programm enthält Fehler! Im Englischen werden Programmfehler auch bugs (Wanzen) genannt; dementsprechend heißt der Vorgang, Fehler auszumerzen, debugging ("entwanzen"). In VB eingebundener Assemblercode lässt sich in VB selbst überhaupt nicht und mit dem Windows-Programm DEBUG nur schlecht prüfen ("debuggen"), da letzteres nur im 16-Bit-Modus arbeitet. Mit dem Visual Studio wird aber ein Debugger ausgeliefert, den anzusprechen relativ einfach ist: Debugger ermöglichen es, gleich der F8-Taste in VB, ASM-Programme schrittweise auszuführen und so Fehler aufzufinden. Sie nutzen dafür den von Intel festgelegten Debug-Interrupt (Interrupt 3). Wenn ein INT 3-Befehl an eine beliebige Stelle (i.d.R. am Anfang) des ASM-Codes eingesetzt und ausgeführt wird, meldet Windows einen vermeintlichen Fehler, und per Mausklick auf die Schaltfläche Debug wird der Debugger gestartet (das abgebildete Fenster erscheint in W98 SE; andere Windows-Versionen zeigen ähnliche Fenster).


Abbildung 12: Debugger

Der Befehlssatz des Debuggers soll hier nicht erläutert werden; er ist dem der IDE sehr ähnlich. Die wichtigsten Funktionen zum Auffinden von Fehlern sind

Alt-5 Anzeigen der Register. Registerwerte können einfach überschrieben werden
Alt-6 Hexeditor; In der obersten Zeile können sowohl Programmadressen als auch Registernamen eingetragen werden, um den entsprechenden Speicherabschnitt einzusehen. Die angezeigten Werte lassen sich einfach überschreiben. In geschützten Speicherbereichen werden anstelle der Hexwerte Fragezeichen angezeigt.
F10 / F11 Schrittweise Programmausführung; entsprechend den VB-Tasten Shift-F8/F8
Halt Haltepunkte, nächste auszuführende Zeile usw. werden genau wie in VB über das Kontextmenü eingestellt
F5 Programmausführung fortsetzen

In der Entwicklungsphase ist diese Test-Möglichkeit besonders wertvoll. Nach der Fehlerkorrektur kann entweder das Programm ohne den INT-3-Befehl neu assembliert oder INT-3 durch einen NOP-Befehl (no operation = "tu gar nichts") ersetzt werden.

Mithilfe der unten aufgeführten Routine EXE2ARR und des Debuggers kann

  • der Programmcode getestet
  • bei Auffinden eines Fehlers per "Nächsten Befehl setzen" plus F5 zu VB zurückgekehrt
  • die VB-Programmausführung angehalten
  • der ASM-Code geändert/neu assembliert
  • und gleich darauf erneut getestet werden,

ohne, dass VB geschlossen werden muss.

Nächste Seite >>
Kleine Helferlein
<< Vorherige Seite
Begriffsbestimmungen