Skip to content

The EVM Calling Conventions

Alan.Li edited this page Apr 27, 2020 · 3 revisions

The EVM architecture is a simplistic structure, but it has everything we need to do usual program computations.

Types of calls

There are two types of calls in an EVM smart contract:

  1. Internal calls. Internal calls are referred to function calls within a smart contract. An example is that we have two defined function A and B, and somewhere in A we save our context and change our execution flow to the beginning of B.
  2. External calls. Or cross-contract calls. A and B are defined in different deployed EVM contract and A calls B in its context.

Internal call conventions

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.

Procedure of a subroutine call

To illustrate the procedure for a subroutine call, we need to do the following to save the context of current function execution:

  1. 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.
  2. 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.
  3. push all subroutine arguments in order on to stack.
  4. push return address onto stack. (At this moment, the return address is PC + 6).
  5. 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:

  1. push return value on to top of stack.
  2. Do a swap1 to move the return address to top of stack
  3. 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:

  1. restore caller's frame pointer by storing the value at location $fp - 32 to 0x40.

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:

  1. the return address stack is only accessible to VM
  2. the stack is invisible to users and compilers

A better calling convention is made with the support of EIP2315:

To generate a call procedure

  1. 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.
  2. save existing frame pointer at memory location $fp + %frame_size - 32. The frame pointer is maintained at 0x40.
  3. 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.
  4. push all subroutine arguments in order on to stack.
  5. push the beginning address of subroutine and call JUMPSUB

To generate the return

  1. push return value on to top of stack.
  2. call RETURNSUB to resume execution of caller function.

External calls

External calls are implemented using intrinsic calls.