AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Tutorials Delphi Inline ASM für Win32 - Einsteiger Crashkurs
Tutorial durchsuchen
Ansicht
Themen-Optionen

Inline ASM für Win32 - Einsteiger Crashkurs

Ein Tutorial von Balu der Bär · begonnen am 7. Okt 2006 · letzter Beitrag vom 1. Mai 2007
Antwort Antwort
Seite 1 von 2  1 2      
Balu der Bär
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)
 
Balu der Bär
 
#2
  Alt 7. Okt 2006, 15:56
Muetze1 merkte per PN an, dass für Anfänger das Arbeiten mit Sprüngen unter Umständen leichter sein könnte wenn sie die Abkürzungen der Sprünge selbst bilden können. Vielen Dank für diesen Hinweis, ich setze ihn an dieser Stelle um.

Die Abkürzungen sind natürlich logisch aufgebaut und leicht nachzubilden: j steht daher immer für jump, e für equal (gleich), a für above (größer), g für greater (größer), b für below (kleiner) und so weiter. Eine komplett englische Liste findet Ihr hier:
Code:
jne             jump if not equal
je              jump if equal
ja              jump if above
jna             jump if not above
jae             jump if above or equal
jnae            jump if not above or equal
jb              jump if below
jnb             jump if not below
jbe             jump if below or equal
jnbe            jump if not below or equal
jg              jump if greater
jng             jump if not greater
jge             jump if greater or equal
jnge            jump if not greater or equal
jl              jump if less
jnl             jump if not less
jle             jump if less or equal
jnle            jump if not less or equal
jmp             jump directly to
jz              jump if zero
jnz             jump if not zero
jc              jump if carry
jnc             jump if not carry
jcxz            jump if CX = 0 
jecxz           jump if ECX = 0 
js              jump if sign
jns             jump if not sign
jo              jump if overlow
jno             jump if not overflow
jp              jump if parity
jnp             jump if not parity
  Mit Zitat antworten Zitat
Olli
 
#3
  Alt 30. Dez 2006, 22:33
Zwei Anmerkungen:
1. Die Intel-Doku gibt es kostenlos ins Haus geliefert (Paperback).
2. CMP ist das gleiche wie SUB, nur daß das Ergebnis verworfen wird.
  Mit Zitat antworten Zitat
Benutzerbild von sirius
sirius

 
Delphi 7 Enterprise
 
#4
  Alt 5. Feb 2007, 12:45
Schönes Tut
(Ich bin nur zufällig durch deine Signatur hierher gekommen. Manchmal ist man aber auch neugierig...)

Was einen Anfänger verwirren könnte ist folgende Zeile:
Delphi-Quellcode:
function ShowText : PChar;
asm
  JMP @start
  @test: DB 'Hallo Du!', 0
  @start: LEA EAX, @test
  RET
end;
Du sprichst davon, dass hinter dem Label test eine Variable steht. Das könnte für Frustration sorgen, da die Speicherseiten in die der Code am Anfang geladen werden, "nur" mit 'Execute and Read' ausgestattet sind. Demnach kann man da nicht speichern und demnach ist es keine Variable sondern eine Konstante. Stört in deiner Funktion ja auch nicht weiter, denn du greifst ja nicht schreibend auf diese Speicherplätze zu.
Aber der eine oder andere könnte ja auf die Idee kommen, auf diese Art lokale Variablen anzulegen. Und das klappt nicht (==>Frustration).


Also müsste man (insofern du überhaupt willst) etwas über lokale Variablen erzählen (EBP+Stack)


Oder, man holt ganz weit aus (da ich im Erklären/Tut schreiben nicht so gut bin, nehme ich lieber gleich Code):
Delphi-Quellcode:
var MBI: TMemoryBasicInformation;
    tmp:integer;
    memory:pointer;
begin
  memory:=@Showtext;
  VirtualQuery(memory, MBI, SizeOf(MBI));
  Virtualprotect(memory,mbi.RegionSize,page_execute_readWRITE,@tmp);
...und verändert einfach die Zugriffsrechte auf die Speicherseiten.
Jetzt könnte man mit "mov byte ptr [eax],64" aus Dem "H" von Hallo ein "A" machen.


Oder man legt die Funktion in einen neu alloziierten Speicherbereich und tobt sich da aus (inkl. selbst modifiziertem code)

Delphi-Quellcode:
program Pasm;

{$APPTYPE CONSOLE}

uses windows;


//Die Assembler-Routine mit einer statischen/lokalen Variable hinter @1
//Hier wird der Parameter c entsprechend mit add (oder sub) zur statischen Variable @1 gerechnet und zurückgegeben
function testasm(c:integer):integer;
asm
     jmp @2 //Variable überspringen

 @1: dd 10 //unsere 32bit-Variable (static)

 @2: call @3 //Addresse von @1 in edx speichern
 @3: pop edx
     sub edx,9

     add eax,[edx] //@1 zu c addieren

     //Speichern des Ergebnisses in @1
     mov [edx],eax //normalerweise: EAccessViolation;
                    //aber wir dürfen ja in unserem eigenen Speicherbereich (siehe VirtualAlloc)schreiben
     
     {Hier wäre eigentlich soweit Schluss, aber man kann ja auch noch etwas rumspielen ;-)
      und mal einen Befehl verändern}

     // verändern von "add eax,[edx]" zu "sub eax,[edx]"
     // =>verändern von $0302 zu $2B02
     //und natürlich auch wieder zurück
     add edx,13 //Addresse auf den add-Befehl errechnen
     cmp byte ptr [edx],$03 //gucken, was drinn ist
     je @4
     mov byte ptr [edx],$03 //und ändern
     ret
 @4: mov byte ptr [edx],$2b //oder eben wieder zurückändern
end;

//wird als Referenz benötigt um "size" zu bestimmen
procedure dispatch;
asm
  nop
end;

var memory:pointer;
    size:cardinal;
    result:integer;
    i:integer;
begin

  //function testasm in eine neue Speicherseite kopieren -->memory
  size:=cardinal(@dispatch)-cardinal(@testasm); //Größe der Funktion testasm ermitteln
  memory:=Virtualalloc(nil,size,MEM_COMMIT,PAGE_EXECUTE_READWRITE); //Ich will auf meinem neuen Speicherbereich (memory)
                                                                    //ausführen,lesen und schreiben
  movememory(memory,@testasm,size); //copy

  //kleine Test-Schleife
  for i:=1 to 10 do begin
    asm
      mov eax,i
      call memory
      mov result,eax
    end;
    writeln(result);
  end;

  //folgende Zeile würde hier so nicht funktionieren
  //writeln(testasm(2));

  virtualfree(memory,size,MEM_DECOMMIT);
  readln;
end.

Es reicht fürs erste sicherlich aus dem Wort Variable in deinem Tut "Konstante" zu machen Für die Einführung in asm würde alles weitere nur verwirren.


Edit: Wie langweilig, wäre doch das Leben ohne Fehler; Ich habe mal eine sinnlose Variable aus dem zweiten CodeSchnippsel von mir entfernt.
Edit2: Fehler über Fehler (im code zum ändern von add und sub)
  Mit Zitat antworten Zitat
Benutzerbild von himitsu
himitsu

 
Delphi 12 Athens
 
#5
  Alt 5. Feb 2007, 13:00
Zitat von sirius:
Was einen Anfänger verwirren könnte ist folgende Zeile:
Delphi-Quellcode:
function ShowText : PChar;
asm
  JMP @start
  @test: DB 'Hallo Du!', 0
  @start: LEA EAX, @test
  RET
end;
welche der Zeilen?

außerdem wäre dieses wohl etwas besser, abgesehn von dem unnötigem Jump müssen die daten ja nich mitten im Code rumliegen (selbst Delphi legt seine Daten nach den Funktionen/Prozeduren ab)
Delphi-Quellcode:
function ShowText : PChar;
asm
  LEA EAX, @test
  RET
  @test: DB 'Hallo Du!', 0
end;
  Mit Zitat antworten Zitat
Benutzerbild von sirius
sirius

 
Delphi 7 Enterprise
 
#6
  Alt 5. Feb 2007, 13:04
Zitat von himitsu:
welche der Zeilen?
Die davor bzw. dahinter im Originaltext. Ja, da ist mein Auszug auch etwas verwirrend
Aber ich dachte meine Erklärung reicht.
Denn wenn man "showtext" so in ein Programm einfügt (ohne das Programm entspr. meiner Anmerkung zu modifizieren), ist bei "Hallo Du" keine Variable sondern eine Konstante.

Edit: Bei deiner Variante (Variable/Konstante hinten) muss man allerdings auf "RET" aufpassen (Anzahl der Übergabeparameter auf dem Stack).
  Mit Zitat antworten Zitat
Benutzerbild von himitsu
himitsu

 
Delphi 12 Athens
 
#7
  Alt 5. Feb 2007, 13:32
Na dann

Zitat von sirius:
Edit: Bei deiner Variante (Variable/Konstante hinten) muss man allerdings auf "RET" aufpassen (Anzahl der Übergabeparameter auf dem Stack).
Muß man bei der "anderen" Variante auch, denn dort wird auch RET aufgerufen :zwinker;

In diesem Fall blieben dann ja noch diese Möglichkeiten (ohne RET) ^^
Delphi-Quellcode:
function ShowText : PChar;
asm
  JMP @start
  @test: DB 'Hallo Du!', 0
  @start: LEA EAX, @test
end;

function ShowText : PChar;
asm
  LEA EAX, @test
  JMP @exit

  @test: DB 'Hallo Du!', 0
  @exit;
end;
Davon abgesehn das man eh aufpassen muß, daß RESULT auch wirklich in EAX liegt und nicht bei ASM auf den Stack ausgelagert und bei END erst nach EAX kopiert wird...

Man muß also so, oder so aufpassen
  Mit Zitat antworten Zitat
dino

 
Delphi 5 Professional
 
#8
  Alt 2. Apr 2007, 19:18
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;
da ich 22 durch 5 für komisch hielt, hab ichs gleich mal ausprobiert und kriegte sogar etwas noch komischereres raus: idiv führt scheinbar folgende operationen aus: dec ecx;mov eax,ecx
  Mit Zitat antworten Zitat
Benutzerbild von sirius
sirius

 
Delphi 7 Enterprise
 
#9
  Alt 2. Apr 2007, 19:53
Du hast ganz normal 22 div 5 ausgeführt. Und das ergibt 4 (Rest 2). Der Rest, also die 2, steht in EDX. Dafür machst du ja das CDQ.
Falls du am Ende Single, oder Double haben willst, musst du über dir FPU gehen.

Variante über FPU:
Delphi-Quellcode:
procedure TForm1.Button1Click(Sender: TObject);
var test : Integer;
    erg:double;
begin
  test := 22; // test wird der Wert 22 zugewiesen
  asm
    fild dword ptr [test] //32bit-Integer Wert aus Test in FPU laden
    push 5 //5 auf den Stack legen
    fild dword ptr [esp] //32bit-Integer-Wert am ende des Stacks in FPU laden
    pop eax //Stack bereinigen
    fdiv //Division in FPU (der beiden zuletzt geladenen Werte)
    fstp qword ptr [erg] //Ergebnis als 64bit Fließkommawert (=double) in erg speichern
  end;
  ShowMessage(floatTosTr(erg)); //erg = 4,4
end;
  Mit Zitat antworten Zitat
dino

 
Delphi 5 Professional
 
#10
  Alt 2. Apr 2007, 20:03
oh nein, ja natürlich stimmt, danke

stand wohl gerade ziehmlich auf den schlauch
  Mit Zitat antworten Zitat
Antwort Antwort
Seite 1 von 2  1 2      


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 04:42 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz