Friday, December 4, 2009

MIPS calling convention with GCC

In this article, we are going to learn MIPS calling conventions by examples. And we will focus on the one used by GCC. All the examples will be compiled with GCC flag -mno-abicalls not generating SVR4-style position-independent code. Normally -mabicalls should be used, we disable it for the simplification of assembly codes.



First up, take a look at the most simple leaf function without any arguments.

int empty(void)
{
return 0;
}

 1 empty:
 2         addiu  $sp,$sp,-8
 3         sw     $fp,0($sp)
 4         move   $fp,$sp
 5         move   $2,$0
 6         move   $sp,$fp
 7         lw     $fp,0($sp)
 8         addiu  $sp,$sp,8
 9         j      $31
10         nop
line 2: reserve 8 bytes stack space
line 3: push the previous $fp to stack
line 4: take current $sp as the subroutine's frame pointer
line 5: set $v0 to zero ($v0 stores the return value)
line 6,7,8: pop out $fp from stack, restore back original $sp
line 9: jump to return address
line 10: branch delay slot

The function has its own stack frame. It must save the old frame pointer ($fp) before using it, and restore it back before returning to the caller. Generally speaking, if any of $16..$23 or $29..$31 is changed within the called function, it must be saved in the stack frame before use and restored from the stack frame before return from the function. It is called general register save area. As to the example, 4 bytes is enough for saving $fp, why does it reserve 8 bytes stack space? It is because that the register save area must be doubleword (8 byte) aligned.

Besides saving $fp, a non-leaf function must save return address ($ra) as well. When it calls other subroutines, the $ra will be modified for returning from the subroutines. The non-leaf function has to save $ra in order to return to its caller.
int emptycaller(void)
{
empty();
return 0;
}

 1 emptycaller:
 2         addiu  $sp,$sp,-24
 3         sw     $31,20($sp)
 4         sw     $fp,16($sp)
 5         move   $fp,$sp
 6         jal    empty
 7         nop
 8         move   $2,$0
 9         move   $sp,$fp
10         lw     $31,20($sp)
11         lw     $fp,16($sp)
12         addiu  $sp,$sp,24
13         j      $31
14         nop
line 3: push $ra onto the stack
line 4: push $fp onto the stack
line 10: pop $ra from the stack
line 11: pop $fp from the stack
In the example, 8 bytes stack space should be enough for saving $ra and $fp. The additional 16 bytes (total 24 bytes) is the function call arguemnt area, and we will dicuss about it later.

We know that arguments are passed to subroutines in argument registers ($a0, $a1, $a2, and $a3). Let's take a look at the simple leaf function with 3 arguments.
int args3(int x, int y, int z)
{
return (x + y + z);
}

 1 args3:
 2         addiu  $sp,$sp,-8
 3         sw     $fp,0($sp)
 4         move   $fp,$sp
 5         sw     $4,8($fp)
 6         sw     $5,12($fp)
 7         sw     $6,16($fp)
 8         lw     $3,8($fp)
 9         lw     $2,12($fp)
10         nop
11         addu   $2,$3,$2
12         lw     $3,16($fp)
13         nop
14         addu   $2,$2,$3
15         move   $sp,$fp
16         lw     $fp,0($sp)
17         addiu  $sp,$sp,8
18         j      $31
19         nop
line 2~4: the function prologue as described previously
line 5~7: push the arguments from $a0, $a1, and $a2 to the stack
line 8~14: add the 3 arguments, and put the sum in $v0 for the return value
line 15~17: the function epilogue as described previously
line 18~19: return to the caller
The arguments are pushed to 8($fp) to 16($fp), which are located in the caller's stack frame. A non-leaf function (Caller) will always reserve the top 4 words (16 bytes) at least to storing arguments to called functions. It is called the function call argument area.

If the maximum number of arguments to the called function is fewer than 4 words, the caller still has to reserve 4 words. Take the followed non-leaf function for example. The function reserved 24 bytes stack space instead of 8, and the additional 16 bytes is the function call argument area.
int arg3caller(void)
{
args3(5, 6, 7);
return 0;
}

 1 arg3caller:
 2         addiu  $sp,$sp,-24
 3         sw     $31,20($sp)
 4         sw     $fp,16($sp)
 5         move   $fp,$sp
 6         li     $4,5                        # 0x5
 7         li     $5,6                        # 0x6
 8         li     $6,7                        # 0x7
 9         jal    args3
10         nop
11         move   $2,$0
12         move   $sp,$fp
13         lw     $31,20($sp)
14         lw     $fp,16($sp)
15         addiu  $sp,$sp,24
16         j      $31
17         nop
line 2: reserve 24 bytes stack space
line 3~4: Since 0($sp) ~ 12($sp) is function call argument area, the old $fp will be pushed onto 16($sp), and $ra will be pushed onto 20($sp).
line 6~10: pass the arguments in $a0, $a1, and $a2

There are four argument registers ($a0, $a1, $a2, and $a3) totally. When calling a function with more than 4 arguemnts, the first 4 arguments are passed in registers, and the others are passed on the stack.
int args6(int x, int y, int z, int a, int b, int c)
{
return (x + y + z + a + b + c);
}

 1 args6:
 2         addiu  $sp,$sp,-8
 3         sw     $fp,0($sp)
 4         move   $fp,$sp
 5         sw     $4,8($fp)
 6         sw     $5,12($fp)
 7         sw     $6,16($fp)
 8         sw     $7,20($fp)
 9         lw     $3,8($fp)
10         lw     $2,12($fp)
11         nop
12         addu   $2,$3,$2
13         lw     $3,16($fp)
14         nop
15         addu   $2,$2,$3
16         lw     $3,20($fp)
17         nop
18         addu   $2,$2,$3
19         lw     $3,24($fp)
20         nop
21         addu   $2,$2,$3
22         lw     $3,28($fp)
23         nop
24         addu   $2,$2,$3
25         move   $sp,$fp
26         lw     $fp,0($sp)
27         addiu  $sp,$sp,8
28         j      $31
29         nop
line 2~4: function prologue
lin 5~8: push the first 4 arguments to the stack
line 9~18: add the first 4 arguments, and put the sum in $v0
line 19~21: fetch the 5th argument from 24($fp), and add it to $v0
line 22~24: fetch the 6th argument from 28($fp), and add it to $v0 for the return value
line 25~27: function epilogue
line 28~29: return to the caller

In order to call the 6-argument function, the caller has to push the 5th and 6th arguments onto the stack. The function call argument area becomes of length 6 words (24 bytes) instead of 4 words. It reserves total 32 bytes, 8 bytes for registers $ra and $fp, and 24 bytes for function call argument area. Note that the area will also be doubleword aligned.
int arg6caller(void)
{
args5(5, 6, 7, 8, 9, 10);
return 0;
}

 1 arg6caller:
 2         addiu  $sp,$sp,-32
 3         sw     $31,28($sp)
 4         sw     $fp,24($sp)
 5         move   $fp,$sp
 6         li     $2,9                        # 0x9
 7         sw     $2,16($sp)
 8         li     $2,10                       # 0xa
 9         sw     $2,20($sp)
10         li     $4,5                        # 0x5
11         li     $5,6                        # 0x6
12         li     $6,7                        # 0x7
13         li     $7,8                        # 0x8
14         jal    args5
15         nop
16         move   $2,$0
17         move   $sp,$fp
18         lw     $31,28($sp)
19         lw     $fp,24($sp)
20         addiu  $sp,$sp,32
21         j      $31
22         nop
line 2: reserve 32 bytes stack space
line 3~4: Since 0($sp) ~ 20($sp) is function call argument area, the old $fp will be pushed onto 16($sp), and $ra will be pushed onto 20($sp).
line 6~9: pass the 5th and 6th arguments on the stack
line 10~13: pass the first 4 arguments in argument registers

Next, we are going to see a little bit complicated example. We declare 2 variables stored in registers $s0 and $s1. The function must save them in the general register save area before using them. Besides, we declare 2 local variables (automatic variables), and they will also be saved on the top of the general register save area.
int comp(int m, int n)
{
int a = 1, b = 2;
register int x = 5, y = 6;
args6(a, b, m, n, x, y);
return (x + y);
}
Therefore, the stack frame will be like:


The assembly code line by line:
 1 comp:
 2         addiu  $sp,$sp,-48
 3         sw     $31,44($sp)
 4         sw     $fp,40($sp)
 5         sw     $17,36($sp)
 6         sw     $16,32($sp)
 7         move   $fp,$sp
 8         sw     $4,48($fp)
 9         sw     $5,52($fp)
10         li     $2,1                        # 0x1
11         sw     $2,24($fp)
12         li     $2,2                        # 0x2
13         sw     $2,28($fp)
14         li     $16,5                       # 0x5
15         li     $17,6                       # 0x6
16         sw     $16,16($sp)
17         sw     $17,20($sp)
18         lw     $4,24($fp)
19         lw     $5,28($fp)
20         lw     $6,48($fp)
21         lw     $7,52($fp)
22         jal    args6
23         nop
24         addu   $2,$16,$17
25         move   $sp,$fp
26         lw     $31,44($sp)
27         lw     $fp,40($sp)
28         lw     $17,36($sp)
29         lw     $16,32($sp)
30         addiu  $sp,$sp,48
31         j      $31
32         nop
line 3~4: save previous $ra and $fp
line 5~6: save $s0 and $s1 before using it
line 8~9: save $a0 and $a1 (arguments m and n)
line 10~13: storing local variable a and b
line 14~15: variable x and y are saved in registers $s0 and $s1
line 16~17: pass arguments x and y on stack
line 18~21: pass arguments a, b, m, and n in argument registers
line 22~23: call the subroutine
line 24: put the sum of x + y to $v0 for the return value
line 26~30: restore registers back
line 31~32: return to the caller


References:

2 comments:

kecsap said...

Hi,

as I can see you hack gcc with mipsel. I have a problem. Can you take a look on my message at gcc-help mailing list? http://gcc.gnu.org/ml/gcc-help/2010-10/msg00056.html

I could not find out your email address, therefore, I write this comment.

Thanks,
Csaba

Moritz said...

Thank you very much for this straightforward tutorial on a poorly documented topic.