-
Notifications
You must be signed in to change notification settings - Fork 21
The EVM Calling Conventions
The EVM architecture is a simplistic structure, but it has everything we need to do usual program computations.
There are two types of calls in an EVM smart contract:
-
Internal calls. Internal calls are referred to function calls within a smart contract. An example is that we have two defined function
A
andB
, and somewhere inA
we save our context and change our execution flow to the beginning ofB
. -
External calls. Or cross-contract calls.
A
andB
are defined in different deployed EVM contract andA
callsB
in its context.
Up to ETH 1.5, there is no link and jump EVM opcode for easy handling of subroutines(even though some discussions are on-going). So we have to manually handle subroutine calls. Here are the calling conventions for an internal calls:
- current subroutine's frame pointer is saved at stack, at memory location
$fp - 32
where$fp
is the subroutine call's frame pointer. - arguments are all pushed on stack, along with the return address. Argument with smaller index number occupies a stack slot on top of another argument with a larger index number. For example, when we want to do a function call:
func abc(x, y, z)
, here is the arrangement of the arguments:
+-----------+
|Return Addr|
+-----------+
| X |
+-----------+
| Y |
+-----------+
Current FP | Z |
+------------> +-----------+
| Old FP |
+-----------+
| ..... |
+-----------+
Note: Putting the return address on top of the stack is because it is easier to compute the location, but this will result in more stack manipulation overhead for the subroutine calls. We will improve this design in a later version.
- A subroutine's return value is stored on stack top. Note: currently we only support one return value. In the future we will improve it by supporting multiple return values.
To illustrate the procedure for a subroutine call, we need to do the following to save the context of current function execution:
- calculate the current frame size. The frame size should be the size sum of: a) slots occupied by frame objects, b) slots occupied by spilled variables, and c) one more slot for storing current frame pointer. let's assume the frame size is calculated to be
%frame_size
. - bump the frame pointer to:
$fp = $fp + %frame_size
. After that, we can easily restore the old frame pointer by looking at location$fp - 32
. - push all subroutine arguments in order on to stack.
- push return address onto stack. (At this moment, the return address is
PC + 6
). - push the beginning address of subroutine and jump.
Right before we return from a subroutine, the stack should be empty and the return address should be at the top of the stack. When returning from a subroutine call, we should do the following:
- push return value on to top of stack.
- Do a
swap1
to move the return address to top of stack - jump to return address and resume the execution in caller function. If the function returns nothing, simply jump to return address.
After jumping back to caller, we have to resume the execution:
- restore caller's frame pointer by storing the value at location
$fp - 32
to0x40
.
EIP2315 Support: Subroutine calls
The support of subroutines inside EVM enables compiler to generate better performance code. To be more specific: With EIP235, it is up to EVM to maintain the stack:
- the return address stack is only accessible to VM
- the stack is invisible to users and compilers
A better calling convention is made with the support of EIP2315:
- calculate the current frame size. The frame size should be the size sum of: a) slots occupied by frame objects, b) slots occupied by spilled variables, and c) one more slot for storing current frame pointer. let's assume the frame size is calculated to be
%frame_size
. - save existing frame pointer at memory location
$fp + %frame_size - 32
. The frame pointer is maintained at0x40
. - bump the frame pointer to:
$fp = $fp + %frame_size
. After that, we can easily restore the old frame pointer by looking at location$fp - 32
. - push all subroutine arguments in order on to stack.
- push the beginning address of subroutine and call
JUMPSUB
- push return value on to top of stack.
- call
RETURNSUB
to resume execution of caller function.
External calls are implemented using intrinsic calls.