func – functions without writing the stack frame

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.

The idea in one picture

A function with two parameters. On the left what you write, on the right what the assembler makes of it.

You write
func Add2(a, b)
begin
  mov eax, a
  add eax, b
end
The assembler produces
Add2:
  push ebp
  mov ebp, esp
  mov eax, dword [ebp+8]
  add eax, dword [ebp+12]
  pop ebp
  ret 8
added by 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.

The syntax

Seven building blocks. Everything else in a function is plain assembler.

Written asMeaning
func Name(p1, p2:word) cdecl uses ebxStarts 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:16Local variables below ebp: a dword (no type), byte/word/dword (each takes 4 bytes) or an array of N bytes.
uses ebx, esiSaves registers behind the locals and restores them before the function ends.
beginThe frame goes here: push ebp, mov ebp, esp, sub esp, n and the saved registers.
retA lone ret inside the function is the epilog: registers back, leave, ret or ret 4*n. This is how you leave a function early.
endEnd 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).

Parameters, locals and arrays

A name stands for the memory location, not for a register. The assembler puts the right address in.

You write
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
The assembler produces
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 sourceBecomesNote
mov eax, countmov eax, dword [ebp+8]a dword parameter or variable stands for its value
mov cx, flagsmov cx, word [ebp+12]declared with :word or :byte, that width is used
lea edx, bufferlea edx, [ebp-20]an array stands for its address
mov buffer+4, eaxmov [ebp-16], eaxname+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.

Leaving early and saving registers

A ret in the middle of a function is the epilog, at every place where you want to leave the function.

You write
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
The assembler produces
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.

Calls with arguments: call Name(...)

A call in one line. The assembler pushes the arguments and, where needed, cleans up behind the call.

You writeThe assembler producesNote
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].

Addresses and small values need a register

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:

You write
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
The assembler produces
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 upthe function: ret 4*nthe caller: add esp, 4*n behind the call
TypicalWindows functions (kernel32, user32 …)the C runtime (printf, scanf …), functions with a varying number of arguments
In Asmonitedefault for func, labels and importsfunc … 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.

What is checked, and what is not possible

An error always shows the line of your source.

Is checked

  • Number of arguments of a call against the func line
  • Convention: call cdecl on a stdcall func is an error
  • C runtime without cdecl in a call with arguments
  • Names: no registers, instructions or keywords; no simple variable in […]
  • Scratch register: one of eax, ecx, edx has to be free

Limits

  • only .arch i386 (32 bit); x64 and ARM have other conventions
  • parameters are always dwords in a frame with ebp; no structures by value
  • the call call Name(…) stands alone on the line (a comment behind it is allowed)
  • the result stays in eax; func has no return types

In the debugger

One step per source line

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

Values under the mouse

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.

Errors in your line

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.

Examples to try

In the ZIP, folder examples\func. Each file can be started in the debugger with F9 or assembled with asmoc file.asm.

FileShows
func_add.asmthe simplest function with two parameters; exit code 12
func_locals.asmlocals, an array, uses and an early ret
func_call.asmcall Name(…) with numbers, registers and both conventions
func_call_locals.asmarray, byte and & as arguments, the scratch register
func_cdecl.asma cdecl function and wsprintfA into a local buffer
func_printf.asmprintf from the C runtime with .import msvcrt.dll cdecl printf
func_console.asm, func_file.asmWindows functions with many arguments: console, files (seven arguments for CreateFileA)
func_computername.asm, func_time.asm, func_yesno.asmcomputer name, local time in a message box, a yes/no question with an exit code