C函数调用和汇编代码分析

C函数调用和汇编代码分析

risc-v C函数调用反汇编示例

首先有一段C程序,其中main函数调用子函数add

1
2
3
4
5
6
7
//main.c 
int add(int a, int b){
return a + b ;
}
int main(void){
return add(2, 3);
}

终端敲入rvgcc main.c -g-g是为了反汇编时将C和汇编交差显示,方便阅读
rvobjdum -dS a.out > main.S 反汇编看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
000000000001019c <add>:
int add(int a, int b){
1019c: 1101 addi sp,sp,-32 //从原来的栈顶再向下再开辟32Byte栈空间
1019e: ec22 sd s0,24(sp) //保存 保存寄存器,
101a0: 1000 addi s0,sp,32 //栈底保存到fp/s0
101a2: 87aa mv a5,a0 //a5=a0=2 ------
101a4: 872e mv a4,a1 //a4=a1=3 |
101a6: fef42623 sw a5,-20(s0) //a5=a0=2 |
101aa: 87ba mv a5,a4 |
101ac: fef42423 sw a5,-24(s0) //a5=a4=a1=3 |-- 可以优化掉
return a + b ; |
101b0: fec42703 lw a4,-20(s0) //a4=2 |
101b4: fe842783 lw a5,-24(s0) //a5=3 -------
101b8: 9fb9 addw a5,a5,a4 //相加赋值给a5
101ba: 2781 sext.w a5,a5 //有符号扩展
}
101bc: 853e mv a0,a5 //a5赋给返回寄存器a0
101be: 6462 ld s0,24(sp)
101c0: 6105 addi sp,sp,32 //释放子函数开辟的栈空间
101c2: 8082 ret //返回到ra内容地址

00000000000101c4 <main>:
int main(void){
101c4: 1141 addi sp,sp,-16 //向下开辟16Byte栈空间
101c6: e406 sd ra,8(sp) //调用子函数前需要将ra压栈,否则子函数会覆盖ra
101c8: e022 sd s0,0(sp)
101ca: 0800 addi s0,sp,16 //栈底保存到fp/s0
return add(2, 3);
101cc: 458d li a1,3 //参数寄存器赋值
101ce: 4509 li a0,2 //参数寄存器赋值
101d0: fcdff0ef jal ra,1019c <add> //跳进函数add的地址1019c,并将返回地址(pc+4)存入ra
101d4: 87aa mv a5,a0 //计算结果a0 赋给a5
}
101d6: 853e mv a0,a5 //赋给a0,作为main函数的返回值
101d8: 60a2 ld ra,8(sp) //main函数的ra出栈
101da: 6402 ld s0,0(sp) //main函数的sp出栈
101dc: 0141 addi sp,sp,16 //栈指针指向栈底,释放栈
101de: 8082 ret //从main函数的ra返回

函数调用

将上面的代码用伪代码概况精简如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
子函数:
malloc stack //申请栈空间
push ra //如果内部还有子函数,ra入栈,否则省略该步骤
push s0/s... //保存子函数用到的保存寄存器
ld sp s0 //若是栈传参,将参数堆栈地址赋给sp,将参数出栈 ,否则省略该步骤
pop a1
pop a2
ld sp crt //将当前子函数的栈指针恢复
process //执行子函数运算
ld a0 res //讲计算结果保存到返回寄存器ra
pop s0/s... //恢复保存寄存器
free stack //释放栈
ret //从ra地址返回

母函数:
malloc stack //申请栈空间
push ra //母函数ra入栈,否则会被子程序覆盖
push s0/s... //保存子函数用到的保存寄存器
passParameter //3种传参方法
call subFunc //pc跳转到子函数地址
ld a5 a0 //获取返回值
pop s0/s... //恢复保存寄存器
pop ra //恢复ra
free stack //释放栈空间


栈底在最上面,示意如下

riscv存储空间分配

汇编语言参数传递的3种方法

  • 寄存器传参, 效率高,适合少量参数
  • 地址传参, 参数打包到一个结构体,讲结构体的地址传入,然后解包
  • 堆栈传参,调用前先压栈,再调用,返回从堆栈返回,也可以从寄存器返回

三种传参方法可以单独使用,也可以联合使用

不同的编译器处理方式也是不一样,堆栈传参最为常见,有些编译器也会支持fast call
会将其编译成寄存器传参(条件是参数要小于可供传参的寄存器个数,一般6个左右)
C语言返回一般都是通过返回寄存器返回,如果一下返回多个参数,那么一般将多个参数放到一个结构体
将结构体的地址返回。

一些高级语言返回一次可以返回多个参数,实际上只不过编译器帮你做了包装的事情,计算机本身只能返回一个寄存器

评论