Tutorial AutoIt und Assembler UPDATE 24. Oktober 2010 Verwendung von Autoitvariablen im Assemblercode

  • Tutorial AutoIt und Assembler


    //EDIT 11/03/2011
    AssembleIt als Hilfsmittel für das Programmieren mit dem Embedded-FASM-Assembler nun inclusive Debugger! autoit.de/wcf/attachment/12819/


    //EDIT 19/10/2010
    _AssembleIt() etwas angepasst, bitte Beispiele beachten! LÄUFT NICHT mit den hier im Tut gezeigten ASM-codes, da _AssembleIt() nun den Stack selbst aufräumt! Daher ist als Rücksprung zu AutoIt NUR EIN EINFACHES RET nötig! Also kein RET 8, um 2 Integer vom Stack zu räumen! Die hier im Tut vorgestellten Beispiele sind natürlich lauffähig.autoit.de/wcf/attachment/11553/(ohne Debugger)


    Die Anpassung der bisherigen Beispiele habe ich vor, ggf gibts von euch weitere Ideen, was und wie man das "Handling" weiter verbessern könnte.
    Links für Tutorials hinzugefügt im Abschnitt "Was muss ich an Vorwissen haben?"


    Beispiele:
    Beispiel_1: Addition von 2 Integer-Zahlen
    Beispiel_2: Pixelsuchen32, findet ein Pixel innerhalb einer Bitmap
    Beispiel_3 Verwenden eigener Variablennamen
    Einfache Schleifen, der LOOP
    Beispiel Stringbehandlung und Pointer auf AutoIt-Variablen
    Aufruf von "Funktionen" mit CALL-Befehl, Beispiel Rekursion Fakultät
    Beispiel Verwendung von Floatingpoint-Registern und ein Versuch, ein AutoIt-Script 1:1 in Assembler umzusetzen
    Optimierung von Code am Beispiel des Tunnelfluges
    Weitere Optimierung am "Tunnelflug" am Beispiel eines Arrays bzw. LookUpTabelle LUT
    Einsatz von SSE/SSE2 Befehlen am Beispiel Tunnelflug
    Beispiel für Übergabe und Rückgabe von Parametern an/von einer AutoItfunktion aus dem Assemblercode
    Beispiel um aus einer Assemblerfunktion eine DLL zu machen
    Verwendung von AutoIt-Variablen im Assemblercode NEU



    Edit BugFix:
    Fragen zum Thema bitte hier: Assembler Hilfe-Thread stellen.
    Bisher hier gestellte Fragen sind dorthin verschoben.




    Hallo zusammen,
    da schon mehrfach die Frage nach der Verbindung von einfach zu erstellenden AutoIt-Scripten und sehr schnellen und kleinen Assemblerprogramen auftrat, stelle ich in diesem Tutorial einige Möglichkeiten vor.


    Was ist ein Assembler?

    • Ein Assembler ist ein recht einfaches Programm, welches nichts weiter macht, als Befehle in vom Prozessor direkt verarbeitbare Mnemonics (Maschinensprache) umzuwandeln.


    Was muss ich an Vorwissen haben?

    • Ich werde hier keine "Einführung" in Prozessortechnik machen, wer also keine Ahnung hat, um was es bei Assembler eigentlich geht, und warum jemand ein Prozessorregister oder Speicher braucht, dem empfehle ich die einschlägigen Seiten im Internet. Zum Beispiel HIER oder hier und für "oldschool"-Usenetnutzer hier. Sehr schön auch diese Seite und diese, welche auch hier teilweise in deutsch übersetzt wurde!
      Mir geht es primär um die Frage, wie man Daten von AutoIt aus an das Maschinenspracheprogramm übergeben kann, und wie man Daten zurückbekommt.


    Assembler ist MegaOUT!

    • Für diejenigen, die meinen, Assembler gehöre zu den Dinosauriern, weil man auch objektorientiert mit Hilfe eines tonnenschweren Software-Supertankers ein 800 Megabyte großes "Hallo Welt" schreiben kann, sei gesagt, ja, macht das auch weiterhin! Es lebe die Zukunft!
      Allerdings verpasst man dann, einen kompletten Sudoku-Löser incl. Ein- und Ausgabe in 67 Byte (ja BYTE! ) vorgestellt zu bekommen...


    Was brauche ich?

    • Wer nun nicht völlig abgeschreckt ist, dem würde ich empfehlen, HIER die Datei FASM.zip, einen in AutoIt "integrierten" Assembler, herunterzuladen und auszupacken, und schon kann´s losgehen. Bitte beachten, daß die vorgestellten Programme nur als 32Bit-Kompilat laufen, weiterhin sollte das System so "clean" wie möglich sein, ein neu aufgesetztes BS in einer virtuellen Maschine bietet sich an, ist aber keine Voraussetztung. Ich habe sowohl mit XP/32 als auch mit WIN7/32 in Produktivumgebung keinerlei Probleme gehabt.
      Die im Programmpaket enthaltene FasmDemo.Au3 sollte ohne Probleme durchlaufen. Dann kanns losgehen...
      EDIT: AssembleIt als Hilfsmittel für das Programmieren mit dem Embedded-FASM-Assembler sollte das arbeiten mit dem Code vereinfachen.


    Wozu Assembler?

    • Nun ja, mit keiner anderen Sprache ist man dichter "im" und am Prozessor dran, clever programmiert, kann ein sehr kurzes Programm aus einigen Bytes hunderte Male schneller als ein AutoIt-Script arbeiten..
      Man benutzt also den Assembler dort, wo man große Geschwindigkeitsvorteile erreichen kann. Quasi nebenbei erfährt man einiges über Strukturen (Dllstructxxxxx) und den Aufruf von externen Funktionen z.B. aus DLL´s.
      Das Errorhandling habe ich in den folgenden Beispielen weggelassen, aber gerade bei Assembler rächt sich das SEHR SCHNELL!
      Ein einziges "fehlerhaftes" Bit in einem der Prozessorregister, und der Computer stürzt ab! Glücklicherweise sorgt der Protected Mode nur für eine Fehlermeldung^^


    Zunächst stellt sich die Frage, warum in AutoIt "integriert"?

    • Die Antwort ist relativ einfach, man hat EINE ausführbare Datei. Selbstverständlich ist es mit Assembler möglich, sehr einfach eine DLL zu erstellen, auch darauf werde ich natürlich eingehen!


    Wie kommen die Assemblerprogramme ins AutoIt?

    • Nun, ganz grob gesagt, reserviert AutoIt für den Assembler etwas Speicher, schreibt die aus dem Assemblercode erzeugten Maschinensprache-Befehle hinein, und springt aus dem AutoIt-Script in das Maschinespracheprogramm und nach der Abarbeitung der Befehle wird in das AutoIt-Script zurückgekehrt. Ähnlich einem DLL-Funktionsaufruf.
      Hat man erst einmal Mit Hilfe des Assemblers die Maschinesprache-Sequenz (im folgenden nenne ich das Bytecode) an Befehlen erstellt, ist der Funktions-Aufruf mit einigen Zeilen AutoIt-Code erledigt.
      Damit erübrigt sich auch ein Obfuscator, denn der "lesbare" Assemblercode wird nicht weiter benötigt!
      Aber dazu später, los gehts mit einigen Beispielen!


    Wir fangen damit an, einige einfache Funktionen zu erstellen. Die Eingabeparameter werden an die Assembler-Funktion übergeben, eine Berechnung wird ausgeführt und das Ergebnis wird an AutoIt zurückgegeben.
    Dazu werden von AutoIt die Parameter auf den "Stack" (Stapel von Bytes) "gepushed" (geschoben), und von dort mit dem Assemblerprogramm weiterverarbeitet. Die Rückgabe erfolgt im AX (EAX)-Register.
    Da wir meist im 32-Bit-Bereich arbeiten, verwende ich größtenteils die "großen" 32-Bit-Register (mit dem E-Präfix, bspw. EAX EBX ESP usw.), auch wenn das nicht immer notwendig sein sollte.
    Die Beispiele sind natürlich kommentiert, wer trotzdem etwas nicht versteht, immer fleissig Fragen!
    Eine 8088(286,386,486 usw)Befehlreferenz gibt es z.b. HIER oder HIER
    Nur keine Panik, die allermeisten Befehle im ersten Link sind Erweiterungen (MMX, SSE usw), die man nicht unbedingt braucht. 2-3 Handvoll Befehle reichen aus, um effektive Assemblerprogramme zu schreiben.

  • Beispiel_1: Addition von 2 Integer-Zahlen
    AutoIt-Funktion: AddierenInteger($int1, $int2) ;zwei Integer sollen addiert werden


    Die Übergabe von Funktionsparametern erfolgt in der Regel (wie auch bei DLL-Calls) über den Stack, welcher nur ein Teil des Arbeitsspeichers ist.
    Auf dem Stack wird bei einem Funktionsaufruf die Rücksprungadresse in das aufrufende Programm (z.B. Autoit) abgelegt, gefolgt von den übergebenen Parametern.
    Die Rücksprungadresse ist bei 32-Bit-Systemen 32-Bit groß, d.h. 4 Byte oder ein DWORD.
    Die Parameter sind so groß, wie sie im Funktionsaufruf definiert sind, also (INT) = (4 Byte) = (1 Dword), man kann natürlich auch 64 Bit große Parameter oder 1 Byte große Parameter übergeben.
    Wenn also die Funktion Test mit 2 Parametern aufgerufen wird, dann sieht der Stack beim Aufruf der Funktion also folgendermassen aus:


    • Stack: Inhalt:
      Speicheradresse_xxx+0 Byte Rücksprungadresse: (4 Byte groß)
      Speicheradresse_xxx+4 Byte 1. Parameter INTeger (4 Byte groß)
      Speicheradresse_xxx+8 Byte 2. Parameter INTeger (4 Byte)


    Um im Assembler auf den Stack zuzugreifen, benutzt man das Stackpointerregister ESP. (SP=StackPointer)
    ESP zeigt beim Start der Funktion immer auf die Rücksprungadresse!
    Um also mittels ESP auf den ersten Parameter zu zeigen, muss man die 4 Byte der Größe der Rücksprungadresse zur aktuellen Adresse hinzuzählen. Dabei weisen die rechteckigen Klammern auf den INHALT der Speicherstelle hin!
    ESP+4 ist also die Speicheradresse des 1. Parameters, [ESP+4] ist der INHALT an dieser Speicheradresse, also unser erster Parameter. Damit der Assembler weiß, wie viele Bytes er einlesen muss, schreibt man die "Anzahl" in Form eines Datenwortes vor die Klammer. Allerdings muss das nicht zwangsläufig sein, wenn der Assembler dem Ziel eine bestimmte Größe zuordnen kann!
    Den (4 Byte = 1 DWORD großen) Inhalt des ersten Parameters schreiben wir nun mittels des MOV-Befehls in ein Prozessorregister, beispielsweise EAX:

    Code
    ;In den Kommentaren versuche ich mit AutoIt-ähnlicher Syntax den Assemblerbefel zu verdeutlichen
    MOV EAX,DWORD[ESP+4]     ;EAX=[ESP+4]  Viele Assembler leiten aus der Größe des Zieles die Anzahl der Bytes ab, daher braucht man sie in diesem Fall nicht
    ;oder
    MOV EAX,[ESP+4]     ;EAX=[ESP+4]     EAX ist 4 Byte groß, also werden 4 Byte ab der Position des 1. Parameters=Stackpointers+4 Bytes eingelesen


    Sehr schön, nun steht der erste Parameter im EAX-Register, gleiches mit dem 2. Parameter, der soll ins EBX-Register

    Code
    MOV EBX,[ESP+8] ;EBX=[ESP+8]      EBX ist 4 Byte groß, also werden 4 Byte ab der Position des 2. Parameters=Stackpointers+8 Bytes eingelesen


    Das wars schon, nun sind die Prozessorregister gefüllt, und der Prozessor braucht den Befehl zur Addition

    Code
    ADD EAX,EBX     ;EAX = EAX + EBX


    Der Prozessor addiert die beiden Registerinhalte und schreibt das Ergebnis ins EAX-Register. Unsere Funktion ist fertig! Das Ergebnis wird vom aufrufenden Programm im EAX-Register erwartet.
    Nun muss man dem Assembler nur noch die Speicherstelle "zeigen" an der unsere Funktion aufgerufen wurde.
    Das ist nun die Rücksprungadresse, welche ja auch auf dem Stack steht.
    Der Stackpointer zeigt ja zur Zeit auf diese Rücksprungadresse! Aber was machen wir mit den Parametern auf dem Stack?
    Die müssen dort weg! Ansonsten würde in kürzester Zeit der Speicher überlaufen, weil jede Funktion ihren "Müll" im Speicher stehen ließe!
    Wir sagen also dem Assembler, er soll die beiden Parameter (2 Integer = 8 Byte) auf dem Stack löschen und an das aufrufende Programm zurückkehren.

    Code
    RET 8 ; 8 Byte(=die beiden Parameter) vom Stack löschen und kehre zum ausfrufenden Programm zurück, die Rücksprungadresse wird auch gelöscht.


    Das war´s schon^^


    Übrigens sind die meisten Assembler relativ genügsam im Bezug auf Groß- und Kleinschreibung.
    Man muss beachten, daß die Mehrzahl der Prozessorbefehle nur mit Kombinationen aus Registern und Speicherinhalten arbeiten.
    Ein ADD EAX,[ESP+8] hätte auch den 2. Parameter zum EAX-Register dazugezählt, aber ADD [ESP+4],[ESP+8] FUNKTIONIERT NICHT!
    Im ausführbaren AutoIt-FASM-Code sieht das nun so aus:

    #include "FASM.au3"
    #include <array.au3> ;zur Anzeige des Rückgabe-Arrays


    $Fasm = FasmInit() ;FASM initialisieren
    FasmReset($Fasm) ;FASM resetten
    ;*** hier beginnt der Assemblercode
    FasmAdd($Fasm, "use32") ;wir benutzen den 32-Bit Assembler
    FasmAdd($Fasm, "mov eax, [esp + 4]") ;erster Parameter ins EAX-Register, 4 Byte nach der Rücksprungadresse
    FasmAdd($Fasm, "mov ebx, [esp + 8]") ;zweiter Parameter ins EBX-Register
    FasmAdd($Fasm, "add eax, ebx") ;EAX = EAX + EBX
    FasmAdd($Fasm, "ret 8") ;8 Bytes vom Stack und zurück zur aufrufenden Funktion
    ;*** Ende des Assemblercodes
    ConsoleWrite(String(FasmGetBinary($Fasm))&@CRLF) ;Bytecode
    MsgBox(0,"Bytecode, der vom Assembler erstellt wurde:",String(FasmGetBinary($Fasm))) ;Bytecode anzeigen
    $Ret = MemoryFuncCall("int", FasmGetFuncPtr($Fasm), "int", 12, "int", 44) ;Bytecode aufrufen, nach dem Pointer auf den Code werden die Parameter zugewiesen. Rückgabe als Array!
    _arraydisplay($ret,"Ergebnis, Parameter1, Parameter2") ;Angezeigt werden das Ergebnis und die Übergabeparameter
    MsgBox(0, "Ergebnis", "12 + 44 = " & $Ret[0]) ;EAX steht in $ret[0]


    FasmExit($Fasm) ;FASM aus dem Speicher entfernen

    Noch eine kurze Erklärung zum MemoryFuncCall().

    MemoryFuncCall("Format_Rückgabeparameter", $Pointer_zur_Funktion,"Format_Parameter1",$Parameter1,"Format_Parameter2",$Parameter2, "Format_Parameter3",$Parameter3...uswusf)


    Wie man in der Ausgabe der ersten MessageBox sieht, erstellt der Assembler den Bytecode.
    Wie schon angedeutet, reicht dieser Bytecode für das Ausführen des Programms aus, man muss also den Assembler im fertigen Programm nicht mehr mitschleppen!


    Wie das funktioniert, sieht man nun hier:
    Der Speicher für den Bytecode wird reserviert, der Bytecode (kann aus der Console des Assemblerprogramm-Teils kopiert werden) wird dort eingetragen und Windows wird mit einem Funktionsaufruf (CallWindowProcW) angewiesen, diesen Code auszuführen.
    Allerdings gibt es bei dieser Methode einige Einschränkungen, so können maximal 4 Parameter (32Bit-Typen, also DWORD, INT, FLOAT usw.) übergeben werden. Um mehr Parameter zu übergeben, sollten diese in eine Struktur geschrieben, und dem Call nur der Pointer auf diese Struktur übergeben werden.
    Weiterhin übernimmt die CallWindowProcW-Funktion selbstständig alle "Aufräumarbeiten" auf dem Stack, das ist bei unserem Beispiel ziemlich schlecht, denn der RET 8 löscht schon 2 Parameter! Also was tun?
    Hier muss man sich nun entscheiden, entweder man startet seinen Bytecode mit dem vorgestellten MemoryFuncCall() und muss somit immer den kompletten Assembler bzw die Memory.AU3 "mitschleppen" oder man verwendet CallWindowProcW und kann im fertigen Programm auf die FASM.AU3 (Assembler) und die MEMORY.AU3 verzichten.


    Ich habe mich bei der Zusammenarbeit FASM/AutoIt für CallWindowProcW entschieden. Das eigentliche Assemblerprogramm bleibt identisch, aber da CallWindowProcW den Stack "aufräumt" muss man nur noch mit einem einfachen RET den Assemblercode abschliessen.
    Es ergibt sich also aus

    #include <FASM.au3>
    #include <array.au3>


    $Fasm = FasmInit() ;FASM initialisieren
    FasmReset($Fasm) ;FASM resetten
    FasmAdd($Fasm, "use32") ;wir benutzen den 32-Bit Assembler
    FasmAdd($Fasm, "mov eax, [esp + 4]") ;erster Parameter ins EAX-Register, 4 Byte nach der Rücksprungadresse
    FasmAdd($Fasm, "mov ebx, [esp + 8]") ;zweiter Parameter ins EBX-Register
    FasmAdd($Fasm, "add eax, ebx") ;EAX = EAX + EBX
    ;ein einfaches RET reicht nun
    FasmAdd($Fasm, "ret") ;zurück zur aufrufenden Funktion
    $bytecode=FasmGetBinary($Fasm)
    ConsoleWrite($bytecode&@CRLF) ;Bytecode per Copy/Paste in das nächste Script transferieren
    MsgBox(0,"Bytecode, der vom Assembler erstellt wurde:",String(FasmGetBinary($Fasm))) ;Bytecode anzeigen
    ;die folgenden 3 Zeilen ersetzen den MemoryFuncCall()
    $tCodebuffer=dllstructcreate("byte["&stringlen($Bytecode)/2-1&"]") ;Speicher für den Bytecode reservieren
    dllstructsetdata($tCodeBuffer,1,$Bytecode) ;Bytecode in den Speicher schreiben
    $Ret= DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodeBuffer), "byte", 12, "int", 33, "int", 0, "int", 0);bytecode aufrufen, rückgabe in a[0], aufruf IMMER mit 4 Parametern!
    MsgBox(0,"Ergebnis",$Ret[2]&" + "&$Ret[3]&" = "&$Ret[0])


    FasmExit($Fasm) ;FASM aus dem Speicher entfernen

    der ohne Assembler ausführbare AutoIt-Code:

    $Bytecode="0x8B4424048B5C240801D8C3" ;wurde vom Assembler erstellt und im vorherigen Programm in die Console geschrieben
    $tCodebuffer=dllstructcreate("byte["&stringlen($Bytecode)/2-1&"]") ;Speicher für den Bytecode reservieren
    dllstructsetdata($tCodeBuffer,1,$Bytecode) ;Bytecode in den Speicher schreiben
    $Ret= DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodeBuffer), "int", 12, "int", 33, "int", 0, "int", 0);bytecode aufrufen, rückgabe in a[0]
    MsgBox(0,"Ergebnis",$Ret[2]&" + "&$Ret[3]&" = "&$Ret[0])

    und als AutoIt-Funktion

    ;an den Anfang des Programms
    $Bytecode = "0x8B4424048B5C240801D8C3" ;wurde vom Assembler erstellt
    $tCodebuffer = DllStructCreate("byte[" & StringLen($Bytecode) / 2 - 1 & "]") ;Speicher für den Bytecode reservieren
    DllStructSetData($tCodebuffer, 1, $Bytecode) ;Bytecode in den Speicher schreiben


    $zahl1 = 1337
    $zahl2 = 3537
    $ergebnis = AddierenInteger($zahl1, $zahl2)
    MsgBox(0, "Ergebnis", $zahl1 & " + " & $zahl2 & " = " & $ergebnis)
    Exit


    Func AddierenInteger($int1, $int2) ;addition zweier Integer
    $Ret = DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodebuffer), "int", $int1, "int", $int2, "int", 0, "int", 0);bytecode aufrufen, rückgabe EAX in $ret[0]
    Return $Ret[0]
    EndFunc ;==>AddierenInteger


    Im Prinzip geht es also nur darum, den Bytecode zu erstellen! Nur nebenbei bemerkt, man kann ohne weiteres den Bytecode per

    Filewrite("test.bin",binarytostring($bytecode))

    in eine Datei schreiben und diese dann mit einem Debugger/Disassembler bearbeiten...



    //EDIT//Die Rückgabe an AutoIt erfolgt abhängig vom Format des Datentyps des Dll-Calls!
    Es geht also um den rot markierten Teil des Aufrufs:
    $Ret = DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodebuffer)


    • "int oder long ", es wird zurückgegeben, was im EAX-Register steht, bei Byte gilt z.B. der Inhalt des AL-Registers (unterster Teil von eax)
    • "float oder double", es wird zurückgegeben, was im ST0 des Floatingpointprozessors steht
    • "ptr" auch EAX, aber Ausgabe als 0xAABBCCDD also Hex
    • "str" in eax steht nun der Zeiger auf den Anfang des Strings!
  • Beispiel_2: Pixelsuchen32, findet ein Pixel innerhalb einer Bitmap
    AutoIt-Funktion Pixelsuchen32($Pointer_auf_die_Bitmapdaten,$FarbePixel,$Größe_der_Bitmap,$Breite_der_Bitmap)


    Wir werden nun das allseits beliebte und bekannte Suchen nach 4 aufeinanderfolgenden Bytes im Hauptspeicher durchführen.
    Dazu wird eine 32Bit-Bitmap erstellt, und mit beliebigem Inhalt gefüllt. Ich fülle die Bitmap mit der AutoIt.exe...
    Um den Assemblerteil so einfach wie möglich zu gestalten, konzentrieren wir uns nur auf die reine Suche.
    Die Bitmap liegt bereits im Speicher und ist durch 2 Parameter bestimmt. Die Adresse des ersten Bytes und die Größe=Anzahl der Bytes.
    Ein "Pixel" ist 4 Byte groß und setzt sich aus den Farben Rot RR,Grün GG,Blau BB und dem Alphakanal AA für die Transparenz zusammen.
    Die Reihenfolge im Speicher lautet AABBGGRR


    Das Assemblerprogramm soll folgendes machen:
    Lade die Farbe in ein Prozessorregister (32Bit= 4 Byte)
    Lade die Startadresse der Bitmap in ein Zähl-Register.
    Vergleiche die 4 Bytes an dieser Speicheradresse mit den 4 Byte im Register (der gesuchten Farbe)
    Wenn die 4 Bytes im Speicher identisch sind mit den 4 Bytes im Register, gebe die Position des ersten Bytes im Speicher aus. Wenn nicht, erhöhe das Zähl-Register um 4 und vergleiche die nächsten 4 Bytes.
    Wiederhole so lange, bis das Ende der Bitmap erreicht ist.


    Im folgenden Script habe ich das Erstellen des Bytecodes in eine eigene Funktion MakeByteCode() ausgelagert, da dieser Vorgang natürlich nur ein einziges Mal beim Start des AutoIt-Scriptes erfolgen muss.
    Die AutoIt-Funktion Pixelsearch() habe ich als Vergleich eingefügt. Das ist natürlich unfair, da Pixelsearch() wesentlich komfortabler ist, und nicht nur mit 32Bit-Bitmaps arbeitet. Aber mit einer Handvoll Bytes in unserer Assembler-Funktion erreichen wir eine immerhin 15-20 mal schnellere Suche! Erweitert die Bitmap mal auf 10000x10000 Pixel, Pixelsearch steigt dann zwar aus, aber unser Assemblerprogramm findet tapfer das gesuchte Pixel...

    ;Beispielscript PixelSuchen
    ;findet ein Pixel mit einer bestimmten Farbe innerhalb einer 32-Bit-Bitmap


    #include <FASM.au3>
    #include <WinAPI.au3>


    Opt("PixelCoordMode", 2) ;ausschliesslich für Pixelsearch()


    Global $bytecode = MakeBytecode() ;den Assembler anweisen, den Bytecode zu erstellen
    ;wenn der Bytecode ausgetestet ist, dann kann man auch
    ;Global $bytecode ="0x8B7C24048B4424088B54240C8B5C241089D1F2AFB80000000083F900741383C10129CA89D0BA00000000F7F3C1E21001D0C3"
    ;schreiben und #include <FASM.au3> und die gesamte Funktion MakeBytecode() weglassen



    local $ptr_Bitmap, $hbmp_Bitmap ;Pointer und Handle der Bitmap, Handle wird hier nicht gebraucht
    $iwidth = 1000 ;Breite und Höhe der Bitmap, probiert mal 10000x10000, das Pixel wird mit PixelSuche32() gefunden , aber Pixelsearch() steigt aus
    $iheight = 700 ;
    $hDC_Bitmap = _CreateNewBmp32($iwidth, $iheight, $ptr_Bitmap, $hbmp_Bitmap) ;leere 32Bit-Bitmap erstellen


    ;wir füllen die Bitmap mit dem Inhalt der AutoIt.EXE
    $Struct = DllStructCreate("byte[" & $iwidth * $iheight * 4 & "]", $ptr_Bitmap) ;struct an der Position der Bitmap erstellen
    DllStructSetData($Struct, 1, StringLeft(StringToBinary(FileRead(@AutoItExe)), $iwidth * $iheight * 8)) ;und mit den Bytes der AutoIt.exe füllen


    ;Pixel setzen an Position x=150 y=height-10 mit der Farbe 0xFFE199F0, das Pixel sollte im unteren linken Teil des schwarzen Feldes in der GUI zu sehen sein
    $col = 0xFFE199F0 ;gesuchte Farbe 0xAABBGGRR Alpha,Blau,Gruen,Rot
    $Struct2 = DllStructCreate("dword", $ptr_Bitmap + $iwidth * ($iheight-10) * 4 + 150 * 4) ;struct an Position des Pixels erzeugen
    DllStructSetData($Struct2, 1, $col ) ;AABBGGRR Alpha,Blau,Gruen,Rot Farbe in die Bitmap eintragen


    ;GUI erstellen und anzeigen (wird nur für Pixelsearch() benötigt, PixelSuchen32() braucht kein aktives Fenster)
    $hgui = GUICreate("AutoIt.EXE visualisiert^^", $iwidth, $iheight) ;Gui erstellen
    $hDC_GUI = _WinAPI_GetDC($hgui) ;Device Context des Fensters
    GUISetState(@SW_SHOW, $hgui)
    _WinAPI_BitBlt($hDC_GUI, 0, 0, $iwidth, $iheight, $hDC_Bitmap, 0, 0, 0xCC0020) ;Bitmap in GUI darstellen 0xCC0020=SRCCOPY



    $t=timerinit()
    $pos = PixelSuchen32($ptr_Bitmap, $col, $iwidth * $iheight , $iwidth) ;findet 0xAABBGGRR in einer Bitmap
    $m=timerdiff($t)


    $t=timerinit()
    $a=pixelsearch(0,0,$iwidth,$iheight, dec(hex($col,6)),0,1,$hgui) ;pixelsearch findet 0xBBGGRR
    $f=timerdiff($t)


    if $pos=0 Then ;falls EAX=0, dann wurde keine Übereinstimmung gefunden
    msgbox(0,"Pixel mit der Funktion PixelSuchen32()","nicht gefunden!")
    Else ;ansonsten
    msgbox(0,"Pixel mit PixelSuchen32() gefunden an","X= "&$pos[0]&@crlf&"Y= "&$pos[1]&@crlf&@crlf&stringformat("in %.2f Millisekunden wurden ",$m)&$pos[1]*$iwidth+$pos[0]&" Pixel verglichen, das"&@crlf&"sind "&int(($pos[1]*$iwidth+$pos[0])/$m/1000)&" Millionen Pixel pro Sekunde!")
    msgbox(0,"Pixel mit Pixelsearch() gefunden an","X= "&$a[0]&@crlf&"Y= "&$a[1]&@crlf&@crlf&stringformat("in %.2f Millisekunden",$f))
    endif


    While GUIGetMsg() <> -3
    ;_WinAPI_BitBlt($hDC_GUI, 0, 0, $iwidth, $iheight, $hDC_Bitmap, 0, 0, $SRCCOPY) ;Bitmap in GUI darstellen
    WEnd
    _WinAPI_DeleteObject($hbmp_Bitmap) ;wurde zwar nicht benötigt, aber erstellt!
    exit



    Func PixelSuchen32($ptr, $col, $Breite_x_Hoehe,$Breite) ;pointer auf die Bitmap, Farbe und Größe der Bitmap
    $tCodebuffer = DllStructCreate("byte[" & StringLen($bytecode) / 2 - 1 & "]") ;Speicher für den Bytecode reservieren
    DllStructSetData($tCodebuffer, 1, $bytecode) ;Bytecode in den Speicher schreiben
    $Ret = DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodebuffer), "ptr", $ptr, "int", $col, "int", $Breite_x_Hoehe, "int", $Breite);bytecode aufrufen, rückgabe in a[0], Aufruf IMMER mit 4 Parametern!
    if $ret[0]=0 or @error then return 0 ;Pixel wurde nicht gefunden
    dim $koord[2]
    ;die oberen 16 Bit von EAX enthalten die x-Koordinate, die unteren 16 Bit enthalten die y-koordinate
    $koord[0]=BitShift($ret[0],16) ;x-Koordinate
    $koord[1]=Bitand($ret[0],0x0000FFFF);y-Koordinate
    return $koord
    EndFunc ;==>PixelSuchen32



    Func MakeBytecode() ;aus dem Assemblercode einen Bytecode machen, den der Prozessor ausführen kann
    Local $Fasm = FasmInit() ;FASM initialisieren
    FasmReset($Fasm) ;FASM resetten


    FasmAdd($Fasm, "use32") ;wir benutzen den 32-Bit Assembler
    FasmAdd($Fasm, "mov edi, [esp + 4]") ;ES:EDI = Pointer auf die Bitmap
    FasmAdd($Fasm, "mov eax, [esp + 8]") ;EAX = gesuchte Farbe
    FasmAdd($Fasm, "mov edx, [esp + 12]") ;EDX = Größe der Bitmap
    FasmAdd($Fasm, "mov ebx, [esp + 16]") ;EBX = Breite der Bitmap
    FasmAdd($Fasm, "mov ecx,edx") ;ECX ist der Zähler
    FasmAdd($Fasm, "REPNE SCASD") ;so lange ES:EDI mit EAX vergleichen, bis entweder
    ;- Speicherinhalt von ES:EDI und Register EAX identisch sind
    ;- oder ECX=0 ist, ECX wird immer um eins subtrahiert und EDI um ein DWORD(=4 Bytes) addiert
    ;Im Prinzip wird ein DWORD im angegebenen Speicherbereich gesucht
    ;For $ECX=$EDX to 0 Step -4
    ;If $EDI[$EDX-$ECX]=$EAX then exit ; gefunden
    ;Next
    FasmAdd($Fasm, "mov eax,0") ;EAX=0 Wir nehmen an, es wurde kein Pixel gefunden
    FasmAdd($Fasm, "cmp ecx,0") ;Compare(Vergleiche) ECX mit 0 , Ist ECX=0, dann wurde kein Pixel gefunden, die Zählschleife ist komplett durchgelaufen
    FasmAdd($Fasm, "JE Ende") ;Jump if Equal, springe zum ENDE-Label, wenn ECX=0
    FasmAdd($Fasm, "add ecx,1") ;ECX=ECX+1 gefundene Byte-Position des Pixels in der Bitmap
    FasmAdd($Fasm, "sub edx,ecx") ;EDX=EDX-ECX ECX zählt ja "rückwärts" bis auf 0
    FasmAdd($Fasm, "mov eax,edx") ;EAX=EDX Position des Pixels
    FasmAdd($Fasm, "mov edx,0") ;EDX auf Division vorbereiten
    FasmAdd($Fasm, "div ebx") ;EAX=int(eax/ebx) EDX=mod(eax,ebx) in EAX steht der ganzzahlige Quotient, in EDX der Rest
    FasmAdd($Fasm, "shl edx,16") ;EDX 16 Bits nach links schieben...
    FasmAdd($Fasm, "add eax,edx") ;...und zu EAX addieren in den oberen 16 Bit steht nun die Zeile, in den unteren 16 bit die Spalte des gefundenen Pixels
    FasmAdd($Fasm, "Ende:") ;Sprungmarke
    FasmAdd($Fasm, "ret") ;zurück zur aufrufenden Funktion


    ;hier könnte man das Errorhandling einfügen aus der FASMDemo.au3


    Local $bytecode = String(FasmGetBinary($Fasm)) ;bytecode erstellen
    ConsoleWrite('@@ Debug(' & @ScriptLineNumber & ') : $bytecode = ' & $bytecode & @crlf & '>Error code: ' & @error & @crlf) ;### Debug Console
    FasmExit($Fasm) ;FASM aus dem Speicher entfernen
    Return $bytecode
    EndFunc ;==>MakeBytecode




    Func _CreateNewBmp32($iwidth, $iheight, ByRef $ptr, ByRef $hbmp) ;erstellt leere 32-bit-Bitmap; Rückgabe $HDC und $ptr und handle auf die Bitmapdaten
    Local $hcdc = _WinAPI_CreateCompatibleDC(0) ;Desktop-Kompatiblen DeviceContext erstellen lassen
    Local $tBMI = DllStructCreate($tagBITMAPINFO) ;Struktur der Bitmapinfo erstellen und Daten eintragen
    DllStructSetData($tBMI, "Size", DllStructGetSize($tBMI) - 4);Structgröße abzüglich der Daten für die Palette
    DllStructSetData($tBMI, "Width", $iwidth)
    DllStructSetData($tBMI, "Height", -$iheight) ;minus =standard = bottomup
    DllStructSetData($tBMI, "Planes", 1)
    DllStructSetData($tBMI, "BitCount", 32) ;32 Bit = 4 Bytes => AABBGGRR
    Local $adib = DllCall('gdi32.dll', 'ptr', 'CreateDIBSection', 'hwnd', 0, 'ptr', DllStructGetPtr($tBMI), 'uint', 1, 'ptr*', 0, 'ptr', 0, 'uint', 0)
    $hbmp = $adib[0] ;hbitmap handle auf die Bitmap, auch per GDI+ zu verwenden
    $ptr = $adib[4] ;pointer auf den Anfang der Bitmapdaten, vom Assembler verwendet
    ;_arraydisplay($adib)
    _WinAPI_SelectObject($hcdc, $hbmp) ;objekt hbitmap in DC
    Return $hcdc ;DC der Bitmap zurückgeben
    EndFunc ;==>_CreateNewBmp32

  • Ich machs kurz und kann es auch nicht besser ausdrücken: DANKE :love:
    Du kannst diesem Forum etwas beitragen was wohl kein anderer hier kann, bzw. kein anderer macht.
    Solche Leute die die Grenzen von AutoIt immer weiter nach außen tragen (auch Progandy & Co. mit ihren Objekten sind da ebenfalls in der selben Liga zu nennen) sind wirklich eine absolute Bereicherung.
    Nachdem diese meine letzte Prüfungswoche als Student vorbei ist werde ich mich gewissenhaft durch dein Tutorial arbeiten und hoffe das dies meiner Optimierungsgier weitere Nahrung darbietet.

  • Das Tutorial ist wirklich gut. Ich glaube, ich muss mich auch mal mit ASM beschäftigen ;)
    Wie ist das eigentlich mit CallWindowProc auf x64 (Falls man x64-Bytecode hat, ohne FASM)? Kann man da auch 4byte-int-Parameter statt dem 8byte Fenster-Handle und so verwenden? oder muss man dann den restlichen Platz "auffüllen", indem man weitere Paremeter einbaut?

  • Hi,
    dieses Tut ist heute bei einigen Kannen Kaffee aus meinen Fingern geflossen, aber ich habe noch einige weitere Ideen^^
    Einige Beispiele sind auch in Vorbereitung, wenn ihr Ideen habt, wäre ich für einen kleinen Stupser in eine bestimmte Richtung dankbar. Es ist immer schwer, sich etwas aus den Fingern zu saugen :D

    Zitat von AspirinJunkie

    und hoffe das dies meiner Optimierungsgier weitere Nahrung darbietet.

    Der Unterschied zwischen Assembler und Assembler fällt einem dann auf, wenn man sieht, was Spezialisten aus der Hardware herauskitzeln können. Ich habe einige PDF´s von AMD, die sich ausschliesslich mit der Optimierung befassen, da fragt man sich dann, ob die noch alle auf der Reihe haben, wenn bei einer 3Gigahertz-Maschine einzelne Takte eingespart werden.^^ Andererseits ist es irre, mit welch einfachen Mitteln man z.B. den Prozessorcache einsetzen kann und somit Programme um mehrere Faktoren beschleunigt.

    Zitat von Prog@ndy

    Wie ist das eigentlich mit CallWindowProc auf x64 (Falls man x64-Bytecode hat, ohne FASM)? Kann man da auch 4byte-int-Parameter statt dem 8byte Fenster-Handle und so verwenden? oder muss man dann den restlichen Platz "auffüllen", indem man weitere Paremeter einbaut?

    Gute Frage, in Ermangelung eines 64-Bit-Systems bin ich mir nicht so ganz sicher. Definitiv steht fest, daß bei 64Bit ausschliesslich 64Bit-Parameter übergeben werden. Das habe ich schon in einigen Beispielen gesehen. Das heisst, auch wenn du nur ein 8 Bit breites Byte als Parameter bräuchtest, musst du einen 64 Bit-Parameter verwenden, Handles und der gesamte andere Dreck, alles 64 Bit breit! Hat auch was Gutes, es gibt nur noch ein Datenformat^^
    Da im 64-bit-Modus der Prozessor auch ausschliesslich mit 64- bzw 128 Bit-Registern arbeitet, hat sich der "normale" 8088/8086 Assembler verabschiedet. Mein schöner Traum, 64-Bit sei eine "Erweiterung" wie bisher 8088 (8-Bit) -> 80286 (16 Bit) -> 80386 (32 Bit) -> 80486 .....64-Bitprozessor ist ausgeträumt. Im 64-Bit-Modus gibt es scheinbar keinen simultanen "80x86"-Kompatibilitätsmodus. Will z.B. heissen, das EAX-Register (32 Bit) besteht aus AH+AL, welches zu AX zusammengefasst ist, und 16 weitern Bits. Ich hatte mich schon darauf gefreut, einfach zu den 32 Bits dieses Registers 32 weitere zu bekommen und dieses sei dann das RAX-Register....aber Pustekuchen! RAX ist 64 Bit breit, und nicht weiter "unterteilt". :thumbdown:


    Alles andere als 64 Bit wird wohl in speziellen Umgebungen laufen müssen, DOS-Box lässt grüssen. Jag mal eine beliebige 64-bit-Datei durch einen Disassembler, also ich verstehe da nur noch Bahnhof^^


    /EDIT/ Eine Idee hätte ich noch:
    Mach doch mal einen CallWindowProc-Call ohne spezielle Parameter, also nur "INT64",dllstructgetptr($struct_mit_Bytecode), "INT64",0, "INT64",0, "INT64",0, "INT64",0)
    Dann schreib als Bytecode nur "0xC3" (das ist ein einfaches RET). Wenn dieser Call fehlerfrei ausgeführt wird, dann stimmt wenigstens das Format der Parameter. Dann muss man sich "nur noch" überlegen, wie man die Register mit den Parametern füllt. Leider weiss ich nicht, welches Register für die Rückgabe verantwortlich ist, aber da ist auf jeden Fall reichlich Platz zum Rumspielen :thumbup:

  • @Mod´s,
    leider habe ich "verpennt", unter den obersten 3 Beiträgen noch einige von mir erstellte leere Postings als Platzhalter für weitere Beiträge zu reservieren. Das Tut steht ja erst am Anfang^^
    Ist es euch möglich, dort nachträglich Beitrage einzufügen?

  • Andy: Erstelle die Posts doch zwischendurch und führe im ersten Posting ein Inhaltsverzeichnis ein. Ansonsten kannst du von Gun eventuell eine eigene Subdomain bekommen, auf der du dann deine Tutorials hochladen kannst ;)

  • Prima umgesetzt, die Sache. :)


    Sieht wirklich gut aus.


    RAX ist 64 Bit breit, und nicht weiter "unterteilt".


    Du kannst im 64bit Mode die (Unter-)Register so wie sonst auch ansprechen, also RAX, EAX, AX und AH/AL, bzw. R8, R8D, R8W, R8B.
    Du hast lediglich kein HIBYTE Opcode für die zusätzlichen Register, also bei den R-Registern nichts entsprechendes für AH, BH, CH, usw.
    3.1.2 64-Bit-Mode Registers


    Das Tutorial ist wirklich gut. Ich glaube, ich muss mich auch mal mit ASM beschäftigen ;)
    Wie ist das eigentlich mit CallWindowProc auf x64 (Falls man x64-Bytecode hat, ohne FASM)? Kann man da auch 4byte-int-Parameter statt dem 8byte Fenster-Handle und so verwenden? oder muss man dann den restlichen Platz "auffüllen", indem man weitere Paremeter einbaut?


    Alle ganzzahligen Parameter sind 64 bit breit. Die Aufrufkonvention ist nicht mehr "stdcall" sondern "fastcall", d.h. die ersten vier Parameter werden in den Registern RCX, RDX, R8 und R9 übergeben. Sind die ersten vier Parameter Fließkommazahlen, dann sind sie 128 bit breit und werden über die Register XMM0 bis drei übergeben (zumindest bei den M$ Calling Conventions).


    Assembler lernen an sich ist nicht schwer, aber es braucht viele Jahre wirklich guten Assembler zu schreiben. Da muss man etwas Zeit mitbringen.



    Gruß
    Greenhorn

  • Danke, Greenhorn. Mein einziger x64-Assemblercode bisher war die Jump-Stub für den x64-MemoryDLLCall, den wir in AutoItObject gebraucht haben ;)

  • Zitat

    Die Aufrufkonvention ist nicht mehr "stdcall" sondern "fastcall"

    na subba :thumbup: , im Prinzip ja eine schöne Sache. Auch daß die Floatingpoints direkt in die XMM-Register geschrieben werden, sehr fein. Da kann ich ja im nächsten Tutorialteil direkt mit dem alignen für die MMX-Register weitermachen^^. Lieber doch nicht...bestimmt wurden einige schon von dem REPNE SCASD-Monster im 2. Beispiel abgeschreckt....
    Morgen gibts den nächsten Teil, Variablen verwenden und bissl String-Management.
    Lasst mich nicht am ausgestreckten Arm verhungern, auf welchem Gebiet gibts Interesse?

  • Beispiel_3 Verwenden eigener Variablennamen


    Ein großer Vorteil von Assembler ist die enorme Geschwindigkeit, mit der Daten, die sich in den Prozessorregistern befinden, verarbeitet werden. Leider ist die Anzahl dieser Register IMMER zu wenig^^. Es gibt nun 2 Möglichkeiten, einen "Zwischenspeicher" zu erstellen.
    Man legt per PUSH-Befehl die Daten einfach auf dem Stack ab, um sie mit dem POP Befehl wieder abzuholen, oder man definiert einfach Variablen in seinem Script!


    PUSH EAX ;legt den Inhalt von EAX auf dem Stack ab
    PUSH 122 ;legt 122 auf dem Stack ab
    ;irgendwelcher Code
    POP EBX ;EBX=122 nimmt das oberste Element vom Stack und löscht dieses Element auf dem Stack
    POP ECX ;ECX=(ehemaliges)EAX nimmt das oberste Element vom Stack und löscht dieses Element auf dem Stack


    Push und Pop sollten immer zusammen verwendet werden, um nach dem Abschluss des Programms einen definierten Zustand auf dem Stack zu erhalten!
    Wer mehrere Register auf einen Streich sichern möchte, sollte sich PUSHA (Push all) und POPA mal ansehen, bzw PUSHAD und POPAD.


    Eine weitere Möglichkeit ist die Verwendung von Variablen, die auch im Code verwendet werden können.
    Da diese Variablen eigentlich nur Zeiger auf einen verwendeten Speicherbereich sind, gilt auch hier: Bei Verwendung von Prozessorbefehlen immer das Format beachten!
    Es gibt nur sehr wenige Befehle, die direkt Daten im Speicher manipulieren können, ohne den Befehl über ein Register zu gehen.


    Variablen sind wie gesagt einfache Speicherbereiche. Diese können in Assembler nach folgendem Muster angelegt werden:
    Variablenname db(Byte)/dw(word)/dd(dword)/dq(quadword) Inhalt (s. Zeilen 15-19)

    ;Verwendung von Variablen
    Dim $Fasm = FasmInit()


    FasmReset($Fasm)
    FasmAdd($Fasm, "use32")
    FasmAdd($Fasm, "org "&FasmGetBasePtr($Fasm)) ;Dem Assembler die zzt aktuellen Segmentregister bzw Offsets mitteilen
    ; FasmAdd($Fasm, "return db 195 ") ;trägt das Mnemonic 195=C3h in den Code ein = RET , also zurück zum aufrufenden Programm!
    FasmAdd($Fasm, "mov eax,dword[erste]") ;EAX=22 speicherinhalt an der Adresse von "erste"
    ; FasmAdd($Fasm, "mov eax,erste") ;EAX= Speicheradresse, an der die Variable "erste" beginnt
    FasmAdd($Fasm, "ret") ;zurück zum aufrufenden Programm
    ;ab hier kommen die Variablen, diese sollten immer HINTER dem Programmcode stehen
    ;wer erfahren möchte was passiert, wenn Variablen irgendwo im Code eingetragen werden, der kommentiert einfach die "return db 195"-Zeile aus....
    ;der Assembler assembliert natürlch stur seinen Code, und wenn der Prozessor ein C3 als nächsten Befehl bekommt, dann bedeutet das RET!!!


    FasmAdd($Fasm, "erste dd 77 ") ;den Wert 22 als Inhalt in das reservierte dword eingetragen $erste=22
    FasmAdd($Fasm, "zweite db 8 dup(7)") ;8 Bytes reservieren mit jeweils dem Inhalt 7 das wird zu 0707070707070707
    FasmAdd($Fasm, "dritte dq 1001001b") ;Quadword (4 Words= 8 Bytes) und binären Inhalt eintragen
    FasmAdd($Fasm, "string db 'Hallo, das ist ein nullterminierter String',0 ") ;String
    FasmAdd($Fasm, "variable1 dd ?") ;nicht näher bezeichnete resevierte 4 bytes


    ConsoleWrite(String(FasmGetBinary($Fasm)) & @CRLF)
    $a = MemoryFuncCall("int", FasmGetFuncPtr($Fasm))
    _arraydisplay($a)
    FasmExit($Fasm)


    Was ist zu beachten? Zunächst muss dem Assembler mit der ORG-Anweisung mitgeteilt werden, in welchem Speicherbereich er zu werkeln hat.
    Anhand dieser Adresse "errechnet" er die Position unserer Variablen BEVOR er das Script assembliert! Das heisst im vorliegenden Fall, daß er die Speicheradresse, in der unser Bytecode steht (Startadresse) nimmt, und die Bytes zählt, bis er beim ersten Byte der entsprechenden Variablen ist. Kompliziert? Egal, Beispiel:
    Unser obiges Programm vom ersten mov eax...blabla bis zum ret besteht aus 6 Bytes. (kann man schön in der Konsole sehen, C3 ist das RET)
    Das heisst, das 7. Byte hinter dem Programmcode ist das erste Byte der Variablen "erste".
    Im weiteren zählt der Assembler zu der Startadresse einfach 7 Bytes dazu und hat so die Adresse von "erste". "erste" ist 4 Bytes lang, also ist Adresse von "zweite" = Startadresse + 6 Bytes Programm + 4 Bytes "erste" +1 (das erste Byte von "zweite")
    Muss uns das eigentlich interessieren was der Assemmbler da macht?
    Eigentlich nicht, aber aufmerksame Mitleser haben bemerkt, daß ich im Beispiel den MemoryFuncCall() verwendet habe!
    Der Witz ist nämlich, daß der Assembler ohne das ORG die Startadresse mit Null annimmt. Die Adresse der Variablen wird also zu "zweite" = 0 + 6 Bytes Programm + 4 Bytes "erste" +1 (das erste Byte von "zweite")
    Nun ratet mal was passiert, wenn das Programm irgendwo in den Speicher geladen wird und auf die (vermeintliche) Variable an Adresse 0x0000000B zugegriffen wird?


    BÄÄÄÄÄM, Windows-Schutzverletzung, den dieser Speicherbereich ist geperrt, Windows erlaubt nur Speicherzugriffe im AutoIt zugeteilten Speicherbereich!
    Und nu?
    MemoryFuncCall()-Verwender sind aus dem Schneider und lachen sich eins^^, aber wer auch den Bytecode (ohne die FASM.AU3 und MEMORY.AU3) verwenden will, der muss was herausfinden?
    Naja, die Startadresse (Position, an der der Bytecode im Speicher liegt) des Programms! Die müsste man dann einfach zu den Adressen der Variablennamen dazuzählen, und alles wäre paletti!
    Und das ist ja einfach, denn die Startadresse haben wir ja bereits! Diese Startadresse wird ja dem CallWindowProcW mittels "ptr", DllStructGetPtr($tCodeBuffer) übergeben! Aber um nicht einen der 4 wertvollen der Übergabe-Parameter mit dem Pointer zu "verschwenden" habe ich ein wenig gestöbert und herausgefunden, daß beim Starten unseres Bytecodes die Startadresse im ESI-Register steht! Ob AutoIt oder Windows dafür verantwortlich sind, ist mir ehrlich gesagt egal^^


    Unsere Variablenadresse erhalten wir also mit


    FasmAdd($Fasm, "mov eax,dword[esi+erste]") ;EAX=22 Speicherinhalt an der Adresse von "erste"

    und lassen den ORG-Befehl weg. Denn wie schon gesagt, ermittelt ORG die Adressen ja VOR dem Assemblerlauf, aber unser Bytecode kann ja später irgendwoanders im Speicher stehen!
    Wer jetzt aufgepasst hat, der fragt sich was passiert, wenn man im Beispiel den ORG-Befehl NICHT weglässt....in unserem Beispiel nichts aufregendes, denn das aktuelle Speichersegment wurde ja während des Programmlaufs nicht verändert! Aber wenn der Bytecode kopiert, und von einem anderen Programm verwendet wird, gibt es einen Absturz...weil durch die ORG-Anweisung im Bytecode FESTE Adressen der Variablen verwendet wurden...


    Für die Bytecoder...

    ;Verwendung von Variablen
    Dim $Fasm = FasmInit()
    FasmReset($Fasm)
    FasmAdd($Fasm, "use32")


    FasmAdd($Fasm, "mov eax,dword[esi+erste]") ;EAX=77 ESI ist Basisadresse des Programms, "erste" die Anzahl Bytes bis zur Variable
    ;folgende 2 Zeilen zum Verdeutlichen auskommentieren
    ; FasmAdd($Fasm, "mov eax,erste") ;erste die Anzahl Bytes von 0 bis zur Variable
    ; FasmAdd($Fasm, "mov eax,esi") ;in ESI steht die Startadresse im Speicher
    FasmAdd($Fasm, "ret") ;zurück zum aufrufenden Programm
    ;ab hier kommen die Variablen
    FasmAdd($Fasm, "erste dd 77 ") ;den Wert 77 als Inhalt in das reservierte dword eingetragen $erste=77
    FasmAdd($Fasm, "zweite db 8 dup(7)") ;8 Bytes reservieren mit jeweils dem Inhalt 7 das wird zu 0707070707070707
    FasmAdd($Fasm, "dritte dq 1001001b") ;Quadword (4 Words= 8 Bytes) und binären Inhalt eintragen
    FasmAdd($Fasm, "string db 'Hallo, das ist ein nullterminierter String',0 ") ;String
    FasmAdd($Fasm, "variable1 dd ?") ;nicht näher bezeichnete resevierte 4 bytes


    ConsoleWrite(String(FasmGetBinary($Fasm)) & @CRLF)
    $tCodeBuffer = DllStructCreate("byte[512]") ;Speicher für den assemblercode belegen
    DllStructSetData($tCodeBuffer, 1, String(FasmGetBinary($Fasm)))
    $a = DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodeBuffer),"int",0,"int",0, "int", 0,"int",0);bytecode aufrufen, rückgabe in a[0]
    _arraydisplay($a)
    FasmExit($Fasm)


    Bytecode aus der Konsole kopieren und starten..

    $tCodeBuffer = DllStructCreate("byte[512]") ;Speicher für den Assemblercode belegen
    DllStructSetData($tCodeBuffer, 1, "0x8B4604C34D0000000707070707070707490000000000000048616C6C6F2C20646173206973742065696E206E756C6C7465726D696E69657274657220537472696E6700")
    $a = DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodeBuffer),"int",5,"int",3, "int", 0,"int",0);bytecode aufrufen, rückgabe in a[0]
    ;Inhalt der Variablen "erste" im Bytecode ausgeben
    ConsoleWrite('@@ Debug(' & @ScriptLineNumber & ') : $m = ' & $a[0]& @crlf & '>Error code: ' & @error & @crlf) ;### Debug Console


    Die Variablen sind ja nur eine aufeinanderfolgende Anzahl von Bytes, daher muss penibel auf den Datentyp bzw. Datenformat geachtet werden! Man kann natürlich einfach "mehr" Daten lesen, als für diese Variable reserviert ist.

    ;Verwendung von Variablen
    Dim $Fasm = FasmInit()
    FasmReset($Fasm)
    FasmAdd($Fasm, "use32")
    FasmAdd($Fasm, "mov eax,0") ;register leeren
    FasmAdd($Fasm, "mov al,byte[esi+var1]") ;Var1 ist ein Byte, es wird ein Byte ins Lowbyte von EAX ausgelesen
    ; FasmAdd($Fasm, "mov ax,word[esi+var1]") ;Var1 ist ein Byte, es wird aber ein Word=2 Byte ins Low-und Highbyte (zusammen AX) von EAX ausgelesen
    ; FasmAdd($Fasm, "mov eax,dword[esi+var1]") ;Var1 ist ein Byte, es wird aber ein dWord=4 Byte ausgelesen
    ;da beim Lesen unerlaubterweise auf DATEN AUSSERHALB DES PROGRAMMCODES zugegriffen wird (die 2 Byte "hinter" var2) gibt es
    ;ggf einen Absturz, wenn Windows diese 2 Byte für ein anderes Programm reserviert hat!!!!
    ;daher niemals über das letzte Datenbyte hinaus Daten lesen oder schreiben!
    FasmAdd($Fasm, "ret") ;zurück zum aufrufenden Programm
    ;ab hier kommen die Variablen
    FasmAdd($Fasm, "var1 db 15h") ;15hex ins Byte schreiben
    FasmAdd($Fasm, "var2 db 66h") ;66hex ins Byte schreiben


    ConsoleWrite(String(FasmGetBinary($Fasm)) & @CRLF)
    $tCodeBuffer = DllStructCreate("byte[512]") ;Speicher für den assemblercode belegen
    DllStructSetData($tCodeBuffer, 1, String(FasmGetBinary($Fasm)))
    $a = DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodeBuffer),"int",0,"int",0, "int", 0,"int",0);bytecode aufrufen, rückgabe in a[0]
    msgbox(0,"EAX",hex($a[0],8))
    _arraydisplay($a)
    FasmExit($Fasm)

    Zum Verdeutlichen die auskommentierten Zeilen ausführen...


    Apropos...Daten in Variablen einfügen geht natürlich auch^^

    ;Verwendung von Variablen
    Dim $Fasm = FasmInit()
    FasmReset($Fasm)
    FasmAdd($Fasm, "use32")
    FasmAdd($Fasm, "mov eax, [esp+4]") ;EAX=33 erster Parameter von CallWindowProcW
    FasmAdd($Fasm, "mov dword[esi+var1],eax") ;$var1=33 wenn die Datentypen "passen", kann man das "dword" weglassen!
    FasmAdd($Fasm, "mov eax, [esp+8]") ;EAX=4567 zweiter Parameter von CallWindowProcW
    FasmAdd($Fasm, "mov [esi+var2],eax") ;$var2=4567
    ;beide Variablen addieren, ADD erlaubt nicht, Speicherzellen direkt zu addieren ADD [SpeicherX],[SpeicherY] funktioniert nicht
    FasmAdd($Fasm, "mov ebx,[esi+var2]");EBX=$var2=4567
    FasmAdd($Fasm, "mov ecx,[esi+var1]");ECX=$var1=33
    FasmAdd($Fasm, "add ecx,ebx") ;ECX=ECX+EBX addieren
    FasmAdd($Fasm, "mov eax,ecx") ;EAX=ECX
    FasmAdd($Fasm, "ret") ;zurück zum aufrufenden Programm
    ;ab hier kommen die Variablen
    FasmAdd($Fasm, "var1 dd 0") ;dword reservieren
    FasmAdd($Fasm, "var2 dd 0") ;dword reservieren


    ConsoleWrite(String(FasmGetBinary($Fasm)) & @CRLF)
    $tCodeBuffer = DllStructCreate("byte[512]") ;Speicher für den assemblercode belegen
    DllStructSetData($tCodeBuffer, 1, String(FasmGetBinary($Fasm)))
    $a = DllCall("user32.dll", "int", "CallWindowProcW", "ptr", DllStructGetPtr($tCodeBuffer),"int",33,"int",4567, "int", 0,"int",0);bytecode aufrufen, rückgabe in a[0]
    _arraydisplay($a)
    FasmExit($Fasm)


    Das ESI-Register ist ziemlich wichtig, wenn es z.B. um das Kopieren oder durchsuchen von Speicher geht. Daher sollte man, falls ESI anderweitig gebraucht wird, ein anderes, sonst nicht verwendetes Register (z.B. EBX) verwenden.
    mov ebx,esi
    ;esi verwenden
    add ecx,[ebx+var1] ;Variable in ECX schreiben


    oder
    PUSH esi
    ;esi verwenden
    POP esi
    add ecx,[esi+var1] ;Variable in ECX schreiben



    /EDIT/ 14. Februar 2012
    HIER wird beschrieben, wie man auf Variablen zugreift, auch wenn das ESI-Register nicht die Startadresse des ASM-Codes im Speicher enthält.

  • Wow, Super Tutorial.
    Es ist wirklich sehr gut aufgebaut und erklärt.
    Assembler ist zwar eine schwere "Sprache", aber man kann dem Tutorial trotzdem gut folgen.
    Ich habe mich schon immer gefragt, was dieses mov eax und so macht, jetzt weiß ich es.
    Gute Arbeit, und von mir ein großes Lob :thumbup: :thumbup:

  • Beispiel Stringbehandlung und Pointer auf AutoIt-Variablen siehe auch einen Post obendrüber!


    Im folgenden Beispiel stelle ich rudimentäre Stringbearbeitung vor.
    An den Assemblercode soll ein String übergeben werden. Man kann den String nicht direkt übergeben, denn es sind nur maximal 4 Bytes als Parameter erlaubt. Also macht das mit einem Umweg über die Anfangsadresse des Strings, den sogenannten Pointer. Man übergibt also nicht den String, sondern die Adresse, an der der String im Speicher liegt. Bei Dll-Aufrufen wird dazu an den Datentyp ein Asterix * angehängt.
    Um also den String zu übergeben, benutzt man

    "str*","Hallo!" oder "str*",$string

    Nun ist es aber nicht so, daß dieser Pointer direkt auf die Anfangsadresse der Variablen (oder des Strings) zeigt, sondern auf eine Adresse, an deren Stelle der eigentliche Pointer abgelegt ist...Warum das so ist, führt zu weit, ihr wisst schon, damit werden Bücher gefüllt :D


    Im folgenden Beispiel kopiere ich einen String aus einer AutoIt-Variablen in eine Struct und lese diesen String auch wieder aus. Nebenbei erfährt man noch etwas über Ansi bzw UTF-Kodierung^^
    Ändert mal den Typ im Dll-Aufruf von "str*" in "wstr*", das "verwandelt" den String in einen Unicodestring...


    Hmmm, man erhält also einen Pointer auf die AutoIt-Variablen....na, wenn das nicht interessant ist ^^
    Damit kann man nun ALLE möglichen AutoIt-Variablen direkt an den Assembler übergeben!
    Testet das mal z.B. mit Arrays^^


    //EDIT//
    Man kann natürlich direkt Strings vom Assemblercode zurückgeben lassen, ohne eine Struct
    Wenn der Pointer auf den String im EAX register steht vor der Rückgabe zu AutoIt, dann erhält man, wenn man als Rückgabetyp "str" oder "wstr" in der Dll wählt, den String direkt in $ret[0]


    ;Assemblercode...
    mov eax,Adresse_an_der_der_String_im_Speicher_steht ;z.B. eine Variable
    ret
    ;Ende Assemblercode


    $ret = DllCall("user32.dll", "str", "CallWindowProcW", ;oder "wstr" für Unicodestrings
    Msgbox(0,0,$ret[0]) ;zeigt den String

  • Aufruf von "Funktionen" mit CALL-Befehl, Beispiel Rekursion Fakultät


    Im folgenden Beispiel wird der CALL-Befehl zum rekursiven Aufrufen einer Unterfunktion benutzt.
    Fakultät => 5! = 5 * 4 * 3 * 2 * 1


    Die Zahl, deren Fakultät bestimmt werden soll, wird an ECX übergeben.
    Es wird geprüft, ob ECX 0 oder 1 ist, Ergebnis ist dann 1.
    Weiterhin wird überprüft, ob ECX>12 ist (oder negativ). Dann wird als Ergebnis 0 ausgegeben.
    12! ist deshalb oberes Limit, weil EAX nur maximal 32 Bit große Werte aufnehmen kann. (Hausaufgabe: erweitert das Programm so, daß der volle 64 (128 ) Bit große Zahlenraum ausgenutzt werden kann)
    Der MUL-Befehl schreibt, wenn das Ergebnis einer Multiplikation > 32 Bit ist, den "Übertrag" in das (E)DX-Register.
    Man hat also für das Ergebnis 64 Bit, und zwar EDX:EAX. Diese 64 Bit werden in die vorher von AutoIt angelegte Struct geschrieben.


    Dann wird eine Funktion (Multiplizieren) geCALLed. Bei einem Call wird die aktuelle Adresse des Call-Befehls (EIP-Register IP=Instruction-Pointer) auf den Stack gepushed. Bei Stackoperationen inenrhalb der Funktion also den Stack im Auge behalten!
    Die Funktion "Multiplizieren" multipliziert EAx=EAX*ECX, und DECrementiert ECX in jedem Durchlauf um 1.
    Ist ECX = 0, wird die Rekursion unterbrochen, und per RET aus der Funktion zurückgesprungen. Das RET (des zugehörigen CALLs) popt nun den EIP vom Stack, damit wird das Programm an der Stelle fortgesetzt, an der der CALL-Aufruf stand.


    Ist das Programm aus der letzten Rekursion zurückgekehrt, werden EDX und EAX in die 64-Bit-Struct des AutoItprogramms geschrieben und das Assemblerprogramm verlassen.


    Die Überprüfung der Assemblersyntax habe ich auch implementiert.


    //EDIT//
    das ganze als AutoIt-Funktion

  • Schade, dass wir in der Schule mit einem anderen Assembler anfangen :/


    Benutzen einen für Mikrocontroller Programmierung, aber der hat nicht die Register EAX, EBX, ECX usw. sondern nur r0 - r31 und auch die Befehle sind anders.


    Dort gibt es kein JE, JNZ, JNE usw :/


    Gibt es bei FASM eigentlihc auch den Befehl ldi?



    Ansonsten super Tutorial :thumbup:

  • Pinguin, ich gehe einfach davon aus, daß die "Standard"-Befehle bei allen Microcontrollern und Prozessoren ähnlich sind, kannste einen, kannste alle^^


    @Sprenger, schau mal IM FASM-Forum nach den Beispielen für DOS.
    Eine EXE-Datei ist völlig anders aufgebaut und strukturiert wie eine COM-Datei. Um eine EXE zu erstellen, nehme ich immer eine "Vorlage", fülle diese mit Code und jage sie durch den Assembler (FASM oder FASMW)
    1. Bsp für GUI (RAW-Mode, den keiner braucht...)


    2.Bsp: GUI per #include, so wie sich das gehört^^


    Einfache EXEs brauchen diesen Aufwand nicht, da reicht ein

    um die Nachricht in die Konsole zu schreiben


    Aber um mit dem Assembler EXE-files zu basteln ist das der falsche Thread, mach doch einen in OT auf!