In this case, which C compiler you are using (and in some cases, what OS you're developing under) would be more relevant than which assembler you using. Assuming you're using gcc under Linux, then the rules are as follows:
The registers EBX, ESP, EBP, ESI, and EDI are sacred; any function that uses them has to save them first and restore them before returning.
All other user-available registers are not preserved; if you have a value in them hat you'll need later, you have to save them before calling a C function, and restore them afterwards.
Return values 32 bits or less in size are passed through EAX; 64-bit values through EDX and EAX, with the high dword in EDX; all larger values are passed by value in EAX.
Arguments pushed on the stack are cleaned up after return, by the calling function; and
Arguments are pushed onto the stack frame in reverse order.
Calling a C function is relatively simple: you would want to save anything you didn't want to lose (EAX, etc.), and push the arguments to it in the correct (reversed) order. That is, to call
int foo(int bar, char baz, float quux);
you would push them as
mov EAX, quux
push EAX
mov AX, baz
push EAX
mov AL, bar
push EAX
call foo
The return value would be in EAX when the call completes. You would then have to clear them off the stack yourself, probably by the expedient of
mov ECX, ESP
sub ECX, 2 + 1 + 1 ; one quadword, one dword, and one less than dword
mov ESP, ECX
Handling a call from a C function is a bit more work, as you have to deal with the frame offsets of the variables.
Assuming that fee(int fie, char foe, float fum) were your assembly function, then the C caller would pass the variables as shown here:
addr stack contents
------------------------------------------
... | ... |
FC28 | (caller's stack frame) |
FC24 | fum[4] | fum[5] | fum[6] | fum[7] |
FC20 | fum[0] | fum[1] | quux{2]| fum[3] |
FC1C | foe[0] | (empty)| (empty)| (empty)|
FC18 | fie[0] | fie[1] | fie[2] | fie[3] |
FC14 | (return pointer) |<------ stack pointer at calling time
(Addresses are arbitrary). Everything between the stack pointer (ESP) and the previous function's stack is pushed on the stack at calling time by the caller. As a matter of convention, a C function would then push the EBP of the
old program and replace it with the ESP; this allows the caller to use the EBP as a frame pointer, while the stack is now free to be used. In the case of a C program, they would then push any other registers the function uses, followed by the auto variables. Past that, the stack would be used for temporary storage as usual.
When coding in assembly, you do not have to follow these conventions, as long as you are careful not to trash the arguments or the sacred registers, It is probably a wise idea, however, as it reduces the number of possible points of confusion. In any case, I will assume you are following them, in the examples below, to illustrate the issue clearly.
Given that, let's posit that fee() has two auto variables, 'int bang;' and 'char boom;', which would then go on the stack this way:
addr stack contents
------------------------------------------
... | ... |
FC28 | (caller's stack frame) |
FC24 | fum[4] | fum[5] | fum[6] | fum[7] |
FC20 | fum[0] | fum[1] | quux{2]| fum[3] |
FC1C | foe[0] | (empty)| (empty)| (empty)|
FC18 | fie[0] | fie[1] | fie[2] | fie[3] |
FC14 | (return pointer) |
FC10 | (caller's EBP) |<------ current EBP points here
FC0C | (caller's EBX) |\
FC08 | (caller's ESI) | > only needed if you use these regs
FC04 | (caller's EDI) |/
FC10 | bang[0]| bang[1]| bang[2]| bang[3]|
FBFC | boom[0]| (empty)| (empty)| (empty)|
FBF8 | (temporary values) |<------ ESP now here
... | ... |
------------------------------------------
To access an argument on the stack, you need to use pointer offsets from the EBP. For example, to copy foe to boom by way of BL, you would do something like this (assuming you've already saved EBX):
mov bl, [EBP + 0x08]
mov [EBP - 0x10], bl
Normally the work of tracking the offsets is done by the compiler automagically, but when calling an assembly routine, the coder has to provide them manually. It may be useful to use a struct definition to help keep track of them. The struct should be in the calling order, not the order on the stack, so that the offsets are positive from the base pointer.
struc fee_param
.fie resw 1
.foe resb 1,3
.fum resd 2
enstruc
struc fee_local
.boom resb 1,3
.buzz resw 1
I believe you can then replace the previous with a construct like
mov bl, [EBP + fee_param.foe]
mov [ESP - fee_local.boom], bl
[Sources: _Assembly Language Step-by-step_ by Jeff Dunteman; the DJGPP homepage (
http://www.delorie.com/djgpp/doc/ug/asm/calling.html); Linux Assembly HOWTO (
http://www.linux-embedded.com/howto/@$$ ... WTO-5.html)]
Return values, as stated before, are passed through EAX and EDX.
HTH. Corrections and additions are solicited and appreciated. TIA.
(Considered Silly: All this talk of function environments and state makes me wonder if you could feasibly write a library for C to support continuations... ah, the hell with it, it would either be a real mess, or would be highly non-portable, or both. Too little state to save, with too much of it being hardware specific. Still, it would be interesting, in a 'I can't believe it really works' sort of way...)