On 21/07/2017 07:38, James Harris wrote:
...
> Function calls are more of a challenge but I have worked out a way that
> I think will work. I'll post about it separately.
Here are the translations I came up with for function definitions and
function calls. As an anorak, I thought this was fun and will go into
the details but YMMV. It is a long post, and perhaps one to read only if
you are really, really interested!
All values in the following will be of the same type: 32-bit words.
When translating on the fly, function-call code comes up in four
contexts: encountering the start of a function definition, coming to the
end of a function definition, encountering a return statement, and
calling a function. All work with a common calling convention. Central
to that is the layout of the stack frame. For that, I ended up with the
following form (top of stack, low addresses first).
Callee-saved registers
Array of temporaries, if needed
Named locals (at EBP - offset)
Old EBP <-- pointed at by current EBP
Return address
Parameters (at EBP + offset)
In other words, during execution that frame is built from the bottom up:
parameters first, and so on.
The layout is conventional. And I found that the assembler provides
facilities which work with the convention and make such a stack frame
easy to use. As below.
Start of a function definition
==============================
Say we come across source which starts the definition of a function. In
C terms it would look something like
void f1 (int a, int b) {
int i;
int j;
It would be translated to
section .text
;******************************
;
; function f1
;
;******************************
f1:
%push
%stacksize flat
%arg a :dword
%arg b :dword
%assign %$localsize 0
%local i :dword
%local j :dword
push ebp
mov ebp, esp
sub esp, %$localsize + _f1_tempsize
push the usual callee-save regs
In that,
%push pushes a new assembler "context" - maybe akin to a namespace
%stacksize flat tells NASM to address relative to EBP
%arg names an argument
%assign %$localsize, 0 sets/resets a name which %local uses
%local names a local/automatic identifier
None of those generate any code. They merely set up assembler symbols.
The rest, from PUSH EBP, does generate code and is a standard function
prologue. It sets up the stack frame as described above.
Crucially, within the function, references to names now relate to the
right names. As if by magic!
* A local, L, is assembled as [EBP - offset]
* An argument, A, is assembled as [EBP + offset]
* A global, G, is assembled as [G]
Maybe that's cheating. I was going to address the symbols manually using
structs and all kinds of good stuff. But, at least for now, the inbuilt
Nasm facilities seem to work well and make other things easier. So I've
taken off the hair shirt for a moment. Ahh!
End of a function definition
============================
What goes up must come down. And a function started must end. When we
parse the end of the function definition there's obviously the need to
restore registers and return etc. But I found it helpful to have labels
that allowed two types of exit: fall into, with a named return, and
return with a preset EAX which "return val" can jump to. The function
end, then:
_f1_return:
mov eax, [retval]
_f1_return_value: ;Already in EAX
pop callee-save regs
mov esp, ebp
pop ebp
ret
_f1_tempsize equ N * 4 ;See below
%pop
That has underscores in front of its labels as I only use non-underscore
names for names which appear in the source. A function called "fred"
would begin with the label
fred:
and end with the labels
_fred_return:
_fred_return_value:
The convention of copying names to the output is intended to make it
easier to work with assembly - though I know that many compilers mangle
original names in different ways, including prepending an underscore.
The "_f1_tempsize equ N" line in the above sets the size in bytes of the
temporaries needed by this function. There may be none. In fact, I
haven't needed any yet. But they might be needed by expression
evaluators. And the number of them needed by evaluations in this
function, N, won't be known until we get to the end of the function.
Hence, the line appears here.
The %pop removes the context (which appears to be an assembler
namespace) which was pushed at the start.
To return
=========
A return statement within the function would compile simply.
return 53;
would become
mov eax, 53
jmp _f1_return_value
Function calls
==============
Function /calls/ are conventional but they do have an interesting
requirement in that the above %arg directive expects its arguments to be
named in the order they appear on the stack. That means they have to be
pushed in reverse order. And that, in turn, presents a challenge to a
compiler which is supposed to be working through the source in sequence.
Fortunately, it is easy to evaluate the arguments in order and to push
them in reverse order. And I do mean "evaluate", not just copy.
Arbitrary expressions can form arguments just as easily as constants or
identifier names. And they can end up on the stack in the right (i.e.
reverse) order. Pretty cool!
It does take up twice as much stack but, remember, this is a proof of
concept, not an optimising compiler. If it works, then it works. And
that's what's being sought now.
The scheme is as follows. To evaluate
f1(a, b, c)
where a, b and c can be arbitrary expressions and need to be pushed in
reverse order I found it easiest to "evaluate and push" them in order
and then to re-push each one in reverse order. So f1(a,b,c) would be
compiled to:
{evaluate a}
push eax
{evaluate b}
push eax
{evaluate c}
push eax
push dword [esp + 0]
push dword [esp + 8]
push dword [esp + 16]
call f1
add esp, (2 * 3) * 4 ;NParms is 3
That leaves the result in EAX, as was the adopted convention.
The offsets from ESP are easy to generate as they always go up by twice
the word size.
Note that I could avoid the 4 bytes used in duplicating the last
parameter but it's not worth the extra work for this application. And
keeping the translator simple makes it easier to apply the same
translation algorithm to any number of arguments. For example,
f1(a)
becomes
{evaluate a}
push eax
push dword [esp + 0]
call f1
add esp, (2 * 1) * 4 ;NParms is 1
and plain
f1()
becomes
call f1
add esp, (2 * 0) * 4 ;NParms is 0
Sundries
========
I think that's pretty much it on function calling. I should mention that
the assembler puts locals at addresses which are in reverse order to the
order they appear in the source but that shouldn't be an issue unless
you want to step between them. Arguments are in source order. (The
assembler builds its lists of both away from EBP but in opposite
directions.)
Once a compiler can generate function calls then all sorts of useful
facilities can be pre-provided by the compiler writer - perhaps to do
things which would be hard or impossible to express in a proto-language.
As an instance, in the above case, even though the language supports
only 32-bit integers, 16-bit and 8-bit operations could be carried out
by library functions. And, certainly, all IO can be carried out by
library calls.
Because there would be no type checking, I think the above would let a
compile take place even without having a symbol table - making this
basic compilation yet simpler! If so, the assembly step would pick up
absent names but not type mismatches. It would not pick up mismatches in
the number of parameters. But, given that I chose the convention to have
callers tidy the stack, it would automatically allow variadic parameter
lists such as the va_args stuff used in printf.
--
James Harris