前言
本文通过一个代码示例,介绍函数调用过程在汇编层面是如何实现的。
正文
示例代码如下:
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、d
4个变量的数值,所以在调用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 |
第三段 | 函数中定义的变量 |