Einzelnen Beitrag anzeigen

Balu der Bär
(Gast)

n/a Beiträge
 

Inline ASM für Win32 - Einsteiger Crashkurs

  Alt 7. Okt 2006, 14:34
Inline Assembler
mit Borland Delphi

Inhaltverzeichnis:
» 1. Einleitung
» 2. Assembler
2.1 Grundlagen Assembler
2.2 Grundlagen Assemblerbefehle
» 3. Inline Assembler mit Borland Delphi
3.1 Allgemeines
3.2 Funktionen und Prozeduren
3.3 Konstanten und Variablen
3.4 Bedingte Sprünge
3.5 Schleifen
3.6 Praktische Beispiele
» 4. Quellen und Links


1. Einleitung
Jeder Programmierer hat bereits davon gehört, wenn nicht sogar schon damit gearbeitet. Es ist klein, schnell und zuverlässig, ohne es könnte man in der Computerwelt wohl nicht leben. Die Rede ist von Assembler, der maschinenorientierten Programmiersprache. Als Ende der 40er Jahre die ersten EDV-Anlagen aus dem Boden schossen, hatte man es als Programmierer noch nicht sonderlich leicht. Diese Computer verstanden nur Binärcode, d.h. der Programmierer musste jede 0 und 1 manuell in die Maschine eingeben. Etwas später entwickelten sich dann die sogenannten Assemblersprachen, deren Befehlsvorrat speziell auf jeden Rechnertyp zugeschnitten wurde. Diese verwenden anstelle des Binärcodes leichter verständliche Symbole, Mnemonics genannt. Diese Symbole müssen, damit sie der Computer verstehen kann, erst in reinen Binärcode übersetzt werden, ein Vorgang den man auch assemblieren nennt. Assembler wird heutzutage meistens nur noch dort eingesetzt, wo Programme besonders schnell arbeiten und reagieren müssen, bei der Entwicklung eines Betriebssystemes oder beispielsweise bei der Treiberprogrammierung. Assembler ist in seinen Möglichkeiten gewissermaßen eingeschränkt bzw. unkomfortabel, meistens benutzt man es heutzutage nur noch für schnelle Berechnungen aus Hochsprachen heraus. Für jeden "handelsüblichen" Programmierer ist Assembler nicht gerade die erste Wahl unter den Programmiersprachen, da sogenannte Hochsprachen wie C, Java oder Delphi deutlich komfortabler sind. Vielleicht kommt an dieser Stelle die Frage auf, wieso ich dann ein kleines Assembler-Tutorial (oder auch einen Assembler-Crashkurs) schreibe. Diese Frage ist einfach zu beantworten: Aus reinem technischen Interesse heraus wollte ich wissen wie Assembler funktioniert, wie der Computer überhaupt funktioniert, jedenfalls teilweise. Da es eventuell auch für jemand anderen nützlich sein kann, habe ich mich entschlossen diesen kleinen Guide zu schreiben. Da Delphi meine favorisierte Programmiersprache ist und ich, um ehrlich zu sein, damals zu bequem war mir einen Assembler anzuschaffen, habe ich mich entschlossen auf Borland's Delphi zurückzugreifen. Der Win32-Compiler von Borland Delphi verfügt über den sogenannten integrierten Assembler, welcher es uns ermöglicht Assembler-Code direkt in Delphi Programme einzubauen. Das bringt natürlich einige besondere Funktionsmerkmale auf. An dieser Stelle möchte ich kurz die Delphi-Hilfe zitieren:
» Inline-Assemblierung
» Unterstützung aller Anweisungen in Intel Pentium 4, Intel MMX Extensions, Streaming SIMD Extensions (SSE) sowie AMD Athlon (einschließlich 3D Now!)
» Keine Makrounterstützung, ermöglicht jedoch reine Assembler-Prozeduren
» Verwendung von Delphi-Bezeichnern, wie etwa Konstanten, Typen und Variablen in Assembler-Anweisungen
Für alle Kritiker möchte ich noch einmal sagen, dass dieser Crashkurs wirklich nur ein Crashkurs ist. Ich kann und werde an dieser Stelle nicht alle theoretischen Merkmale der Assemblersprache ansprechen, und versuche dies auch bewusst zu vermeiden um mehr praktische Beispiele liefern zu können. Im Internet gibt es eine Menge deutsch- und englischsprachige Anleitungen zu Assembler, die wohl durchaus besser sein mögen als dieser Crashkurs. Da ich bisher aber noch nichts konkretes zu Assembler (ab sofort abgekürzt mit ASM) im Bezug auf Delphi gefunden habe, habe ich mich entschlossen, diesen Crashkurs zu schreiben.

2. Assembler
2.1 Grundlagen Assembler
Wie bereits vorhin angesprochen werde ich hier nur auf einige ASM-Grundlagen eingehen, und diese auch nur im Schnelldurchlauf abhandeln. Am Ende dieses Crashkurses findet Ihr einige Links, von denen Ihr noch mehr Informationen zu ASM beziehen könnt.
Jeder Prozessor (CPU) verfügt generell über ein sogenanntes Rechenwerk, welches dem Computer ermöglicht zu Rechnen, d.h. zu Addieren, Subtrahieren, Multiplizieren und Dividieren. Zusätzlich können natürlich auch logische Bedingungen erstellt und geprüft werden. Das Steuerwerk der CPU dekodiert die Grundbefehle die wir eingegeben haben und entscheidet was zu tun ist, es ist also ein weiterer wesentlicher Bestandteil jedes Computers. Jetzt bedarf es natürlich noch einiger Speicherplätze, wo wir Daten ablegen können und aus denen die CPU Daten wieder einlesen kann. Diese Speicherplätze nennt man Register. Jedes Register ist 32 Bit groß (Win32), es können also genau 32 Ziffern (0 oder 1) in einem Register abgelegt werden. Diesen Überbegriff Register unterteilt man in drei Untergruppen: Allgemeine Register: EDI, ESI, ESP, EBP, EBX (der Inhalt dieser Register muss während der Arbeit mit Inline ASM erhalten bleiben) und EAX, ECX, EDX (diese Register können beliebig verändert werden)
Segmentregister: CS (Codesegment), DS (Datasegment), SS (Stacksegment), ES (beliebig), FS (beliebig), GS (beliebig)
Sonstige: EIP (Instruction Pointer) und EF (Flags)
Als nächstes möchte ich hier den Stack erwähnen. Der Stack ist eine Art Schmierzettel, ein Teil des Hauptspeichers, in welchem nicht mit festen Adressen gearbeitet wird. Man kann dort Daten bei Bedarf zwischenspeichern und zu einem späteren Zeitpunkt wieder auslesen. Die Daten werden nicht von oben auf den Stack gelegt, sondern unten an den Stack angehängt. Der Stack wächst von oben nach unten. Generell sollte man die Register EAX, ECX und EDX immer sichern (siehe POP und PUSH in Kapitel 2.2). Zu guter Letzt möchte ich an dieser Stelle noch das Flag Register erwähnen. Das Flag-Register ist ein spezielles Register von 16 Bits, welche man Flags nennt. Jedes von ihnen hat eine Spezialaufgabe und kann mit einem Wert gefüllt werden. Hat eines den Wert 1, spricht man von einem gesetzten Bit, ist es 0, nennt man es gelöscht. Das soll für den Anfang an dieser Stelle reichen, eventuell werde ich weitere Grundlagen bei den praktischen Angelegenheiten noch einmal etwas näher erklären.

2.2 Grundlagen Assemblerbefehle
Bevor wir zur Arbeit mit Inline ASM unter Delphi kommen, möchte ich erst einmal auf einige Grundbefehle von Assembler eingehen. Auch hier sei wieder gesagt, ich beschränke mich auf die meiner Meinung nach wichtigsten Befehle und die, die im Rahmen dieses kurzen Crashkurses inhaltlich möglich sind.
Beginnen wir mit dem wahrscheinlich wichtigsten Befehl der Assemblersprache: MOV Ziel, Quelle Wie man sich vielleicht schon denken kann, MOV ist die Abkürzung für move (englisch). Die Übersetzung "bewegen" entspricht aber nicht ganz den Tatsachen in Assembler. Dieser Befehl kopiert lediglich Quelle nach Ziel, der Wert bleibt in Quelle also erhalten. Es muss unbedingt beachtet werden das Quelle und Ziel die gleiche Größe haben, sonst kann es zu Fehlermeldungen kommen. Als Quelle kommen Register, Speicherstellen oder Konstanten in Frage, als Ziel sind lediglich Register und Speicherstellen möglich.
XCHG Operand , Operand2 Dieser Befehl tauscht den Inhalt der Operanden gegeneinander aus. Wieder muss beachtet werden, dass beide Operanden die gleiche Größe haben müssen.
LEA Ziel, Quelle Der Befehl LEA läd die Speicheradresse der Quelle in das Ziel. Das Ziel muss ein 32 Bit Register sein und die Quelle einen Speicherwert beinhalten. LEA eignet sich aber auch zum einfachen Multiplizieren, welches man beispielswiese per LEA [EAX+EAX*2] // EAX:=EAX*3 machen könnte.
CMP Operand1, Operand2 Der Befehl CMP vergleicht Operand1 mit Operand2, wird z.B. häufig bei bedingten Sprüngen benutzt.
CALL Programm CALL ist der Aufruf eines Unterprogramms. Die Rückkehradresse wird auf dem Stack gespeichert, so dass eine Rückkehr mit dem Befehl RET möglich ist.
RET RET steht für Return; Rückkehr aus dem Unterprogramm und ist das Gegenstück zu CALL.
XOR Ziel, Quelle XOR ist einfach nur eine ganz logische Verknüpfung, die nichts anderes macht als eine
Antivalenz durchzuführen. So würde z.B. XOR EAX, EAX das Register EAX auf 0 setzen.
CDQ Dieser Befehl wird benötigt, um die beiden Register EAX und EDX auf die Multiplikation (IMUL) bzw. Division (IDIV) von signed Werten vorzubereiten, indem EAX auf 64 Bit erweitert wird.
PUSH Quelle Legt Quelle auf dem Stack ab, um es später eventuell wieder auszulesen (z.B. PUSH EAX).
POP Ziel Liest Ziel wieder vom Stack aus (z.B. POP EAX).
Kommen wir nun zu den 4 einfachen Grundrechenarten:
ADD Ziel, Quelle Zum ersten Operanden (Ziel) wird der zweite Operand (Quelle) addiert und in Ziel gespeichert. Beide Operanden müssen vom gleichem Datentyp sein.
SUB Ziel, Quelle Von dem erstem Operanden (Ziel) wird der zweite Operand (Quelle) subtrahiert. Beide Operanden müssen vom gleichem Datentyp sein.
IMUL Ziel, Quelle Der Befehl IMUL stellt eine vorzeichenbehaftete Multiplikation dar, das Gegenstück dazu ist MUL, welches ohne Vorzeichen arbeitet. Beide Operanden müssen vom gleichem Datentyp sein.
IDIV Divisor IDIV führt eine vorzeichenbeachtende Division durch, als Gegenstück führt DIV eine Division ohne Beachtung des Vorzeichens durch. Beide Operanden müssen auch hier vom gleichem Datentyp sein. Bei jeder 32 Bit Division dividiert man aber einen 64 Bit Wert, deshalb sollte vor jeder Division mit IDIV das Register EAX mittels des Befehls CDQ auf 64 Bit erweitert werden.
INC Ziel Das Inkrementieren sollte ja bereits aus jeder anderen Programmiersprache bekannt sein. Dass Ziel ist gleichzeitig auch die Quelle, der Operand wird genau um einen Wert erhöht. Alternativ kann man auch einfach ADD EAX,1 verwenden, was nach Intels Optimization Guidelines sogar schneller arbeitet.
DEC Ziel Das Dekrementieren ist das Gegenstück zum Inkrementieren, hier wird dem Operand genau der Wert 1 abgezogen. Alternativ kann man auch einfach SUB EAX,1 verwenden, was nach Intels Optimization Guidelines auch schneller arbeitet.
Zu guter Letzt möchte ich hier noch Sprünge erwähnen, da diese ebenso unverzichtbar sind.
JMP HierHin JMP ist ein sogenannter unbedingter Sprung, d.h. ohne irgendetwas auszuwerten wird direkt an die angegebene Stelle gesprungen.
Natürlich gibt es auch bedingte Sprünge. Diesen muss in jedem Falle ein Befehl vorausgehen, der die Flags ändert. Dieses Register wird dann ausgewertet und dann wird jeweils an die angegebenen Sprungmarken gesprungen. Weiteres zu diesen Sprüngen und Bedingungen findet man im Kapitel "Bedingte Sprünge". An dieser Stelle möchte ich lediglich alle möglichen Sprungbefehle auflisten.
Delphi-Quellcode:
(Ein eventuelles Vorzeichen wird ignoriert)
jne Springe wenn ungleich
je Springe wenn gleich
ja Springe wenn größer
jna Springe wenn nicht größer
jae Springe wenn größer oder gleich
jnae Springe wenn nicht größer oder gleich
jb Springe wenn kleiner
jnb Springe wenn nicht kleiner
jbe Springe wenn kleiner oder gleich
jnbe Springe wenn nicht kleiner oder gleich

(Ein eventuelles Vorzeichen wird beachtet)
jg Springe wenn größer
jng Springe wenn nicht größer
jge Springe wenn größer oder gleich
jnge Springe wenn nicht größer oder gleich
jl Springe wenn kleiner
jnl Springe wenn nicht kleiner
jle Springe wenn kleiner oder gleich
jnle Springe wenn nicht kleiner oder gleich

jmp Springe immer
jz Springe wenn 0
jnz Springe wenn nicht 0
jc Springe wenn Carriage-Flag gesetzt ist
jnc Springe wenn Carriage-Flag nicht gesetzt ist
jcxz Springe wenn CX = 0
jecxz Springe wenn ECX = 0
js Springe, wenn die letzte Operation ein negatives Ergebnis hatte
jns Springe, wenn die letzte Operation kein negatives Ergebnis hatte
jo Springe wenn Overflow Flag gesetzt ist
jno Springe wenn Overflow Flag nicht gesetzt ist
jp Springe wenn das Parity Flag gesetzt ist
jnp Springe wenn das Parity Flag nicht gesetzt ist
Das soll uns von den Befehlen vorerst reichen. Zugegeben, es sind wirklich nur eine Hand voll Befehle, die jedoch recht mächtig sein können und ich hoffe sie bieten einen Einstieg in die Programmierung mit Assembler. Jetzt wenden wir uns aber so langsam mal dem praktischen Teil zu.

3. Inline Assembler mit Borland Delphi
3.1 Allgemeines
Wie bereits angesprochen, versteht der Delphi Compiler für Win32 integrierten Assemblercode. Das heißt also, wir können Delphi- und Assemblercode mischen. Ob dies in manchen Fällen sinnvoll ist oder nicht lasse ich hier mal offen, wenn man aber zum Beispiel sehr schnell Primzahlen berechnen möchte, kann man wunderbar ASM in Delphi einbauen. Jetzt ist es jedoch nicht so, dass man einfach den ASM-Code in sein Programm einbauen kann, sondern man muss Delphi erst sagen das jetzt Assemblercode folgt. Dies geschieht über die Anweisung asm, danach folgt der Assemblercode und damit Delphi auch wieder weiß das der Assemblerabschnitt beendet ist muss noch ein end; folgen. Genauer gesagt sieht das ganze dann so aus:
Delphi-Quellcode:
asm
  // Assemblercode
end;
Generell ist es von Vorteil, den Delphi- und ASM-Code aber strikt voneinander zu trennen. Allein schon der Übersichtlichkeit halber ist es daher besser, den Assemblercode in eine Prozedur oder Funktion auszulagern. Bevor es jetzt los geht noch ein paar einzelne Hinweise: Ich benutze in diesem Kurs Turbo Delphi Explorer für Win32, da es den integrierten Assembler aber bereits in jeder Delphi-Version gab, sollte es zu keinen Unterschieden zwischen einzelnen Delphi-Versionen kommen (Delphi 1 für 16 Bit mal ausgeschlossen). Auch Kommentare sind im Assemblercode genauso wie in Delphi möglich. Einzige Bedingung ist, dass die Kommentare erst hinter der Assembler-Anweisung kommen dürfen, ansonsten sind diese wie gewohnt z.B. mit // Kommentar oder { Kommentar } möglich. Als Letztes sei gesagt, dass ich in diesem Kurs alle Assemblerbefehle groß schreibe (z.B. MOV). Dies habe ich mir so angewöhnt, natürlich akzeptiert der Compiler auch kleingeschriebenen Code.

3.2 Funktionen und Prozeduren
Funktionen und Prozeduren kennt man ja schon aus Delphi oder anderen Programmiersprachen. Im folgenden wollen wir jetzt einmal unsere erste Assemblerfunktion schreiben. Dazu gehen wir vor wie in Delphi. Das Wort function leitet die Funktion ein, danach kommt der gewünschte Funktionsname, etwaige Variablen und schließlich der Rückgabewert der Funktion. Im Gegensatz zu Delphi schreiben wir jetzt aber kein begin und end;, sondern asm und end;. Das ganze sieht dann zum Beispiel so aus:
Delphi-Quellcode:
function Addiere(X, Y : Integer) : Integer;
asm
  ADD EAX, EDX
end;
Was genau mach diese Funktion jetzt? Man ruft die Funktion auf, übergibt die beiden Werte X und Y und die Funktion gibt die Summe der beiden Zahlen zurück. Wie das ganze jetzt funktioniert, dazu bedarf es einiger Erklärungen: Die Funktion Add kennen wir ja bereits, der Wert aus dem zweiten Operanden wird zu dem ersten Operanden addiert. Jetzt mag sicher mancher fragen, wie und warum kommen die Werte X und Y jetzt in die Register EAX und EDX? Die Antwort is recht simpel, dafür ist Delphi zuständig. Die Variablen aus der Funktionsdeklaration werden beim Funktionsaufruf in die Register geschrieben. Dabei wird zuerst das Register EAX, dann EDX und schließlich ECX bedient. Weitere Variablen werden auf dem Stack abgelegt. Die folgenden Beispiele sollen dies verdeutlichen:
Delphi-Quellcode:
function DoIt(var a : Cardinal) : Boolean; {[EAX] = a}
function DoIt1(var a, b : Cardinal) : Boolean; {[EAX] = a | [EDX] = b}
function DoIt2(a, b, c : Integer) : Boolean; {EAX = a | EDX = b | ECX = c}
Soweit sogut. Die Variablen werden also in die Register geschrieben, danach werden sie addiert und das Ergebnis in EAX gespeichert. Bloß wo wird jetzt der Rückgabewert der Funktion noch definiert? Wie man sieht, nirgendwo. Da die Funktion vom Typ Integer ist, steht der Rückgabewert automatisch in EAX und wird auch automatisch zurückgegeben. Sollte man sich wider Erwarten doch nicht darauf verlassen, dass das Ein- und Auslesen der Variablenwerte automatisch abläuft, so könnte man es auch folgendermaßen manuell machen (dieser Weg ist komplett sinnlos, aber eine gute Möglichkeit hier noch das Arbeiten von Assembler zu verdeutlichen).
Delphi-Quellcode:
function Addiere(X, Y : Integer) : Integer;
asm
  MOV EAX, X // Lese X in EAX ein
  MOV EDX, Y // Lese Y in EDX ein
  ADD EAX, EDX // Addiere
  MOV @Result, EAX // Setze EAX als Rückgabewert
end;
In Delphi könnte man diese Funktion dann zum Beispiel per ShowMessage(IntToStr(Addiere(4, 9))); aufrufen und man würde 13 als Ergebnis erhalten. Bei Prozeduren ist die Handhabung natürlich genauso, lediglich haben diese keinen Rückgabewert.
Selbstverständlich könnten wir ASM-Code auch in eine Delphi-Funktion (oder Prozedur) integrieren, nur der Vollständigkeit halber möchte ich dies hier nochmals aufzeigen:
Delphi-Quellcode:
function Beispiel : Integer;
var i : Integer;
begin
 i := i + 9;
 asm
  // ASM-Befehle
 end;
 result := i;
end;
Wie bereits angesprochen, können wir mittels CALL Unterprogramme oder Funktionen aufrufen. Dies können direkte Assembler- oder Betriebssystem-Funktionen sein, oder auch einfache Funktionen die wir in unserem Delphi-Programm bereits deklariert haben. Ein Beispiel für einen einfachen CALL wäre zum Beispiel:
Delphi-Quellcode:
asm
  CALL ExitProcess
end;
Die Funktion ExitProcess wird also aufgerufen, welche nichts anderes macht als das Programm zu beenden. Zum Ende dieses Kapitels noch ein Beispielcode wie wir relativ einfach eine Delphi-Funktion aus ASM heraus aufrufen können:
Delphi-Quellcode:
procedure Callme;
begin
  ShowMessage('Call me');
end;

procedure TForm2.Button2Click(Sender: TObject);
begin
 asm
  CALL Callme
 end;
end;
3.3 Konstanten und Variablen
Ohne Variablen und Konstanten hätte man viel Mühe zu arbeiten. Wie wir bereits wissen, könnte man in Assembler Werte zwar direkt in den Registern oder auf dem Stack zwischenspeichern, ganz ohne Variablen kann man aber wahrscheinlich doch nicht leben. Das Gute an Inline Assembler in Delphi ist, dass wir direkt auf die bereits in Delphi deklarierten Variablen zugreifen und mit ihnen arbeiten können. Wir müssen nur besonders stark darauf achten, dass die Variablen von ihrer Größe und ihrem Typ zu den im Assemblercode verwendeten Befehlen passt. Hier ein kleines Beispiel, wie man eine Addition von zwei Zahlen noch (aber durchaus umständlicher) in ASM realisieren könnte:
Delphi-Quellcode:
procedure TForm1.Button1Click(Sender: TObject);
var
 zahl1, zahl2, ergebnis : Integer;
begin
  zahl1 := 5;
  zahl2 := 8;
  asm
   PUSH EAX
   MOV EAX, zahl1
   ADD EAX, zahl2
   MOV ergebnis, EAX
   POP EAX
  end;
  ShowMessage(IntToStr(ergebnis));
end;
Gehen wir kurz durch, was hier gemacht wurde. Wir befinden uns in einem ButtonClick-Ereignis in einem Delphi-Projekt. Es werden die 3 Variablen zahl1, zahl2 und ergebnis erstellt, und im nächsten Atemzug werden den beiden Variablen zahl1 und zahl2 beliebige Werte zugewiesen. Jetzt springen wir in den Assemblercode. Per PUSH EAX sichern wir EAX auf dem Stack, falls EAX nicht leer sein sollte und wir diesen Wert später noch einmal brauchen sollten. Mittels MOV EAX, zahl1 schreiben wir den Wert (in diesem Beispiel die Zahl 5) aus zahl1 in das Register EAX hinein. Per ADD EAX, zahl2 wird jetzt wie im ersten Beispiel, eine einfache Addition durchgeführt. Dabei wird der Wert aus zahl2 eingelesen, zu EAX addiert und das Ergebnis natürlich wieder in EAX gespeichert. In der letzten ASM-Codezeile schreiben wir jetzt das Ergebnis der Addition aus EAX in die Variable ergebnis. Jetzt lesen wir per POP EAX EAX wieder vom Stack, da wir es ja vorher gesichert hatten. Diese Variable ergebnis lassen wir, wieder angekommen im Delphi-Code, dann mittels einer MessageBox ausgeben. Und mehr ist es eigentlich nicht. Durch den Inline-Assembler können wir auch im Assemblercode so einfach auf Variablen zugreifen wie auch in Delphi selbst. Bei Konstanten geht dies natürlich genauso leicht, man muss nur beachten das diesen selbstverständlicherweise keine Werte zugewiesen werden können.
Was machen wir aber, wenn wir selbst im Assemblercode neue Variablen erstellen wollen? Der Inline Assembler von Delphi verfügt über die folgenden Datentypen:
Delphi-Quellcode:
Typ Bezeichnung Bits Wertebereich
DB Byte/Char 8 -128 bis +127 / 0 bis 255
DW Word 16 -32768 bis +32767 / 0 bis 65535
DD DoubleWord 32 -2.147.483.648 bis +2.147.483.647 / 0 bis 2^31
DQ QuadWord 64 -2^63 bis +2^63-1 / 0 bis 2^64-1
Der integrierte Assembler unterstützt diese Variablendeklarationen jedoch nicht. Die einzige Möglichkeit, wie wir doch noch Variablen in ASM realisieren können, ist über Labels. Man könnte auch behaupten ein Label ist wie ein Lesezeichen. Man setzt es irgendwo in den Programmcode, und kann dann sofort und ohne Probleme zu dieser Stelle springen. Wir müssen also sozusagen die Variable hinter einem Label verstecken. Folgende Beispielfunktion soll das verdeutlichen.
Delphi-Quellcode:
function ShowText : PChar;
asm
  JMP @start
  @test: DB 'Hallo Du!', 0
  @start: LEA EAX, @test
  RET
end;
Nehmen wir den Code mal auseinander: Gleich in der ersten Zeile springen wir mittels JMP sofort zum Label @start. Die zweite Zeile, in der wir das Label @test, hinter welcher sich die Variable versteckt, erstellt haben, wird also sofort übersprungen. Die dritte Zeile ist schon die Sprungmarke von @start, d.h. genau hierher sind wir durch die erste Codezeile gesprungen. Dort wird nun der Code weiter ausgeführt, es wird als die Adresse von @test nach EAX geschrieben und somit als Rückgabewert der Funktion vorbereitet, Mittels RET (return) kehren wir nun wieder zum Delphi-Programmcode zurück. Hier noch zwei Hinweise: Anstatt String wie wir es von Delphi her gewohnt sind muss man in ASM immer PChar nehmen, da der Typ String im Assembler noch nicht vorhanden ist und auch nicht benötigt wird. Ein Label wird immer durch das @-Zeichen eingeleitet, dadurch kann man Labels auch schneller finden und erkennen (z.B. bei längeren ASM-Quelltexten).

3.4 Bedingte Sprünge
Auf Sprünge bin ich ja bereits im Kapitel 2.2 eher kurz eingegangen. Mittels JMP kann man direkt z.B. zu einem Label springen. Nun gibt es noch bedingte Sprünge (Auflistung im Kapitel 2.2). Diese ermöglichen es uns unter anderem, aus Delphi bekannte if .. then .. - Bedingungen auch in Assembler umzusetzen. Dies geht in ASM natürlich nicht so schnell und leicht wie in Delphi, ähnelt sich aber stark. Im nachfolgenden erst einmal eine Beispielbedingung in Delphi, danach diese Bedingung in Assembler.
Delphi-Quellcode:
function GroesserOderKleiner(X, Y : Integer) : String;
begin
  if x < y then result := 'X kleiner als Y'
   else result := 'X größer als Y';
end;
Und jetzt das ganze in Assembler (zum besseren Verständnis Kommentare hinzugefügt):
Delphi-Quellcode:
function GroesserOderKleiner(X, Y : Integer) : PChar;
asm
  JMP @start // Springe zu @start
  @groesser: DB 'X groesser als Y', 0 // Variable groesser wird erstellt
  @kleiner: DB 'X kleiner als Y', 0 // Variable kleiner wird erstellt
  @start: CMP EAX, EDX // Vergleiche EAX (X) mit EDX (Y)
  JA @IsBigger // Wenn EAX größer springe zu @IsBigger
  LEA EAX, @kleiner // Sonst gib @kleiner aus
  RET
  @IsBigger: LEA EAX, @groesser //gib @groesser aus
end;
Die Delphi-Funktion ist schnell erklärt. Es wird geprüft ob X kleiner als Y ist, wenn dem so ist wird die entsprechende Meldung ausgegeben, wenn nicht wird ausgeben, dass X größer als Y ist. Bei unserer Assembler-Funktion ist das ganze schon einen Tick komplizierter. Unsere erste Codezeile springt zum Label @start. In den nächsten zwei Zeilen erstellen wir die beiden Variablen, die unseren Ausgabetext enthalten. Mittels CMP EAX, EDX vergleichen wir jetzt die beiden Register (also X und Y) miteinander. JA @IsBigger ist nun der bedingte Sprung. JA (Springe wenn größer) springt wenn X größer ist als Y zum Label @IsBigger (welches den @groesser-Text ausgibt), ist X nicht größer als Y wird der @kleiner-Text ausgegeben. So kann man doch recht einfach auch mit Assembler Bedingungen prüfen und entsprechend handeln.

3.5 Schleifen
Selbstverständlich sind auch Schleifen mit Assembler möglich. Folgendes Beispiel zeigt eine einfache for-Schleife, wie wir sie auch aus Delphi kennen.
Delphi-Quellcode:
function forSchleife : Integer;
asm
  XOR EAX, EAX
  MOV ECX, 100
  @Schleife: INC EAX
             LOOP @Schleife
end;
Wenn Ihr halbwegs verstanden habt was ich versuche euch zu vermitteln, müsstet Ihr eigentlich selbst darauf kommen, was hier passiert: Als erstes setzen wir mittels XOR EAX, EAX das Register auf 0, leeren es sozusagen. In der nächsten Zeile weisen wir ECX den Wert 100 zu, dass ist sozusagen unsere Zählvariable. Danach kommt das Label @Schleife. An dieser Stelle inkrementieren wir EAX um 1. Jetzt kommt der neue Befehl, LOOP. Dieser dekrementiert ECX bei jedem Durchlauf um 1, dass heißt solange ECX noch nicht 0 ist, springt LOOP wieder zu @Schleife und der Vorgang wiederholt sich. Erst wenn ECX 0 ist, die Schleife also 100 mal durchgelaufen wurde und EAX daher den Wert 100 hat, ist die Schleife abgeschlossen.

3.6 Praktische Beispiele
An dieser Stelle sind wir fast am Ende unseres kleinen Crashkurses angelangt. Hier möchte ich nur nochmal ein paar weitere praktische Beispiele im Umgang mit Inline Assembler nennen.
So könnte man ebenfalls eine Case-Bedingung relativ leicht umsetzen.
Delphi-Quellcode:
function CaseAnweisung(X : Integer) : Integer;
asm
  CMP X, 1 // Vergleiche mit 1
  JE @calla // Wenn gleich springe zu @calla
  CMP X, 2 // Vergleiche mit 2
  JE @callb // Wenn gleich springe zu @callb
  CMP X, 3
  JE @callc
  JMP @end // Springe zu @end wenn nichts zutrifft
  
  @calla: MOV EAX, 100 // Wenn X=1 mache dies
          RET
  @callb: MOV EAX, 200 // Wenn X=2 mache das
          RET
  @callc: MOV EAX, 300 // Wenn X=3 mache jenes
          RET
  @end: MOV EAX, 500 // Wenn nicht (X>0) and (X&lt;4) mache ganz was anderes (else)
end;
Und als letztes noch kurz ein Quellcode für eine einfache Division in Assembler.
Delphi-Quellcode:
procedure TForm2.Button1Click(Sender: TObject);
var test : Integer;
begin
  test := 22; // test wird der Wert 22 zugewiesen
  asm
   MOV EAX, test // schreibe test in EAX
   CDQ // EAX wird auf 64 Bit erweitert
   MOV ECX, 5 // schreibe 5 in ECX
   IDIV ECX // dividiere
   MOV test, EAX // schreibe Ergebnis zurück nach test
  end;
 ShowMessage(IntTosTr(test));
end;
4. Quellen und Links
An dieser Stelle möchte ich mich bei allen Leuten bedanken die mir beim Erstellen dieses Tutorials und Erwerben meiner ASM-Kenntnisse geholfen haben. Spezieller Dank geht hierbei an Dax, der/die/das mir hilfreich zur Seite stand, und an Meflin, der sich meiner Rechtschreibung und Ausdruck annahm (obwohl so schlimm war es gar nicht ). Im Nachfolgenden findet Ihr noch einige Links zu guten Assembler Tutorials und Anleitungen, die immer einen Blick wert sind:

8086-Assembler (Assemblerauswahlseite)
Assemblertutorials für x86-Prozessoren
Assembler - Befehlsverzeichnis
Assembler Tutorial
Inline-Assembler Doku von Borland


Abschließend sei gesagt, dass sich dieses Tutorial an Einsteiger richtet, die ein bisschen ASM-Luft schnuppern möchten. Bei Anregungen, Kritik oder Lob könnt Ihr euch einfach bei mir melden. Vielen Dank für die Aufmerksamkeit.

Datum: 07.10.2006
Version: 1.1

Edit: Hinweise und Anmerkungen von Amateurprofi in das Tutorial (Downloadversion ebenfalls aktualisiert) eingefügt. Vielen Dank.

[edit=Phoenix]Link zur Assembler-Doku von Borland auf anfrage von Balu eingefügt. Mfg, Phoenix[/edit]
Angehängte Dateien
Dateityp: rar inline_assembler_win32_333.rar (9,5 KB, 178x aufgerufen)
  Mit Zitat antworten Zitat