CAVA calling convention

The calling convention of the CAVA toolchain is explained through a set of examples.

 

Example #1

 

C code:

void main(){

}

 

Assembly code:

main:

subiw $sp, $sp, -(-8)

sdl $r62, 0, $sp

cp $r62, $sp

cp $r1, $51

ldl $62, 0, $sp

addiw $sp, $sp, 8

j $r63

 

At the benning of a function, the stack pointer reservers as many words as necessary to place its arguments, local variables, stack pointer and return address. 

In this simple example (no arguments, no local variables, no return address) we just reserve 8B for the stack pointer (64bits architecture). The register r62, contains the stack pointer of the function that called this function. Then we save our current stack pointer (sp) in r62 (the same way our parent did). 

r1 is the register used for storing the return address of a function. r51 is the first register gcc allocates for computation. In this example, main doesn't return anything so it doesn't have a meaning. 

Then, we just rollback and restore the stack as it was before being called by our parent. We load in r62 the stack pointer of our parent. We decrement our current stack pointer (sp) to how it was before. Note that now, r62 and sp should have the same value.

At last, we jump to the address specified in the register r63, which contains the return address of our parent.

 

Example #1

 

C code:

void main(){

  int a = 0;

  a = a + 1;

}

 

Assembly code:

 

main:

 

subiw $sp, $sp, -(-16)

sdl $r62, 8, $sp

cp $r62, $sp

ei $51, 0

sdw $r51, 0, $r62

ldw $51, 0, $r62

addiw $r51, $r51, 1

sdw $r51, 0, $r62

cp $r1, $r51

ldl $62, 8, $sp

addiw $sp, $sp, 16

j $r63

 

The first three instructions are the ones that save the context of the stack, Note that gcc allocates chucks multiple of 8B. We reserved space for the stack pointer of our parent (8B) and the global variable (4B). However, the stack pointer decreases by 16B instead of 12B. The stack looks like this:

 

 

Then, $r51 is given the value 0 and the next store, updates the local variable A with the content of r51. This comprises the initialization of the local variable.

After that, we load the local variable into r51 and we increment it by one. After thant, we update the local variable by storing r51 into the stack.

The rest is similar to example #1.

 

 

Example #3

 

C code:

void foo(){

}

void main(){

int a = 0;

foo();

}

 

Assembly code:

foo:

 

subiw $sp, $sp, -(-8)

sdl $r62, 0, $sp

cp $r62, $sp

ldl $62, 0, $sp

addiw $sp, $sp, 8

j $r63

main:

subiw $sp, $sp, -(-24)

sdl $r63, 8, $sp

sdl $r62, 16, $sp

cp $r62, $sp

ei $51, 0

sdw $r51, 0, $r62

call foo

cp $r1, $51

ldl $62, 0, $sp

addiw $sp, $sp, 8

j $r63

 

The first three instructions are the ones that save the context of the stack, In the top most position as usual, we save the old stack pointer. Then, we save the return address, which is allocated in r63. We do that because when we call the function foo, it will automatically save PC + 4 in r63, erasing any previous content in that register. The stack currently looks like this:

 

 

Then, we initialize the local variable a as in the previous example.

Afterwards, we call the foo function. This automatically saves the return address in the register r63. The foo function is really similar to the main function in example #1. The only difference is that foo doesn't return anything, therefore there is no cp $r1, $r51 instruction. Notice that main shouldn't return anything because it's defined as void but the calling convention states that main needs to return a value always.

The rest is the same as in example #1 and #2.