func – Funktionen ohne Stackframe-Handarbeit

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.

Die Idee in einem Bild

Eine Funktion mit zwei Parametern. Links steht, was du schreibst, rechts, was der Assembler daraus macht.

Du schreibst
func Add2(a, b)
begin
  mov eax, a
  add eax, b
end
Der Assembler erzeugt
Add2:
  push ebp
  mov ebp, esp
  mov eax, dword [ebp+8]
  add eax, dword [ebp+12]
  pop ebp
  ret 8
von 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.

Die Syntax

Sieben Bausteine. Alles andere in einer Funktion ist gewöhnlicher Assembler.

SchreibweiseBedeutung
func Name(p1, p2:word) cdecl uses ebxBeginnt 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:16Lokale Variablen unter ebp: ein Dword (ohne Typ), byte/word/dword (jede belegt 4 Bytes) oder ein Feld mit N Bytes.
uses ebx, esiRettet Register hinter den Lokalen und stellt sie vor dem Ende der Funktion wieder her.
beginHier steht der Rahmen: push ebp, mov ebp, esp, sub esp, n und die geretteten Register.
retEin ret allein in der Funktion ist der Epilog: Register zurück, leave, ret bzw. ret 4*n. So beendest du eine Funktion vorzeitig.
endEnde 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).

Parameter, Lokale und Felder

Ein Name steht für die Speicherstelle, nicht für ein Register. Der Assembler setzt die richtige Adresse ein.

Du schreibst
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
Der Assembler erzeugt
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 QuelltextWird zuHinweis
mov eax, countmov eax, dword [ebp+8]ein Dword-Parameter oder eine Variable steht für ihren Wert
mov cx, flagsmov cx, word [ebp+12]mit :word oder :byte deklariert, gilt die Breite
lea edx, bufferlea edx, [ebp-20]ein Feld steht für seine Adresse
mov buffer+4, eaxmov [ebp-16], eaxname+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.

Vorzeitig beenden und Register retten

Ein ret mitten in der Funktion ist der Epilog, an jeder Stelle, an der du die Funktion verlassen willst.

Du schreibst
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
Der Assembler erzeugt
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.

Aufrufe mit Argumenten: 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 schreibstDer Assembler erzeugtAnmerkung
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].

Adressen und kleine Werte brauchen ein Register

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:

Du schreibst
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
Der Assembler erzeugt
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 aufdie Funktion: ret 4*nder Aufrufer: add esp, 4*n hinter dem call
TypischWindows-Funktionen (kernel32, user32 …)die C-Laufzeit (printf, scanf …), Funktionen mit wechselnder Argumentzahl
In AsmoniteStandard für func, Labels und Importefunc … 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.

Was geprüft wird, und was nicht geht

Ein Fehler zeigt immer die Zeile deines Quelltextes.

Wird geprüft

  • Zahl der Argumente eines Aufrufs gegen die func-Zeile
  • Konvention: call cdecl auf eine stdcall-func ist ein Fehler
  • C-Laufzeit ohne cdecl bei einem Aufruf mit Argumenten
  • Namen: keine Register, Befehle oder Schlüsselwörter; keine einfache Variable in […]
  • Hilfsregister: eines von eax, ecx, edx muss frei sein

Grenzen

  • nur .arch i386 (32 Bit); x64 und ARM haben andere Konventionen
  • Parameter sind immer Dwords im Rahmen mit ebp; keine Strukturen als Wert
  • der Aufruf call Name(…) steht allein in der Zeile (ein Kommentar dahinter ist erlaubt)
  • der Rückgabewert bleibt in eax; func kennt keine Rückgabetypen

Im Debugger

Ein Schritt je Quellzeile

begin, 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.

Werte unter der Maus

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.

Fehler in deiner Zeile

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.

Beispiele zum Ausprobieren

Im ZIP, Ordner examples\func. Jede Datei lässt sich mit F9 im Debugger starten oder mit asmoc Datei.asm übersetzen.

DateiZeigt
func_add.asmdie einfachste Funktion mit zwei Parametern; Exit-Code 12
func_locals.asmLokale, ein Feld, uses und ein vorzeitiges ret
func_call.asmcall Name(…) mit Zahlen, Registern und beiden Konventionen
func_call_locals.asmFeld, Byte und & als Argumente, das Hilfsregister
func_cdecl.asmeine cdecl-Funktion und wsprintfA in einen lokalen Puffer
func_printf.asmprintf aus der C-Laufzeit mit .import msvcrt.dll cdecl printf
func_console.asm, func_file.asmWindows-Funktionen mit vielen Argumenten: Konsole, Dateien (sieben Argumente bei CreateFileA)
func_computername.asm, func_time.asm, func_yesno.asmRechnername, Ortszeit im Meldungsfenster, Ja/Nein-Frage mit Exit-Code