You write the function, Asmonite writes the frame.
With func and call Name(...) you write functions with parameters and local variables, and on this page you always see next to it which assembler instructions it becomes. It is not a new language: before assembling, the text is turned into plain lines, and the machine code is the same you would have written by hand.
For Windows (32 bit, .arch i386). The examples below are files in the folder examples\func of the ZIP.
A function with two parameters. On the left what you write, on the right what the assembler makes of it.
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
your line, the names are replaced
unchanged
a and b are the parameters: dword slots at [ebp+8] and [ebp+12]. You need not count the offsets or remember ret 8. Error messages and the debugger show the lines of your text, not the generated ones: one step (F8) runs a whole line of func, end or call.
Seven building blocks. Everything else in a function is plain assembler.
| Written as | Meaning |
|---|---|
func Name(p1, p2:word) cdecl uses ebx | Starts a function. Name is also a label. Parameters are dword slots at [ebp+8], [ebp+12] …; a type (byte, word, dword) only sets the width of the access. stdcall is the default, cdecl has to be written. |
var a, b:byte, buf:16 | Local variables below ebp: a dword (no type), byte/word/dword (each takes 4 bytes) or an array of N bytes. |
uses ebx, esi | Saves registers behind the locals and restores them before the function ends. |
begin | The frame goes here: push ebp, mov ebp, esp, sub esp, n and the saved registers. |
ret | A lone ret inside the function is the epilog: registers back, leave, ret or ret 4*n. This is how you leave a function early. |
end | End of the function. Adds the epilog unless the last statement was a ret already. |
call Name(a, b) | Pushes the arguments from right to left, calls, and cleans up for cdecl (see below). |
A name stands for the memory location, not for a register. The assembler puts the right address in.
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
The frame here: 24 bytes for the locals (temp 4, buffer 16, small 4 – a byte still takes a whole dword), behind them the registers saved by uses. The single ret before end is the epilog; so end adds no second one. The function is cdecl, so it ends with a plain ret.
| In the source | Becomes | Note |
|---|---|---|
mov eax, count | mov eax, dword [ebp+8] | a dword parameter or variable stands for its value |
mov cx, flags | mov cx, word [ebp+12] | declared with :word or :byte, that width is used |
lea edx, buffer | lea edx, [ebp-20] | an array stands for its address |
mov buffer+4, eax | mov [ebp-16], eax | name+4 is the second dword of the array |
mov eax, [buffer+ebx*4] | mov eax, [ebp-20+ebx*4] | an array name in brackets works, a simple variable in brackets is an error |
Names must not be registers, instructions or keywords; a name like ch (the register CH) is refused with an error message.
A ret in the middle of a function is the epilog, at every place where you want to leave the function.
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
Both ret paths restore esi (uses esi) and take the frame down. Without func you would have written pop esi, pop ebp and the right ret form by hand at each of these places, and after a later change of the locals counted every sub esp and [ebp-…] again.
call Name(...)A call in one line. The assembler pushes the arguments and, where needed, cleans up behind the call.
| You write | The assembler produces | Note |
|---|---|---|
call Add2(7, 5) | push 5 push 7 call Add2 | Add2 is a func without a word: stdcall. The function cleans up itself (ret 8). |
call Sum3(10, ebx, 11) | push 11 push ebx push 10 call Sum3 add esp, 12 | Sum3 is declared cdecl: the call cleans up with add esp, 12. An argument may be a register. |
call [MessageBoxA](0, text, caption, 0) | push 0 push dword caption push dword text push 0 call [MessageBoxA] | An import in square brackets: stdcall. A label as an argument is its address. |
call [printf](fmt, 42) | push 42 push dword fmt call [printf] add esp, 8 | The C runtime is cdecl. The line .import msvcrt.dll cdecl printf says so; after that this call is enough. |
An argument is a register, a number, a label (its address), a parameter or local of the function (a dword: its value; a byte/word: zero extended to 32 bits; an array: its address), &name (the address of a local) or a memory operand like [ebx+4].
The address of an array, &name and a byte or word value cannot be pushed directly. The assembler uses the first of eax, ecx, edx that the argument list does not name. In the example eax is an argument (the length), so for &written it takes 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
If the list names all three registers, none is free: that is an error with a line number. esp is not allowed in an argument.
stdcall or cdecl?Both push the arguments from right to left. They differ in who takes them off the stack again.
stdcall (default) | cdecl | |
|---|---|---|
| Who cleans up | the function: ret 4*n | the caller: add esp, 4*n behind the call |
| Typical | Windows functions (kernel32, user32 …) | the C runtime (printf, scanf …), functions with a varying number of arguments |
| In Asmonite | default for func, labels and imports | func … cdecl, call cdecl Name(…), .import msvcrt.dll cdecl printf |
A func knows its convention itself. A call Sum3(…) takes it from the func line, and the number of arguments is checked (a wrong number is an error). For a label without func and for an import, stdcall is assumed unless you say otherwise.
For imports you say it once in the .import line: .import msvcrt.dll cdecl printf, scanf. For the C runtime dlls (msvcrt, ucrtbase, api-ms-win-crt-*) the word is required: a call with arguments without it is an error, so the wrong convention cannot go unnoticed. For a single call, call cdecl [name](…) and call stdcall [name](…) decide.
Why it matters: With the wrong convention the stack is 4*n bytes too high or too low after the call. The function usually still returns correctly; the crash comes later, when a function further up ends with a wrong return address. In the debugger you see it in the value of ESP before and after the call.
An error always shows the line of your source.
func linecall cdecl on a stdcall func is an errorcdecl in a call with arguments[…]eax, ecx, edx has to be free.arch i386 (32 bit); x64 and ARM have other conventionsebp; no structures by valuecall Name(…) stands alone on the line (a comment behind it is allowed)eax; func has no return typesbegin, end and call Name(…) are several instructions but one step. F8 on a call line runs over the whole call including the clean-up, F7 goes into the function.
When the program is paused, the mouse pointer on a parameter or a local shows its current value, for an array the text or the bytes, for a pointer to a text the text too.
Messages and breakpoints refer to the source with func, not to the generated lines. F1 on a line explains the instruction and the Windows function.
In the ZIP, folder examples\func. Each file can be started in the debugger with F9 or assembled with asmoc file.asm.
| File | Shows |
|---|---|
func_add.asm | the simplest function with two parameters; exit code 12 |
func_locals.asm | locals, an array, uses and an early ret |
func_call.asm | call Name(…) with numbers, registers and both conventions |
func_call_locals.asm | array, byte and & as arguments, the scratch register |
func_cdecl.asm | a cdecl function and wsprintfA into a local buffer |
func_printf.asm | printf from the C runtime with .import msvcrt.dll cdecl printf |
func_console.asm, func_file.asm | Windows functions with many arguments: console, files (seven arguments for CreateFileA) |
func_computername.asm, func_time.asm, func_yesno.asm | computer name, local time in a message box, a yes/no question with an exit code |