CS61C学习笔记(三)-RISCV-Assembly
前言
RISC-V是基于RISC精简指令集架构开发的一个开放式指令集架构,它是由加州大学伯克利分校的计算机科学教授Krste Asanovic(克里斯蒂安·阿萨诺维奇)领导的团队开发,RISC-V是开放的,任何人都可以使用它来开发处理器芯片和其他硬件,而无需支付任何许可或使用费用。RISC-V的设计简单,易于扩展和自定义,可以在各种应用场景和市场中使用。
什么是指令集架构?
指令集架构(Instruction Set Architecture,简称ISA)是计算机系统中的一个重要概念,指的是计算机中处理器的指令集和处理器的内部结构,即处理器是如何执行指令的。
ISA规定了一套指令集,包括指令的种类、指令的格式、指令的操作数、指令的执行方式等。ISA也规定了处理器的内部结构,包括处理器的寄存器、指令流水线、内存管理单元等。
不同的ISA有不同的指令集和内部结构设计,因此处理器的计算能力和性能也会有所不同。常见的ISA包括ARM、x86、MIPS、PowerPC、RISC-V
等。ISA的选择对计算机系统的性能、功耗、软件兼容性、应用场景等都有很大的影响。
简单点比喻可以把指令集架构理解为图纸,处理器就是房子。
RISC和RISC-V的区别
RISC全称Reduced Instruction Set Computer
,即精简指令计算机,它是指令集架构,它的理念在于旨在通过减少指令集的数量和复杂度,提高计算机的性能和效率。
RISC最初由加州大学伯克利分校David Patterson
和John Hennessy
在1980年代提出,并于1984年合著了一本名为“Computer Architecture: A Quantitative Approach”(中文译名为《计算机体系结构:量化研究方法》)的经典教材,该书详细介绍了RISC架构和量化分析方法,成为当时计算机体系结构领域的重要参考书。
RISC-V是由Krste Asanovic教授领导其团队基于RISC精简指令集架构开发的一款开源指令集架构,RISC-V具有更好的灵活性以及扩展性,同时它比RISC更加简单便于它人二次开发和扩展,并且它不需要任何授权费,可以应用于商业场景因为它遵循BSD开源协议,可以把RISC-V理解为RISC的扩展版。
伟大的想法
抽象
寄存器在处理器内部
##
Principle of Locality / Memory Hierarchy
寄存器和存储器的速度对比
32个寄存器的介绍
- x0 - x31(通用寄存器):
- 这些寄存器是通用的,可用于各种目的。
- x0 通常用作零寄存器,任何写入它的值都会被忽略。
- 其余的通用寄存器可以用于存储数据、地址、临时值等。
- pc(程序计数器):
- 用于存储当前正在执行的指令的地址。
- 在每次指令执行后,PC会自动增加,以指向下一条指令。
- ra(返回地址寄存器):
- 用于存储函数调用返回时的地址。
- 在函数调用时,调用者将返回地址存储在此寄存器中。
- sp(栈指针寄存器):
- 指向当前栈顶的地址。
- 用于管理函数调用和局部变量的内存分配。
- gp(全局指针寄存器):
- 指向全局数据区域的起始地址。
- 用于访问全局变量和静态数据。
- tp(线程指针寄存器):
- 指向线程控制块(TCB)的地址。
- 在多线程环境中用于管理线程的上下文信息。
- t0 - t6(临时寄存器):
- 用于存储临时数据。
- 可以在函数调用和数据操作中使用。
- s0 - s11(保存寄存器):
- 用于保存函数调用时需要保持的值。
- 在函数调用期间,这些寄存器的值会被保留,并在函数返回时恢复。
- a0 - a7(参数寄存器):
- 用于存储函数调用的参数。
- 调用者将参数传递给被调用函数时,将它们存储在这些寄存器中。
指令集
Addition/subtraction
1 | add rd, rs1, rs2 |
Add immediate
1 | addi rd, rs1, imm |
Load from Memory to Register
Load Word(lw)
1 | int A[100]; |
1 | lw x10, 12(x15) # Reg x10 gets A[3] |
Note:
x15 – base register (pointer to A[0])
12 – offset in bytes
Offset must be a constant known at assembly time
Store from Register to Memory
Store Word(sw)
1 | int A[100]; |
1 | lw x10,12(x15) # Temp reg x10 gets A[3] |
Note:
x15 – base register (pointer)
12,40 – offsets in bytes
x15+12
andx15+40
must be multiples of 4
Loading and Storing Bytes
load byte:
lb
store byte:
sb
1 | lb x10, 3(x11) |
contents of memory location with address = sum of “3” + contents of register x11 is copied to the low byte position of register x10
- lw:这是”Load Word”的缩写。它从内存中加载一个32位的字(word)到寄存器中。具体来说,它将内存中指定地址的32位数据加载到目标寄存器中。
- lb:这是”Load Byte”的缩写。它从内存中加载一个8位的字节(byte)到寄存器中。与lw不同,lb将会把加载的字节进行符号扩展,即最高位的值会被复制到目标寄存器的所有高位上,以保持字节的符号性。
举个例子,如果在地址0x100处的内存中存储着0xFF123456,当你用lw指令加载0x100地址的数据时,你会得到0xFF123456;但是当你用lb指令加载同样的地址时,你会得到0xFFFFFF56,因为最高位是1,这个字节是负数。
Branches类型
Branch – change of control flow
Conditional Branch – change control flow depending on outcome of comparison
beq: branch if equal
bne: branch if not equal
blt: branch if less than
bge: branch if greater than or equal
bltu: unsigned versions of blt
bgeu: unsigned versions of bge
Unconditional Branch – always branch
- j: jump
- j label
No bgt
or ble
!
例子
1 | beq reg1, reg2, L1 |
Loops
- C
- while
- do…while
- for
- 每一个都可以被重写成另外两个之一,因此相同的分支方法也可以应用到这些循环中。
- 关键概念:虽然在 RISC-V 中有多种编写循环的方式,但决策的关键在于条件分支。
Logical Instructions
有助于在一个字内操作位字段
- 例如,一个字内的字符(8位)
将位打包/解包成字的操作
对于一个字(8个位,例如二进制数字或字符),在其中操作位字段(例如单独的字符)是很有用的。这就像在一个字母里面寻找、添加或者移除特定的位一样。这些操作可能包括将多个位打包(合并)成一个字,或者将一个字解包(拆分)成多个位。这些操作通常被称为逻辑操作。
and/andi
1 | and x5, x6, x7 # x5 = x6 & x7 |
andi
with0000 00FF
hexisolates the least significant byteandi
withFF00 0000
hexisolates the most significant byte
No Not in RISC-V
没有逻辑非运算在RISC-V中
- 使用xor 1111 1111
two
Logical Shifting
Arithmetic Shifting
函数调用
函数调用的六个步骤
- 将参数放在函数能够访问的位置
- 转交权利给函数
- 请求函数所需要的内存资源
- 执行函数预期的任务
- 将返回值放在调用者可以访问的位置并恢复你用过的寄存器;释放内存空间
- 将控制返回调用者,因为一个函数可以被许多项目中的程序点调用
函数的指令支持
在RISC-V中,所有的指令都是4bytes,并且他们像数据一样存储在内存中
- 问题: 为什么这里用
jr
而不是j
? - 答案: sum可能在许多地方被调用,所以我们不可能总是回到固定的位置。sum的调用过程必须能够说”回到这里”。
jal (Jump and Link)
指令格式:
jal rd, offset
功能:跳转到指定地址,并将下一条指令的地址存储到目标寄存器中(通常用于存储返回地址)。
操作
:
- 将当前指令的地址(PC寄存器的值)加上偏移量作为目标地址。
- 将下一条指令的地址(PC+4)存储到目标寄存器中。
示例:
jal x1, 100
表示跳转到地址为 PC+100 处,并将 PC+4 存储到寄存器 x1 中。
jalr (Jump and Link Register)
指令格式:
jalr rd, rs1, offset
功能:跳转到指定地址(通过寄存器计算得出),并将下一条指令的地址存储到目标寄存器中(通常用于存储返回地址)。
操作
:
- 将当前指令的地址(PC寄存器的值)加上偏移量,并将结果与 rs1 寄存器的值相加作为目标地址。
- 将下一条指令的地址(PC+4)存储到目标寄存器中。
示例:
jalr x1, x2, 0
表示跳转到地址为 x2 的值加上偏移量 0 所指示的地址,并将 PC+4 存储到寄存器 x1 中。
用法比较
jal
直接跳转到指定地址,适用于跳转到相对地址已知的情况。jalr
可以通过寄存器计算跳转地址,适用于跳转到相对地址不确定但通过寄存器存储的情况。
总结
准确来说,只有两个指令
jal rd, Label
- jump-and-linkjalr rd, rs, imm
- jump -and-link register
j
, jr
和 ret
是伪指令!
j: jal x0, Label
例子
之前的寄存器值在函数调用之后保存在哪里以便恢复它们?
- 在函数调用前需要一个地方去保存旧的值,以便在返回时恢复它们,然后删除。
- 解决方案就是使用栈-last-in-first-out (LIFO) queue
- Push: placing data onto stack
- Pop: removing data from stack
栈
栈帧包括:
返回“指令”地址
参数(参数)
其他局部变量的空间
栈帧是内存中连续的块;栈指针告诉我们栈帧的底部在哪里。
当过程结束时,栈帧被从栈中移除;为未来的栈帧释放内存。
sp从内存高到低增长
例子
嵌套调用和寄存器约定
嵌套调用
- 有一个叫做
sumSquare
的函数,现在sumSquare
正在调用mult
。 - 所以在
sumSquare
想要跳回的ra
寄存器中有一个值,但是这个值将会被对mult
的调用覆盖。 - 嵌套过程需要在调用
mult
之前保存sumSquare
的返回地址 - 再次使用栈。
寄存器约定
- Caller: 调用函数
- Callee: 被调用的函数
- 当被调用函数执行完毕后,调用者需要知道哪些寄存器可能已经改变,哪些是保证不变的。
- 寄存器约定:一组被广泛接受的规则,规定了在过程调用(
jal
)后哪些寄存器不会改变,哪些可能会改变。
为了减少因溢出和恢复寄存器而产生的昂贵的加载和存储操作,RISC-V函数调用约定将寄存器分为两类:
在函数调用中保留的寄存器:
- 调用者可以依赖于其值不变
- sp、gp、tp、”保存的寄存器” s0-s11(s0也是fp)
在函数调用中不保留的寄存器:
调用者不能依赖于其值不变
参数/返回寄存器 a0-a7、ra、”临时寄存器” t0-t6
Memory Allocation
在栈中分配空间
C语言有两种存储类别:自动的和静态的。
自动变量是函数局部的,并在函数退出时被丢弃。
静态变量存在于程序从一个过程退出到另一个过程进入的过程中。
使用栈来存储不适合寄存器的自动(局部)变量。
过程帧或激活记录:栈中的一个段,包含保存的寄存器和局部变量。
使用栈
内存分配Memory Allocation
当运行C程序时,有三个重要的内存区域被分配:
- 静态区:一次性声明的变量,程序执行完毕后才消失 - 例如,C全局变量
- 堆:通过malloc动态声明的变量
- 栈:在执行过程中被过程使用的空间;这是我们可以保存寄存器值的地方
Where is the Stack in Memory?
RV32约定(RV64/RV128具有不同的内存布局)
- 栈从高地址开始并向下增长
- 十六进制:bfff_fff0
hex - 栈必须对齐到16字节边界(在之前的示例中不是这样)
- 十六进制:bfff_fff0
- RV32程序(文本段)在低端
- 0001_0000
hex
- 0001_0000
- 静态数据段(常量和其他静态变量)在文本段上方,用于存放静态变量
- RISC-V约定全局指针(gp)指向静态变量
- RV32 gp = 1000_0000
hex
- 堆位于静态数据段上方,用于存放增长和收缩的数据结构;堆向高地址增长