1. 变量是放在寄存器里还是堆栈里?

堆栈对于处理器来说就是一块内存区域,而寄存器是处理器触手可及的存储,对于RISC 处理器而言,堆栈中的数据CPU并不能直接进行运算,还是要先加载到寄存器中才行。对于编译器而言,我猜测还是优先会选择将变量用寄存器保存。那什么时候需要用到堆栈呢?什么东西需要保存到堆栈呢?一种是需要切换上下文的地方,另一种是需要传参的地方。函数调用就是一种典型应用。

2. 函数调用时的栈与寄存器


一个典型的函数调用流程如上图所示,关键的涉及栈和寄存器的步骤如下:

  1. 首先,在调用其他函数前,Caller需要保存自身的上下文,比如某个变量的值存在寄存器x1,该寄存器有可能在被调函数Callee中用到,那就需要存到堆栈中;
  2. 调用函数时的传参,参数的传递有两个选择,一个是将参数放到寄存器,另一个则是将参数放到堆栈中。
  3. Caller调用函数的时候需要将PC+4,即函数的返回地址存到寄存器或堆栈,被调函数执行完调到该返回地址执行;
  4. 对于Callee而言,首先要保存上文中的某些寄存器,有的同志可能会问,步骤1中Caller不是已经保存了一波寄存器了,为什么Callee中又来一波?以RISC-V为例,约定了两类寄存器——临时寄存器和保存寄存器,其中临时寄存器可以由Callee随意使用,所以就需要Caller来保存;而保存寄存器需要Callee来保证其值在调用前后不能改变,所以需要Callee存储。
  5. Callee在执行完之后,自然需要将“保存寄存器”的值恢复。此外,返回参数也需要存放至寄存器或堆栈中。

从上面的描述中可以看到,保存寄存器只能存到堆栈中,而保存参数和函数返回地址则既可以放在寄存器,也可以放在堆栈。RISC-V对此进行了如下约定:

寄存器名 ABI名(编程用名) 用途约定 谁负责在函数调用过程中维护这些寄存器
x0 zero 读取时总为 0, 写入时不起任何效果 N/A
x1 ra 存放函数返回值(return address) Caller
x2 sp 存放栈指针(stack pointer) Callee
x5~x7, x28~x31 t0~t2, t3~t6 临时(temporaries)寄存器,Callee 可能会使用这些寄存器,所以Callee 不保证这些寄存器中的值在函数调用过程中保持不变,这意味着对于 Caller 来说,如果需要的话,Caller 需要自己在调用 Callee 之前保存临时寄存器中的值。 Caller
x8, x9, x18~x27 s0, s1, s2~s11 保存(saved)寄存器,Callee 需要保证这些寄存器的值在函数返回后仍然维持函数调用之前的原值,所以一旦 Callee 在自己的函数中会用到这些寄存器则需要在栈中备份并在退出函数时进行恢复。 Callee
x10 , x11 a0 , a1 参数(argument)寄存器,用于在函数调用过程中保存第一个和第二个参数,以及在函数返回时传递返回值。 Caller
x12 ~ x17 a2 ~ 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 routineaa_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         # 第一个参数放入寄存器a0li a0, 4          # 第二个参数放入寄存器a1call aa_bb            # Caller中没用到什么临时寄存器,所以不需要保存,直接跳转; call伪指令会将跳转地址放到寄存器ra中
stop:j stop             # 死循环结束代码   aa_bb:addi sp, sp, -16  # 栈指针初始化时指在栈去末尾,要存放4个word的数据,所以减16sw 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寄存器,覆盖了原来的raadd s2, s2, a0      # 计算的返回参数在a0中,加到s2上mv a1, s1         # 与上面计算同理jal squareadd s2, s2, a0mv a0, s2          # 最终的返回参数防止到a0寄存器lw s0, 0(sp)       # 从栈区中恢复各个保存寄存器的值lw s1, 4(sp)lw s2, 8(sp)lw ra, 12(sp)      # ra寄存器的值恢复回来addi sp, sp, 16        # 释放栈空间,栈指针依旧指向进入函数前的位置  retsquare:addi sp, sp, -8sw s0, 0(sp)sw s1, 4(sp)mv s0, a0mul s1, s0, s0mv a0, s1lw s0, 0(sp)lw s1, 4(sp)addi sp, sp, 8retstack_start:.rept 10          # 定义10个word大小的栈空间.word 0.endr
stack_end:

如果有人读了上面的代码,可能会发出一个疑问——为什么运算的时候非得用s0~s2这种保存寄存器,这不是脱裤子放屁呢吗?因为这个是教学演示代码,为了说明小节2中的函数调用流程。

4. volatile关键字的原理

volatile关键字存在的根本原因正是变量会存在寄存器还是内存中。假如一个变量会在多线程中用到,该变量如果在一个线程运行中被加载到了寄存器中,则显然后续的代码也会继续使用这个寄存器中的变量值,而不会使用内存中的该值。如果别的线程中的代码修改了内存中该变量的值,而本线性依旧使用寄存器中的值就会出错。
volatile就是告诉编译器,每次用到这个变量的时,都去内存中把这个值重新加载一次,而不是沿用之前的寄存器中的值。因为这个值可能被中途改变了。

参考

再次感谢并十分推荐来自PLCT实验室汪辰老师的课程《循序渐进、学习开发一个RISC-V上的操作系统》:https://www.bilibili.com/video/BV1Q5411w7z5?p=12&spm_id_from=pageDriver

变量究竟是存在寄存器还是堆栈?相关推荐

  1. java 常量存储_JAVA 存储空间 寄存器 堆栈 堆 常量存储 非RAM存储

    1.寄存器 这是最快的存储区,因为它位于处理器内部,数量极其有限,所以寄存器根据需求进行分配,你不能直接控制,也不能在程序中感 觉到寄存器存在的任何迹象. 2.堆栈 位于通用RAM(随机访问存储器)中 ...

  2. c语言静态变量存在堆还是栈,c 类 static 函数 什么样是静态变量?嵌入式C语言的堆栈管理如何实现...

    C语言中静态变量是什么意思,有什么作用,static在数据类型前面表示什么 最近刚看了C存储类的章节.所以来说说. C语言为变量提供了⑤种不同的存储模型,或者说是存储类. ①个变量可以用存储时期描述, ...

  3. MCS-51单片机存储器结构-特殊功能寄存器 :堆栈指针SP(Stack Pointer)

    堆栈指针SP(Stack Pointer) 堆栈是一种数据结构,它是一个8位寄存器,它指示堆栈顶部在内部RAM中的位置.系统复位后,SP的初始值为07H,使得堆栈实际上是从08H开始的.但我们从RAM ...

  4. 【C语言必经之路——第1节】自动变量(auto)外部变量(extern)静态变量(static)寄存器变量(register)

    目录 一.auto变量 二.extern变量 三.static变量 static的作用为: 1.修饰全局变量 2.修饰局部变量 3.修饰函数 四.register变量 一.auto变量 若定义一个局部 ...

  5. oracle绑定变量过多,oracle - 在SQL Plus中使用绑定变量并返回多行? - 堆栈内存溢出...

    这是一个愚蠢的问题,但我似乎无法解决. 我有一个查询在OCI程序中引起麻烦,因此我想在SQL * Plus中手动运行它以检查是否有任何区别. 这是查询: select e.label as doc_n ...

  6. c语言用中括号括起来的变量,用大括号将寄存器名括起来是什么意思?

    满意答案 基渣碰址变址寻址 就是到BP中找5261到段内偏拦正移量 然后到SI中找到段基址4102 然后用段基址*10H+段内偏移量找到最终的1653内存单元~ 用中括号括起一个常量 是直接寻址方式: ...

  7. 你的变量究竟存储在什么地方 全局内存

    我相信大家都有过这样的经历,在面试过程中,考官通常会给你一道题目,然后问你某个变量存储在什么地方,在内存中是如何存储的等等一系列问题.不仅仅是在面试中,学校里面的考试也会碰到同样的问题.  如果你还不 ...

  8. ARM微控制器-MCU基础及CPU运行过程(堆栈/中断/寄存器操作)

    目录 为什么计算机能读懂1和0? 一. CPU的基本结构和运行机制 1. 一个基本的MCU内部结构 2. MCU Structure 3. 分析其中的CPU: 一个完整的CPU: 4. 堆栈 5. 堆 ...

  9. stm32h7内存分配_【STM32H7教程】第9章 STM32H7重要知识点数据类型,变量和堆栈...

    第9章   STM32H7重要知识点数据类型,变量和堆栈 本章教程为大家介绍数据类型,变量和堆栈的相关知识. 9.1 初学者重要提示 9.2 数据类型 9.3 局部变量和全局变量 9.4 堆栈 9.5 ...

最新文章

  1. 怎样查看rpm安装包的安装路径
  2. Go 语言中的 new() 和 make()的区别
  3. 不断学习UI框架的写法
  4. 从CTF比赛真题中学习压缩包伪加密与图片隐写术
  5. 原生vue.js实现待办事项清单,支持增删改查
  6. 交换机的基本配置实验报告_交换机入门配置,最基本的IP及登录方式配置,一分钟了解下...
  7. TensorFlow2学习笔记:3、鸢尾花数据集载入
  8. dnf服务器字幕乱码win10系统,Windows10下输入法设置 教你避免DNF卡顿
  9. linux ozip转zip,linux 怎么把rar转换成zip 或者 tar
  10. Guns二次开发(十四):集成 ueditor 富文本编辑器
  11. 学籍管理系统c语言项目作业,C语言实现学生学籍管理系统
  12. 1's Complement和2's Complement的区别
  13. 【渝粤题库】陕西师范大学204001英语写作 作业(高起本、专升本)
  14. 对东方财经个股资金流的爬取分析
  15. 人生没有白读的书,每一本都算数~
  16. 伏神月破、伏神跟飞神、动爻、日月关系的思考
  17. 渗透常用SQL注入语句大全
  18. kubelet参数解释about kubelet gc image and evict pod.
  19. Revit MEP 平面视图中(立管)怎么设置二维表达?
  20. k8s-卸载K8S集群

热门文章

  1. tusimple数据集转换流程
  2. 支付通道对接常见的问题有哪些
  3. js html路径乱码,如何把js获取url中文乱码转码
  4. Web开发问题:IE浏览器中url中文乱码问题
  5. 详解OpneCV的按键值获取函数waitKey()及使用中需要注意的地方
  6. 公牛集团年营收123亿:阮立平兄弟获12亿分红 高瓴大幅减持
  7. 亿级流量架构|day04-PowerDesigner和通用Mapper
  8. java golang gc_Golang GC 垃圾回收机制详解
  9. 密码学上的commitment
  10. el表达式的作用、JSTL的概念和作用