Du schreibst die Funktion, Asmonite den Rahmen.
Mit func und call Name(...) schreibst du Funktionen mit Parametern und lokalen Variablen, und siehst auf dieser Seite immer daneben, welche Assembler-Befehle daraus entstehen. Es ist keine neue Sprache: Vor dem Assemblieren wird der Text in gewöhnliche Zeilen umgeformt, der Maschinencode ist derselbe, den du von Hand geschrieben hättest.
Für Windows (32 Bit, .arch i386). Die Beispiele unten stehen als Dateien im Ordner examples\func des ZIPs.
Eine Funktion mit zwei Parametern. Links steht, was du schreibst, rechts, was der Assembler daraus macht.
func Add2(a, b) begin mov eax, a add eax, b end
Add2: push ebp mov ebp, esp mov eax, dword [ebp+8] add eax, dword [ebp+12] pop ebp ret 8
func ergänzt
deine Zeile, die Namen sind ersetzt
unverändert
a und b sind die Parameter: Dword-Plätze bei [ebp+8] und [ebp+12]. Du musst die Abstände nicht zählen und nicht an ret 8 denken. Fehlermeldungen und der Debugger zeigen die Zeilen deines Textes, nicht die erzeugten: ein Schritt (F8) läuft über eine ganze Zeile func, end oder call.
Sieben Bausteine. Alles andere in einer Funktion ist gewöhnlicher Assembler.
| Schreibweise | Bedeutung |
|---|---|
func Name(p1, p2:word) cdecl uses ebx | Beginnt eine Funktion. Name ist auch ein Label. Parameter sind Dword-Plätze bei [ebp+8], [ebp+12] …; ein Typ (byte, word, dword) legt nur die Breite des Zugriffs fest. stdcall ist der Standard, cdecl muss dastehen. |
var a, b:byte, buf:16 | Lokale Variablen unter ebp: ein Dword (ohne Typ), byte/word/dword (jede belegt 4 Bytes) oder ein Feld mit N Bytes. |
uses ebx, esi | Rettet Register hinter den Lokalen und stellt sie vor dem Ende der Funktion wieder her. |
begin | Hier steht der Rahmen: push ebp, mov ebp, esp, sub esp, n und die geretteten Register. |
ret | Ein ret allein in der Funktion ist der Epilog: Register zurück, leave, ret bzw. ret 4*n. So beendest du eine Funktion vorzeitig. |
end | Ende der Funktion. Fügt den Epilog an, außer die letzte Anweisung war schon ein ret. |
call Name(a, b) | Legt die Argumente von rechts nach links ab, ruft auf und räumt bei cdecl auf (siehe unten). |
Ein Name steht für die Speicherstelle, nicht für ein Register. Der Assembler setzt die richtige Adresse ein.
func DoStuff(count, flags:word) cdecl uses ebx, esi var temp, buffer:16, small:byte begin mov eax, count mov temp, eax lea edx, buffer mov small, 7 mov cx, flags ret end
DoStuff: push ebp mov ebp, esp sub esp, 24 push ebx push esi mov eax, dword [ebp+8] mov dword [ebp-4], eax lea edx, [ebp-20] mov byte [ebp-24], 7 mov cx, word [ebp+12] pop esi pop ebx leave ret
Der Rahmen hier: 24 Bytes für die Lokalen (temp 4, buffer 16, small 4 – ein Byte belegt trotzdem ein ganzes Dword), dahinter die mit uses geretteten Register. Das einzelne ret vor end ist der Epilog; end fügt deshalb keinen zweiten an. Die Funktion ist cdecl, also endet sie mit einem einfachen ret.
| Im Quelltext | Wird zu | Hinweis |
|---|---|---|
mov eax, count | mov eax, dword [ebp+8] | ein Dword-Parameter oder eine Variable steht für ihren Wert |
mov cx, flags | mov cx, word [ebp+12] | mit :word oder :byte deklariert, gilt die Breite |
lea edx, buffer | lea edx, [ebp-20] | ein Feld steht für seine Adresse |
mov buffer+4, eax | mov [ebp-16], eax | name+4 ist das zweite Dword des Feldes |
mov eax, [buffer+ebx*4] | mov eax, [ebp-20+ebx*4] | ein Feldname in Klammern geht, eine einfache Variable in Klammern ist ein Fehler |
Namen dürfen keine Register, Befehle oder Schlüsselwörter sein; ein Name wie ch (das Register CH) wird mit einer Fehlermeldung abgelehnt.
Ein ret mitten in der Funktion ist der Epilog, an jeder Stelle, an der du die Funktion verlassen willst.
func Find(text, what) uses esi begin mov esi, text mov ecx, what again: movzx eax, byte [esi] test eax, eax jz notfound cmp eax, ecx je found inc esi jmp again notfound: xor eax, eax ret found: mov eax, esi sub eax, text end
Find: push ebp mov ebp, esp push esi mov esi, dword [ebp+8] mov ecx, dword [ebp+12] again: movzx eax, byte [esi] test eax, eax jz notfound cmp eax, ecx je found inc esi jmp again notfound: xor eax, eax pop esi pop ebp ret 8 found: mov eax, esi sub eax, dword [ebp+8] pop esi pop ebp ret 8
Beide ret-Wege stellen esi wieder her (uses esi) und räumen den Rahmen ab. Ohne func hättest du an jeder dieser Stellen pop esi, pop ebp und die richtige ret-Form von Hand schreiben müssen, und bei einer späteren Änderung der Lokalen auch jedes sub esp und [ebp-…] neu zählen.
call Name(...)Ein Aufruf in einer Zeile. Der Assembler legt die Argumente ab und räumt, wo es nötig ist, hinter dem Aufruf auf.
| Du schreibst | Der Assembler erzeugt | Anmerkung |
|---|---|---|
call Add2(7, 5) | push 5 push 7 call Add2 | Add2 ist eine func ohne Angabe: stdcall. Die Funktion räumt selbst auf (ret 8). |
call Sum3(10, ebx, 11) | push 11 push ebx push 10 call Sum3 add esp, 12 | Sum3 ist als cdecl deklariert: der Aufruf räumt mit add esp, 12 auf. Ein Argument darf ein Register sein. |
call [MessageBoxA](0, text, caption, 0) | push 0 push dword caption push dword text push 0 call [MessageBoxA] | Ein Import in eckigen Klammern: stdcall. Ein Label als Argument ist seine Adresse. |
call [printf](fmt, 42) | push 42 push dword fmt call [printf] add esp, 8 | Die C-Laufzeit ist cdecl. Das sagt die Zeile .import msvcrt.dll cdecl printf; danach genügt dieser Aufruf. |
Ein Argument ist ein Register, eine Zahl, ein Label (seine Adresse), ein Parameter oder eine Lokale der Funktion (ein Dword: sein Wert; Byte/Word: aufgefüllt auf 32 Bit; ein Feld: seine Adresse), &name (die Adresse einer Lokalen) oder ein Speicheroperand wie [ebx+4].
Die Adresse eines Feldes, &name und ein Byte- oder Word-Wert lassen sich nicht direkt ablegen. Der Assembler nimmt dafür das erste Register aus eax, ecx, edx, das in der Argumentliste nicht vorkommt. Im Beispiel kommt eax als Argument vor (die Länge), also nimmt er für &written das Register ecx:
func Show(value, style:byte) var text:32, written begin call cdecl [wsprintfA](text, fmt, value) call [WriteFile](console, text, eax, &written, 0) call [MessageBoxA](0, text, caption, style) end
Show: push ebp mov ebp, esp sub esp, 36 push dword [ebp+8] push dword fmt lea eax, [ebp-32] push eax call [wsprintfA] add esp, 12 push 0 lea ecx, [ebp-36] push ecx push eax lea ecx, [ebp-32] push ecx push dword console call [WriteFile] movzx eax, byte [ebp+12] push eax push dword caption lea eax, [ebp-32] push eax push 0 call [MessageBoxA] leave ret 8
Nennt die Liste alle drei Register, gibt es keinen freien: das ist ein Fehler mit Zeilennummer. esp ist in einem Argument nicht erlaubt.
stdcall oder cdecl?Beide legen die Argumente von rechts nach links ab. Sie unterscheiden sich darin, wer sie wieder vom Stack nimmt.
stdcall (Standard) | cdecl | |
|---|---|---|
| Wer räumt auf | die Funktion: ret 4*n | der Aufrufer: add esp, 4*n hinter dem call |
| Typisch | Windows-Funktionen (kernel32, user32 …) | die C-Laufzeit (printf, scanf …), Funktionen mit wechselnder Argumentzahl |
| In Asmonite | Standard für func, Labels und Importe | func … cdecl, call cdecl Name(…), .import msvcrt.dll cdecl printf |
Eine func kennt ihre Konvention selbst. Ein call Sum3(…) übernimmt sie von der func-Zeile, und die Zahl der Argumente wird geprüft (eine falsche Zahl ist ein Fehler). Bei einem Label ohne func und bei einem Import gilt stdcall, wenn du nichts anderes sagst.
Bei Importen sagst du es einmal in der .import-Zeile: .import msvcrt.dll cdecl printf, scanf. Für die DLLs der C-Laufzeit (msvcrt, ucrtbase, api-ms-win-crt-*) ist das Wort Pflicht: ein Aufruf mit Argumenten ohne es ist ein Fehler, damit die falsche Konvention nicht unbemerkt bleibt. Für einen einzelnen Aufruf entscheiden call cdecl [name](…) und call stdcall [name](…).
Warum das wichtig ist: Mit der falschen Konvention bleibt der Stack nach dem Aufruf um 4*n Bytes zu hoch oder zu niedrig. Die Funktion kehrt meist noch richtig zurück, der Absturz kommt erst später, wenn eine Funktion weiter oben mit einer falschen Rücksprungadresse endet. Im Debugger siehst du es am Wert von ESP vor und nach dem Aufruf.
Ein Fehler zeigt immer die Zeile deines Quelltextes.
func-Zeilecall cdecl auf eine stdcall-func ist ein Fehlercdecl bei einem Aufruf mit Argumenten[…]eax, ecx, edx muss frei sein.arch i386 (32 Bit); x64 und ARM haben andere Konventionenebp; keine Strukturen als Wertcall Name(…) steht allein in der Zeile (ein Kommentar dahinter ist erlaubt)eax; func kennt keine Rückgabetypenbegin, end und call Name(…) sind mehrere Befehle, aber ein Schritt. F8 auf einer Aufrufzeile läuft über den ganzen Aufruf samt Aufräumen, F7 geht in die Funktion hinein.
Hält das Programm an, zeigt der Mauszeiger auf einem Parameter oder einer Lokalen deren aktuellen Wert, bei einem Feld den Text oder die Bytes, bei einem Zeiger auf einen Text auch den Text.
Meldungen und Haltepunkte beziehen sich auf den Quelltext mit func, nicht auf die erzeugten Zeilen. F1 auf einer Zeile erklärt den Befehl und die Windows-Funktion.
Im ZIP, Ordner examples\func. Jede Datei lässt sich mit F9 im Debugger starten oder mit asmoc Datei.asm übersetzen.
| Datei | Zeigt |
|---|---|
func_add.asm | die einfachste Funktion mit zwei Parametern; Exit-Code 12 |
func_locals.asm | Lokale, ein Feld, uses und ein vorzeitiges ret |
func_call.asm | call Name(…) mit Zahlen, Registern und beiden Konventionen |
func_call_locals.asm | Feld, Byte und & als Argumente, das Hilfsregister |
func_cdecl.asm | eine cdecl-Funktion und wsprintfA in einen lokalen Puffer |
func_printf.asm | printf aus der C-Laufzeit mit .import msvcrt.dll cdecl printf |
func_console.asm, func_file.asm | Windows-Funktionen mit vielen Argumenten: Konsole, Dateien (sieben Argumente bei CreateFileA) |
func_computername.asm, func_time.asm, func_yesno.asm | Rechnername, Ortszeit im Meldungsfenster, Ja/Nein-Frage mit Exit-Code |