前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >RISC-V 函数调用约定和Stack使用

RISC-V 函数调用约定和Stack使用

作者头像
大忽悠爱学习
发布于 2023-10-11 00:44:31
发布于 2023-10-11 00:44:31
1.1K0
举报
文章被收录于专栏:c++与qt学习c++与qt学习
RISC-V 函数调用约定和Stack使用

引言

MIT 6.S081 2020 操作系统

本文为MIT 6.S081课程第五节重点笔记整理。


RISC-V vs x86

不同的处理器指令集不一样,而汇编语言中都是一条条指令,所以不同处理器对应的汇编语言必然不一样。

如果你使用RISC-V,你不太能将Linux运行在上面。相应的,大多数现代计算机都运行在x86和x86-64处理器上。x86拥有一套不同的指令集,看起来与RISC-V非常相似。通常你们的个人电脑上运行的处理器是x86,Intel和AMD的CPU都实现了x86。

RISC-V和x86并没有它们第一眼看起来那么相似。RISC-V中的RISC是精简指令集(Reduced Instruction Set Computer)的意思,而x86通常被称为CISC,复杂指令集(Complex Instruction Set Computer)。这两者之间有一些关键的区别:

  • 首先是指令的数量。
    • 实际上,创造RISC-V的一个非常大的初衷就是因为Intel手册中指令数量太多了。
    • x86-64指令介绍由3个文档组成,并且新的指令以每个月3条的速度在增加。
    • 因为x86-64是在1970年代发布的,所以我认为现在有多于15000条指令。
    • RISC-V指令介绍由两个文档组成。
  • 除此之外,RISC-V指令也更加简单。在x86-64中,很多指令都做了不止一件事情。这些指令中的每一条都执行了一系列复杂的操作并返回结果。但是RISC-V不会这样做,RISC-V的指令趋向于完成更简单的工作,相应的也消耗更少的CPU执行时间。这其实是设计人员的在底层设计时的取舍。并没有一些非常确定的原因说RISC比CISC更好。它们各自有各自的使用场景。
  • 相比x86来说,RISC另一件有意思的事情是它是开源的。这是市场上唯一的一款开源指令集,这意味着任何人都可以为RISC-V开发主板。RISC-V是来自于UC-Berkly的一个研究项目,之后被大量的公司选中并做了支持,网上有这些公司的名单,许多大公司对于支持一个开源指令集都感兴趣。

在日常生活中,可能已经在完全不知情的情况下使用了精简指令集。比如说ARM也是一个精简指令集,高通的Snapdragon处理器就是基于ARM。如果你使用一个Android手机,那么大概率你的手机运行在精简指令集上。如果你使用IOS,苹果公司也实现某种版本的ARM处理器,这些处理器运行在iPad,iPhone和大多数苹果移动设备上,甚至对于Mac,苹果公司也在尝试向ARM做迁移(注,刚刚发布的Macbook)。所以精简指令集出现在各种各样的地方。如果你想在现实世界中找到RISC-V处理器,你可以在一些嵌入式设备中找到。所以RISC-V也是有应用的,当然它可能没有x86那么流行。

在最近几年,由于Intel的指令集是在是太大了,精简指令集的使用越来越多。Intel的指令集之所以这么大,是因为Intel对于向后兼容非常看重。所以一个现代的Intel处理器还可以运行30/40年前的指令。Intel并没有下线任何指令。而RISC-V提出的更晚,所以不存在历史包袱的问题。

如果查看RISC-V的文档,可以发现RISC-V的特殊之处在于:它区分了Base Integer Instruction Set和Standard Extension Instruction Set。Base Integer Instruction Set包含了所有的常用指令,比如add,mult。除此之外,处理器还可以选择性的支持Standard Extension Instruction Set。例如,一个处理器可以选择支持Standard Extension for Single-Precision Float-Point。这种模式使得RISC-V更容易支持向后兼容。 每一个RISC-V处理器可以声明支持了哪些扩展指令集,然后编译器可以根据支持的指令集来编译代码。

看起来使用x86而不是RISC-V的唯一优势就是能得到性能的提升,但是这里的性能是以复杂度和潜在的安全为代价的,我的问题是为什么我们还在使用x86,而不是使用RISC-V处理器?

  • 现在整个世界都运行在x86上,如果你突然将处理器转变成RISC-V,那么你就会失去很多重要的软件支持。
  • 同时,Intel在它的处理器里面做了一些有意思的事情,例如安全相关的enclave,这是Intel最近加到处理器中来提升安全性的功能。
  • 此外,Intel还实现了一些非常具体的指令,这些指令可以非常高效的进行一些特定的运算。所以Intel有非常多的指令,通常来说对于一个场景都会有一个完美的指令,它的执行效率要高于RISC-V中的同等指令。
  • 但是这个问题更实际的答案是,RISC-V相对来说更新一些,目前还没有人基于RISC-V来制造个人计算机,SiFive也就是最近才成为第一批将RISC-V应用到个人计算机的公司。所以,从实际的角度来说,因为不能在RISC-V上运行所有为Intel设计的软件,是我对这个问题的最好的答案。

RISC-V寄存器

这个表里面是RISC-V寄存器。

  • 寄存器是CPU或者处理器上,预先定义的可以用来存储数据的位置。
  • 寄存器之所以重要是因为汇编代码并不是在内存上执行,而是在寄存器上执行,也就是说,当我们在做add,sub时,我们是对寄存器进行操作。
  • 所以你们通常看到的汇编代码中的模式是,我们通过load将数据存放在寄存器中,这里的数据源可以是来自内存,也可以来自另一个寄存器。
  • 之后我们在寄存器上执行一些操作。如果我们对操作的结果关心的话,我们会将操作的结果store在某个地方。这里的目的地可能是内存中的某个地址,也可能是另一个寄存器。这就是通常使用寄存器的方法。

寄存器是用来进行任何运算和数据读取的最快的方式,这就是为什么使用它们很重要,也是为什么我们更喜欢使用寄存器而不是内存。

当我们调用函数时,你可以看到这里有a0 - a7寄存器。通常我们在谈到寄存器的时候,我们会用它们的ABI名字。不仅是因为这样描述更清晰和标准,同时也因为在写汇编代码的时候使用的也是ABI名字。

  • 第一列中的寄存器名字并不是超级重要,它唯一重要的场景是在RISC-V的Compressed Instruction中。
  • 基本上来说,RISC-V中通常的指令是64bit,但是在Compressed Instruction中指令是16bit。
  • 在Compressed Instruction中我们使用更少的寄存器,也就是x8 - x15寄存器。
  • 我猜你们可能会有疑问,为什么s1寄存器和其他的s寄存器是分开的?
  • 因为s1在Compressed Instruction是有效的,而s2-11却不是。除了Compressed Instruction,寄存器都是通过它们的ABI名字来引用。

a0到a7寄存器是用来作为函数的参数。如果一个函数有超过8个参数,我们就需要用内存了。从这里也可以看出,当可以使用寄存器的时候,我们不会使用内存,我们只在不得不使用内存的场景才使用它。

表单中的第4列,Saver列,当我们在讨论寄存器的时候也非常重要。它有两个可能的值Caller,Callee。我经常混淆这两个值,因为它们只差一个字母。我发现最简单的记住它们的方法是:

  • Caller Saved寄存器在函数调用的时候不会保存
  • Callee Saved寄存器在函数调用的时候会保存

这里的意思是,一个Caller Saved寄存器可能被其他函数重写。

  • 假设我们在函数a中调用函数b,任何被函数a使用的并且是Caller Saved寄存器,调用函数b可能重写这些寄存器。
  • 我认为一个比较好的例子就是Return address寄存器(注,保存的是函数返回的地址),你可以看到ra寄存器是Caller Saved,这一点很重要,它导致了当函数a调用函数b的时侯,b会重写Return address。
  • 所以基本上来说,任何一个Caller Saved寄存器,作为调用方的函数要小心可能的数据可能的变化;
  • 任何一个Callee Saved寄存器,作为被调用方的函数要小心寄存器的值不会相应的变化。

如果你们还记得的话,所有的寄存器都是64bit,各种各样的数据类型都会被改造的可以放进这64bit中。比如说我们有一个32bit的整数,取决于整数是不是有符号的,会通过在前面补32个0或者1来使得这个整数变成64bit并存在这些寄存器中。


Stack

下面是一个非常简单的栈的结构图,其中每一个区域都是一个Stack Frame,每执行一次函数调用就会产生一个Stack Frame。

每一次我们调用一个函数,函数都会为自己创建一个Stack Frame,并且只给自己用。函数通过移动Stack Pointer来完成Stack Frame的空间分配。

对于Stack来说,是从高地址开始向低地址使用。所以栈总是向下增长。当我们想要创建一个新的Stack Frame的时候,总是对当前的Stack Pointer做减法。一个函数的Stack Frame包含了保存的寄存器,本地变量,并且,如果函数的参数多于8个,额外的参数会出现在Stack中。所以Stack Frame大小并不总是一样,即使在这个图里面看起来是一样大的。不同的函数有不同数量的本地变量,不同的寄存器,所以Stack Frame的大小是不一样的。但是有关Stack Frame有两件事情是确定的:

  • Return address总是会出现在Stack Frame的第一位
  • 指向前一个Stack Frame的指针也会出现在栈中的固定位置

有关Stack Frame中有两个重要的寄存器,第一个是SP(Stack Pointer),它指向Stack的底部并代表了当前Stack Frame的位置。第二个是FP(Frame Pointer),它指向当前Stack Frame的顶部。因为Return address和指向前一个Stack Frame的的指针都在当前Stack Frame的固定位置,所以可以通过当前的FP寄存器寻址到这两个数据。

我们保存前一个Stack Frame的指针的原因是为了让我们能跳转回去。所以当前函数返回时,我们可以将前一个Frame Pointer存储到FP寄存器中。所以我们使用Frame Pointer来操纵我们的Stack Frames,并确保我们总是指向正确的函数

Stack Frame必须要被汇编代码创建,所以是编译器生成了汇编代码,进而创建了Stack Frame。所以通常,在汇编代码中,函数的最开始你们可以看到Function prologue,之后是函数的本体,最后是Epollgue。这就是一个汇编函数通常的样子。

我们从汇编代码中来看一下这里的操作:

在我们上面的sum_to函数中,只有函数主体,并没有Stack Frame的内容。它这里能正常工作的原因是它足够简单,并且它是一个leaf函数。leaf函数是指不调用别的函数的函数,它的特别之处在于它不用担心保存自己的Return address或者任何其他的Caller Saved寄存器,因为它不会调用别的函数。

而另一个函数sum_then_double就不是一个leaf函数了,这里你可以看到它调用了sum_to。

所以在这个函数中,需要包含prologue。

这里我们对Stack Pointer减16,这样我们为新的Stack Frame创建了16字节的空间。之后我们将Return address保存在Stack Pointer位置。

之后就是调用sum_to并对结果乘以2。最后是Epllogue,

这里首先将Return address加载回ra寄存器,通过对Stack Pointer加16来删除刚刚创建的Stack Frame,最后ret从函数中退出。

如果我们删除掉Prologue和Epllogue,然后只剩下函数主体会发生什么?

  • sum_then_double将不知道它应该返回的Return address。所以调用sum_to的时候,Return address被覆盖了,最终sum_to函数不能返回到它原本的调用位置。

我们可以看一下具体会发生什么。先在修改过的sum_then_double设置断点,然后执行sum_then_double:

我们可以看到现在的ra寄存器是0x80006392,它指向demo2函数,也就是sum_then_double的调用函数。之后我们执行代码,调用了sum_to:

我们可以看到ra寄存器的值被sum_to重写成了0x800065f4,指向sum_then_double,这也合理,符合我们的预期。我们在函数sum_then_double中调用了sum_to,那么sum_to就应该要返回到sum_then_double。

之后执行代码直到sum_then_double返回。因为没有恢复sum_then_double自己的Return address,现在的Return address仍然是sum_to对应的值,现在我们就会进入到一个无限循环中。


接下来我们来看一些C代码。

demo4函数里面调用了dummymain函数。我们在dummymain函数中设置一个断点,

现在我们在dummymain函数中。如果我们在gdb中输入info frame,可以看到有关当前Stack Frame许多有用的信息。

  • Stack level 0,表明这是调用栈的最底层
  • pc,当前的程序计数器
  • saved pc,demo4的位置,表明当前函数要返回的位置
  • source language c,表明这是C代码
  • Arglist at,表明参数的起始地址。当前的参数都在寄存器中,可以看到argc=3,argv是一个地址

如果输入backtrace(简写bt)可以看到从当前调用栈开始的所有Stack Frame

如果对某一个Stack Frame感兴趣,可以先定位到那个frame再输入info frame,Stack Frame中有更多的信息,有一堆的Saved Registers,有一些本地变量等等。这些信息对于调试代码来说超级重要。


Struct

struct在内存中的结构是怎样?

  • 基本上来说,struct在内存中是一段连续的地址,如果我们有一个struct,并且有f1,f2,f3三个字段。

当我们创建这样一个struct时,内存中相应的字段会彼此相邻。你可以认为struct像是一个数组,但是里面的不同字段的类型可以不一样。

我们可以将struct作为参数传递给函数。

这里有一个名字是Person的struct,它有两个字段。我将这个struct作为参数传递给printPerson并打印相关的信息。我们在printPerson中设置一个断点,当程序运行到函数内部时打印当前的Stack Frame。

我们可以看到当前函数有一个参数p。打印p可以看到这是struct Person的指针,打印p的反引用可以看到struct的具体内容。


补充

函数调用约定

寄存器约定

函数跳转和返回指令的编程约定

被调用函数的编程约定

RISC-V 汇编与 C 混合编程

RISC-V 汇编调用 C 函数
C 函数中嵌入 RISC-V 汇编
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
芯片架构 X86 、 ARM 、RISC-V、MIPS、POWERPC、SPARC 区别
芯片架构(或指令集架构,ISA)决定了处理器如何执行指令并与外部硬件进行交互。X86、ARM、RISC-V、MIPS、POWERPC 和 SPARC 都是流行的处理器架构,它们之间有许多区别,主要体现在指令集、设计哲学、性能、功耗、市场定位和应用场景等方面。
Linux运维技术之路
2025/02/18
2950
芯片架构 X86 、 ARM 、RISC-V、MIPS、POWERPC、SPARC 区别
悄悄停止仅上线半年的 RISC-V 项目,英特尔:没影响,只是小团队项目
近日,英特尔出人意料地结束了才推出半年的 Intel Pathfinder for RISC-V 项目,而且除了突然通知参与者该公司已停止该计划外,没有任何公开声明。
深度学习与Python
2023/03/01
3230
悄悄停止仅上线半年的 RISC-V 项目,英特尔:没影响,只是小团队项目
RISC-V 简介(1)RISC-V的由来
指令集架构(Instruction Set Architecture, ISA),是一种抽象模型,包括指令集,寄存器,内存处理,寻址模式,中断和异常处理,以及外部的I/0接口。指令集包括一系列的操作码(opcode),或机器码(machine code),以及特定处理器执行的基本命令。ISA以不依赖于其实现特性方式,指定了在其实现上运行机器码的行为。中央处理器(central processing unit, CPU)可以看作是ISA的实现。
IC知识库
2021/07/28
1.3K0
RISC-V 简介(1)RISC-V的由来
四大主流芯片架构(X86、ARM、RISC-V和MIPS)
X86是微处理器执行的计算机语言指令集,指一个Intel通用计算机系列的标准编号缩写,也标识一套通用的计算机指令集合。1978年6月8日,Intel 发布了新款16位微处理器 8086,也同时开创了一个新时代:X86架构诞生了。 X86指令集是美国Intel公司为其第一块16位CPU(i8086)专门开发的,美国IBM公司1981年推出的世界第一台PC机中的CPU–i8088(i8086简化版)使用的也是X86指令。
全栈程序员站长
2022/09/01
9.8K0
RISC-V | 简介
RISC表示精简指令集,英文全称是Ruduced Instruction Set ComputerV表示第五代。RISC-V指令集有这些优点:设计简洁、模块化、开源和有丰富的软件生态。
哆哆jarvis
2023/02/26
1.5K0
RISC-V | 简介
关于risc-v启动部分思考
risc-v的架构有着非常鲜明的特点,如果看过arm,aarch64,mips等架构的一些架构手册的基础知识,再看risc-v的芯片的架构设计,就会觉得非常有意思,可以找到一些影子,但是又比这些架构设计简洁的多。当我看完aarch64的芯片手册,再看risc-v的boot时,设计思想竟然可以做一些对比,同样去看risc-v和mips的寄存器,也可看到高度的一致性。对于x86的架构我未曾深入了解,但是在risc-v上应该也可以找到一些设计元素。总体说来,risc-v的架构设计集合了各种架构的设计的优点。我突然觉得这种堆叠即模块的设计思想,在当前iot物联网发展的如火如荼的时代又要被赋予最新的使命了。我十分看好risc-v的设计思想,也期待着与软件界的Linux一样,发展的繁荣昌盛。
bigmagic
2020/12/22
4.1K1
关于risc-v启动部分思考
CPU_X86架构和ARM架构入门篇
常见的四大CPU体系结构ARM、X86/Atom、MIPS、PowerPC,这里我们来看下主流的X86架构和ARM架构。
小小工匠
2021/08/17
4.8K0
RISC-V 简介(2)RISC-V指令集的特点及分类
上一篇RISC-V 简介(1)RISC-V的由来对RISC-V发展的背景进行了描述,通过与CISC架构的比较,以及与其他RISC架构的比较,将RISC-V的重要性和优势简单列了出来。本文将简单介绍RISC-V的指令集特点及分类。
IC知识库
2021/07/29
2K0
RISC-V 简介(2)RISC-V指令集的特点及分类
ARM、MIPS与RISC-V指令集有什么区别?
ARM、MIPS 和 RISC-V 是三种常见的精简指令集计算(RISC,Reduced Instruction Set Computing)架构,各自都有其特点和设计理念。
不脱发的程序猿
2025/02/25
1120
ARM、MIPS与RISC-V指令集有什么区别?
科技巨头欲转向开源架构 RISC-V,因 ARM 授权费用太贵
不久前,特斯拉加入 RISC-V 基金会,并考虑在新款芯片中使用免费的 RISC-V 设计。至此,已有 IBM、NXP、西部数据、英伟达、高通、三星、谷歌、华为等 100 多家科技公司加入 RISC-V 阵营。
顶级程序员
2018/07/23
7730
芯片也开源?网红RISC-V,到底是什么东东?
今天小枣君又要给大家介绍一个开源的东西,那就是现在半导体行业的网红、被很多人称之为“开源芯片”的RISC-V。
鲜枣课堂
2019/07/20
8230
《手把手教你设计CPU——RISC-V处理器》读书笔记
首先感谢面包板社区提供这本《手把手教你设计CPU——RISC-V处理器篇》书籍的试读机会。这本书和另外一本《 RISC-V架构与嵌入式开发 》是国内最先出版的两本关于RISC-V处理器的书籍,作者是胡振波先生,这里还要感谢胡老师。胡振波先生是国内最早开始研究RISC-V架构的,有超过8年的CPU以及超过10年的ASIC设计与验证经验,历任Marvell CPU高级设计工程师,Synopsys ARC系列处理器内核研发经理等职务,有着近20年的行业积累。
单片机点灯小能手
2020/07/16
2.6K0
『计算机的组成与设计』-指令:计算机的语言
计算机语言中的基本单词称为指令。一台计算机的全部指令称为该计算机的指令集。 尽管机器语言种类繁多,但他们之间十分相似,其差异性更像人类语言的”方言”。 本篇讲解 MIPS 指令集。
1ess
2021/10/29
3K0
『计算机的组成与设计』-指令:计算机的语言
RISC-V架构系列之1:指令集和特权模式
从2010年开始的RISC-V 项目,已经有10年的时间,RISC-V基金会先后批准了RISC-V Base ISA, Privileged Architecture,Processor Trace等规范。RISC-V对Linux的基本支持也已经完成。本文尝试通俗易懂的介绍RISC-V对于Linux的基本支持,包括指令集和异常处理。内存管理,迁移到RISC-V,UEFI,KVM等支持,欢迎继续关注本公众号。
Linux阅码场
2021/02/24
2.8K0
RISC-V架构系列之1:指令集和特权模式
优秀的 Verilog/FPGA开源项目介绍(二)-RISC-V
RISC-V(跟我读:“risk----------------five”)是一个基于精简指令集(RISC)原则的开源指令集架构(ISA)。
碎碎思
2021/10/18
3.8K0
优秀的 Verilog/FPGA开源项目介绍(二)-RISC-V
【AI系统】CPU 指令集架构
我们知道,计算机指令是指挥机器工作的指示和命令,程序就是一系列指令按照顺序排列的集合,执行程序的过程就是计算机的工作过程。从微观上看,我们输入指令的时候,计算机会将指令转换成二进制码存储在存储单元里面,然后在即将执行的时候拿出来。那么计算机是怎么知道我们输入的是什么指令,指令要怎么执行呢?
用户11307734
2024/11/26
2180
为什么要有 RISC-V
• 它要适用于所有实现技术,包括 FPGA(Field-Programmable Gate Array,现场可编程逻辑门阵列)、ASIC(Application-Specific Integrated Circuit,专用集成电路)、全定制芯片,甚至未来的制造元件技术。
刘盼
2023/12/26
2330
为什么要有 RISC-V
哪吒D1开发板RISC-V CLINT编程实践
当前riscv的中断控制器部分比较简单,不像arm那样复杂,设计的简单分析起来就比较容易理解清楚。相比于ARM的GIC,RISC-V这一套CLINT与PLINT简直太容易理解了。或许是因为ARM迭代的时间很长,积累了很多设计上的经验,RISCV还需要经过实际的市场的考验,才能真正的看到中断控制这一块的设计到底是否简洁并且设计合理。
bigmagic
2021/07/23
2.8K1
指令集架构(ISA)之IBM Power ISA开源应对​RISC-V生态(13k字)
科学Sciences导读:指令集架构(Instruction-SetArchitecture, ISA)之IBM Power ISA开源应对RISC-V生态。本文介绍IBMPower ISA开源概述;RISC-V和OpenPOWER如何共存;ower(处理器)九代产品概述;IBM的POWER和Intel的X86处理器比较。关键词:指令集,指令集架构,ISA,RISC-V,x86,中央处理器(CPU),英特尔(Intel),国际商用机器(IBM),POWER PC(或者PPC),开源。分享或赞赏支持后,公号输入框内发送“Power ISA”获取本文PDF。
秦陇纪
2019/08/26
2.8K0
指令集架构(ISA)之IBM Power ISA开源应对​RISC-V生态(13k字)
优秀的 Verilog/FPGA开源项目介绍(三十六)-RISC-V(新增一)
RISC-V(跟我读:“risk----------------five”)是一个基于精简指令集(RISC)原则的开源指令集架构(ISA)。
碎碎思
2023/02/14
6.7K0
优秀的 Verilog/FPGA开源项目介绍(三十六)-RISC-V(新增一)
相关推荐
芯片架构 X86 、 ARM 、RISC-V、MIPS、POWERPC、SPARC 区别
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文