currently I am working with a RISC-V processor implementation. I need to run partially hand-crafted assembly code. (Finally there will be dynamic code injection.) For this purpose I have to understand the basics of function calls within RISC-V assembly.
I found this topic very helpful: confusion about function call stack
But I am still struggling with the stack layout for a function call. Please consider the following c-code:
void some_func(int a, int b, int* c){
int cnt = a;
for(;cnt > 0;cnt--){
*c += b;
}
}
void main(){
int a = 5;
int b = 6;
int c = 0;
some_func(a,b,&c);
}
This program implements a basic multiplication by a sequence of additions. The derived assembly code (riscv64-unknown-elf-gcc -nostartfiles mul.c -o mul && riscv64-unknown-elf-objdump -D mul) looks like this:
0000000000010000 <some_func>:
10000: fd010113 addi sp,sp,-48
10004: 02813423 sd s0,40(sp)
10008: 03010413 addi s0,sp,48
1000c: fca42e23 sw a0,-36(s0)
10010: fcb42c23 sw a1,-40(s0)
10014: fcc43823 sd a2,-48(s0)
10018: fdc42783 lw a5,-36(s0)
1001c: fef42623 sw a5,-20(s0)
10020: 0280006f j 10048 <some_func+0x48>
10024: fd043783 ld a5,-48(s0)
10028: 0007a703 lw a4,0(a5)
1002c: fd842783 lw a5,-40(s0)
10030: 00f7073b addw a4,a4,a5
10034: fd043783 ld a5,-48(s0)
10038: 00e7a023 sw a4,0(a5)
1003c: fec42783 lw a5,-20(s0)
10040: fff7879b addiw a5,a5,-1
10044: fef42623 sw a5,-20(s0)
10048: fec42783 lw a5,-20(s0)
1004c: fcf04ce3 bgtz a5,10024 <some_func+0x24>
10050: 00000013 nop
10054: 02813403 ld s0,40(sp)
10058: 03010113 addi sp,sp,48
1005c: 00008067 ret
0000000000010060 <main>:
10060: fe010113 addi sp,sp,-32
10064: 00113c23 sd ra,24(sp)
10068: 00813823 sd s0,16(sp)
1006c: 02010413 addi s0,sp,32
10070: 00500793 li a5,5
10074: fef42623 sw a5,-20(s0)
10078: 00600793 li a5,6
1007c: fef42423 sw a5,-24(s0)
10080: fe042223 sw zero,-28(s0)
10084: fe440793 addi a5,s0,-28
10088: 00078613 mv a2,a5
1008c: fe842583 lw a1,-24(s0)
10090: fec42503 lw a0,-20(s0)
10094: f6dff0ef jal 10000 <some_func>
10098: 00000013 nop
1009c: 01813083 ld ra,24(sp)
100a0: 01013403 ld s0,16(sp)
100a4: 02010113 addi sp,sp,32
100a8: 00008067 ret
The important steps, that need clarification are: (some_func(int,int,int))
10060: fe010113 addi sp,sp,-32
10064: 00113c23 sd ra,24(sp)
10068: 00813823 sd s0,16(sp)
1006c: 02010413 addi s0,sp,32
and: (main())
10000: fd010113 addi sp,sp,-48
10004: 02813423 sd s0,40(sp)
10008: 03010413 addi s0,sp,48
From my understanding: The stackpointer is moved to make space for return-address and parameters. (Main might be a special case here.) How are the passed arguments treated when located on the stack? How are they obtained back? In general, the methodology is clear to me, but how would I hand-code this segment in order to work.
Regarding the related topic, the stack should look somewhat like
| ??? |
| params for some_func() <???> |
| ra of some_func() |
| locals of main() <int c> |
| locals of main() <int b> |
| locals of main() <int a> |
| params for main() <None> |
But that is pretty much it. Can anybody point out, how this is arranged, and how these two listings (function call) co-related?
The first few parameters, type permitting, are passed in registers so they don't even appear on the stack. Other than that, it's unclear what you really want to know. If you do get some arguments that are on the stack, they stay there even after you adjust the stack pointer so you can still address them relative to the adjusted stack pointer or a frame pointer (here $s0
apparently).
The important steps, that need clarification are:
10060: fe010113 addi sp,sp,-32 # allocate space
10064: 00113c23 sd ra,24(sp) # save $ra
10068: 00813823 sd s0,16(sp) # save $s0
1006c: 02010413 addi s0,sp,32 # set up $s0 as frame pointer