打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
变量究竟是存在寄存器还是堆栈?
1. 变量是放在寄存器里还是堆栈里?
堆栈对于处理器来说就是一块内存区域,而寄存器是处理器触手可及的存储,对于RISC 处理器而言,堆栈中的数据CPU并不能直接进行运算,还是要先加载到寄存器中才行。对于编译器而言,我猜测还是优先会选择将变量用寄存器保存。那什么时候需要用到堆栈呢?什么东西需要保存到堆栈呢?一种是需要切换上下文的地方,另一种是需要传参的地方。函数调用就是一种典型应用。
2. 函数调用时的栈与寄存器
一个典型的函数调用流程如上图所示,关键的涉及栈和寄存器的步骤如下:
首先,在调用其他函数前,Caller需要保存自身的上下文,比如某个变量的值存在寄存器x1,该寄存器有可能在被调函数Callee中用到,那就需要存到堆栈中;
调用函数时的传参,参数的传递有两个选择,一个是将参数放到寄存器,另一个则是将参数放到堆栈中。
Caller调用函数的时候需要将PC+4,即函数的返回地址存到寄存器或堆栈,被调函数执行完调到该返回地址执行;
对于Callee而言,首先要保存上文中的某些寄存器,有的同志可能会问,步骤1中Caller不是已经保存了一波寄存器了,为什么Callee中又来一波?以RISC-V为例,约定了两类寄存器——临时寄存器和保存寄存器,其中临时寄存器可以由Callee随意使用,所以就需要Caller来保存;而保存寄存器需要Callee来保证其值在调用前后不能改变,所以需要Callee存储。
Callee在执行完之后,自然需要将“保存寄存器”的值恢复。此外,返回参数也需要存放至寄存器或堆栈中。
从上面的描述中可以看到,保存寄存器只能存到堆栈中,而保存参数和函数返回地址则既可以放在寄存器,也可以放在堆栈。RISC-V对此进行了如下约定:
寄存器名ABI名(编程用名)用途约定谁负责在函数调用过程中维护这些寄存器
x0zero读取时总为 0, 写入时不起任何效果N/A
x1ra存放函数返回值(return address)Caller
x2sp存放栈指针(stack pointer)Callee
x5~x7, x28~x31t0~t2, t3~t6临时(temporaries)寄存器,Callee 可能会使用这些寄存器,所以Callee 不保证这些寄存器中的值在函数调用过程中保持不变,这意味着对于 Caller 来说,如果需要的话,Caller 需要自己在调用 Callee 之前保存临时寄存器中的值。Caller
x8, x9, x18~x27s0, s1, s2~s11保存(saved)寄存器,Callee 需要保证这些寄存器的值在函数返回后仍然维持函数调用之前的原值,所以一旦 Callee 在自己的函数中会用到这些寄存器则需要在栈中备份并在退出函数时进行恢复。Callee
x10 , x11a0 , a1参数(argument)寄存器,用于在函数调用过程中保存第一个和第二个参数,以及在函数返回时传递返回值。Caller
x12 ~ x17a2 ~ a7参数(argument)寄存器,如果函数调用时需要传递更多的参数,则可以用这些寄存器,但注意用于传递参数的寄存器最多只有 8 个(a0 ~ a7),如果还有更多的参数则要利用栈。Caller
由于编译器做出了以上函数调用约定,这就意味着汇编指令中本来要带的参数不需要了,比如原来跳转并链接到某个label的指令是jal x1, offset,意思是跳转到 offset 制定位置,返回地址保存在 x1 (ra)中,现在由于做好了约定,可以直接使用伪指令jal offset来替代。
3. 函数调用实例
下面举个例子来说明一下小节2中的过程。
首先给出C语言代码如下,代码入口是_start函数,其中调用了aa_bb函数,aa_bb又调用了square函数。
void _start()
{
// calling nested routine
aa_bb(3, 4);
}
int aa_bb(int a, int b)
{
return square(a) + square(b);
}
int square(int num)
{
return num * num;
}
让我们根据2中的一些约定来写RISC-V汇编指令:
_start:
la sp, stack_end # 首先初始化堆栈指针
li a0, 3 # 第一个参数放入寄存器a0
li a0, 4 # 第二个参数放入寄存器a1
call aa_bb # Caller中没用到什么临时寄存器,所以不需要保存,直接跳转; call伪指令会将跳转地址放到寄存器ra中
stop:
j stop # 死循环结束代码
aa_bb:
addi sp, sp, -16 # 栈指针初始化时指在栈去末尾,要存放4个word的数据,所以减16
sw s0, 0(sp) # 考虑到后面会用到保存寄存器,s0~s2,需要Callee进行保存
sw s1, 4(sp)
sw s2, 8(sp)
sw ra, 12(sp) # ra中目前存放的是_start函数下一条指令的地址,由于在aa_bb函数中还要调用别的函数,会覆盖掉ra寄存器的值,因此也要存下来
mv s0, a0 # a0寄存器的值挪到s0中
mv s1, a1 # a1寄存器的值挪到s1中
li s2, 0 # 将最终结果放在s2中,因此先将s2清零
mv a0, s0 # 准备调用square函数啦,传参放入寄存器a0; 作为一个Caller没有需要保存的临时寄存器
jal square # 跳转到square函数中执行,返回地址会放在ra寄存器,覆盖了原来的ra
add s2, s2, a0 # 计算的返回参数在a0中,加到s2上
mv a1, s1 # 与上面计算同理
jal square
add s2, s2, a0
mv a0, s2 # 最终的返回参数防止到a0寄存器
lw s0, 0(sp) # 从栈区中恢复各个保存寄存器的值
lw s1, 4(sp)
lw s2, 8(sp)
lw ra, 12(sp) # ra寄存器的值恢复回来
addi sp, sp, 16 # 释放栈空间,栈指针依旧指向进入函数前的位置
ret
square:
addi sp, sp, -8
sw s0, 0(sp)
sw s1, 4(sp)
mv s0, a0
mul s1, s0, s0
mv a0, s1
lw s0, 0(sp)
lw s1, 4(sp)
addi sp, sp, 8
ret
stack_start:
.rept 10 # 定义10个word大小的栈空间
.word 0
.endr
stack_end:
如果有人读了上面的代码,可能会发出一个疑问——为什么运算的时候非得用s0~s2这种保存寄存器,这不是脱裤子放屁呢吗?因为这个是教学演示代码,为了说明小节2中的函数调用流程。
4. volatile关键字的原理
volatile关键字存在的根本原因正是变量会存在寄存器还是内存中。假如一个变量会在多线程中用到,该变量如果在一个线程运行中被加载到了寄存器中,则显然后续的代码也会继续使用这个寄存器中的变量值,而不会使用内存中的该值。如果别的线程中的代码修改了内存中该变量的值,而本线性依旧使用寄存器中的值就会出错。
volatile就是告诉编译器,每次用到这个变量的时,都去内存中把这个值重新加载一次,而不是沿用之前的寄存器中的值。因为这个值可能被中途改变了。
定期以通俗易懂的方式分享嵌入式知识,关注公众号,加星标,每天进步一点点。
声明:
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
【嵌入式学习】Arm-elf-gcc编译器函数调用规则 - zhangjun2915的日志...
Coroutine in Depth
C/C++ 函数参数和返回值传递机制
vc知识库文章 - 声明函数指针并实现回调
JavaScript 函数
c语言和堆栈
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服