【Cortex-M3】C语言函数调用过程汇编层面分析

前言

本文通过一个代码示例,介绍函数调用过程在汇编层面是如何实现的。

正文

示例代码如下:

int add_val(int pa, int pb, int pc, int pd)
{
    volatile int tmp;
	
	tmp = pa + pb + pc + pd;

    return tmp;
}

int mymain()
{
	volatile int a = 1;
	volatile int b = 2;
	volatile int c = 3;
	volatile int d = 4;
	volatile int e;
	
	e = add_val(a, b, c, d);
	
	return e;
}

对应的汇编代码如下:
mymain函数
在这里插入图片描述

add_val函数
在这里插入图片描述
C语言代码用Keil5编写,对应的反汇编代码在编译项目时生成的.dis文件中,如何生成.dis可以百度查看。

mymain()函数首先声明了5个4字节的变量:

volatile int a = 1;
volatile int b = 2;
volatile int c = 3;
volatile int d = 4;
volatile int e;

对应的汇编代码中将SP栈指针减20,空出20个字节来存储a、b、c、d、e这5个变量。

SUB      sp,sp,#0x14

图像化如下:
请添加图片描述
从此之后,在汇编代码里,

SP+0x00表示变量e的地址
SP+0x04表示变量d的地址
SP+0x08表示变量c的地址
SP+0x0C表示变量b的地址
SP+0x10表示变量a的地址;

给变量a初始化汇编代码如下:

MOVS     r0,#1
STR      r0,[sp,#0x10]

首先给通用寄存器R0赋值为1,然后将R0的写入到SP+0x10这个地址处,也就是将变量a赋值为1。
通用寄存器R0-R12都是32位的。
其他变量初始化过程类似。

函数调用过程中传递参数是通过寄存器R0-R3传递的,mymain函数调用add_val函数传递了a、b、c、d4个变量的数值,所以在调用add_val函数之前需要将a、b、c、d赋值给R0、R1、R2、R3
对应汇编代码如下:

LDRD     r3,r2,[sp,#4]

SP+0x04开始4个字节赋值给R3,然后再取4个字节赋值给R2

LDRD     r1,r0,[sp,#0xc]

SP+0x0C开始4个字节赋值给R1,然后再取4个字节赋值给R0
这个过程就将a、b、c、d赋值给了R0、R1、R2、R3
然后通过

BL       add_val ; 0x8000a5c

跳转到add_val函数开始指令,也就是将PC指针赋值为0x8000a5c。
add_val对应汇编代码如下:

PUSH     {r3,r4,lr}
MOV      r4,r0
ADDS     r0,r4,r1
ADD      r0,r0,r2
ADD      r0,r0,r3
STR      r0,[sp,#0]
LDR      r0,[sp,#0]
POP      {r3,r4,pc}

这段汇编是经过了优化,不优化代码如下:

PUSH     {r4,lr}
SUB      sp,sp,#4
MOV      r4,r0
ADDS     r0,r4,r1
ADD      r0,r0,r2
ADD      r0,r0,r3
STR      r0,[sp,#0]
LDR      r0,[sp,#0]
ADD      sp,sp,#4
POP      {r4,pc}

开始进入函数的PUSH {r4,lr} 与 退出函数的 POP {r4,pc} 相对应,LR是链接寄存器,用于保存函数调用时的返回地址,开始将LR压栈,最后将LR出栈并赋值给PC,也就完成了函数返回。
R4压栈是因为在函数调用过程中有一个协议:

R0-R3、R12、R14(LR) 寄存器,子函数是可以随便使用的,主函数在调用子函数时要有心理准备这些寄存器的内容是可能被子函数修改的,所以如果有需要,这些寄存器的内容主函数在调用子函数之前要保护起来,所以这些寄存器被称为“调用者保护寄存器”

R4-R11寄存器,子函数要保证在进入子函数前和退出子函数后,这些寄存器的内容是不变的,所以这些寄存器被称为“被调用者保护寄存器”

因为add_val用到了R4,所以在执行开始就将R4压栈,在退出函数时对R4进行了出栈。以此来保证R4在子函数执行前后内容不变。

而优化代码将:

PUSH     {r4,lr}
SUB      sp,sp,#4

变成

PUSH     {r3,r4,lr}

是因为将寄存器R3压栈和SP-4(将SP栈指针数值减4)效果是一样的,但是压栈动作的执行效率比减法运算执行效率高。
最后在add_val函数中,将返回数值赋值给了R0,然后通过 POP {pc},将LR赋值给PC完成函数返回。
回到主函数后,主函数读取R0的内容获得子函数的返回参数,并做进一步处理。

总结

1、主函数调用子函数,传递参数是通过R0、R1、R2、R3寄存器传递的。
2、子函数通过return返回给主函数的参数,是通过R0寄存器返回的。
3、子函数被调用后,首先要执行PUSH {lr},将LR寄存器的数据保存,最后通过POP {pc}的方式取出返回地址并返回。
4、子函数中使用到“被调用者保护寄存器”R4-R11时,需要在开始执行时把使用的寄存器压栈,退出函数时再将这些寄存器出栈。
5、函数中定义的变量,汇编代码会通过移动SP的方式将存放这些变量的空间空出来,之后访问这些变量也是以SP为基准来表示他们的地址值。
6、一个函数开始运行后,新的SP与旧的SP之间的内容如下:

函数运行空间内容
第一段链接寄存器LR
第二段本函数中用到的“被调用者保护寄存器”R4-R11
第三段函数中定义的变量
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值