这一部分对应的是 Lec 06 ~ 09 (哔大上的参考课程为 [Summer 20] ),重点围绕 RISC – V 的汇编语言知识点作解释与拓展
啊 RISC – V,想起来写 NJU – nemu PA2 的某些痛苦回忆 ( •́ὤ •̀)
指令集架构 (ISA) 的基本概念
核心定义
指令(Instruction):CPU 可以执行的基本操作,是硬件能够直接理解和执行的“动词”。
指令集架构(Instruction Set Architecture, ISA):一个特定 CPU 所实现的所有指令的集合。它定义了软件与硬件之间的协作关系。
RISC 哲学
ISA 的早期设计理念其实是两极分化的,包括 CISC 和 RISC 两种方向。
具体来说,CISC 倾向于使用一条指令完成复杂工作。希望通过强大的单条指令减少机器指令数,从而简化编译器设计和高阶语言之间的“语义差距”。而 RISC 则倾向于一条指令完成简单工作。希望通过简单的指令集提高执行效率,依赖多条指令的组合来完成复杂任务。
显然 CISC 对硬件的要求要小于 RISC ,但其某些指令的执行周期要远高于 RISC。
现代设计已不存在单纯的 RISC 或 CISC,两者的相互参考、相互借鉴是更常见的情况。
Registers – 寄存器
寄存器是 CPU 内部的高速存储单元,其访问速度远快于内存。使用有限数量的寄存器可以保持硬件简单高效。
RISC – V 规范下包括32个通用寄存器,编号从 x0
~ x31
,每个寄存器32 bits 宽
x0
永远为0,且写入x0
不会产生任何效果,因而被用于提供 Constant 0 或执行空操作- 寄存器本身没有类型一说,数据的类型和解释取决于对其进行操作的指令
RISC – V 的基本指令格式与语法
这部分有更权威的说明 —— RISC – V manual(汉化在这),此处只进行简要说明
基本格式
[操作码 目标寄存器, 源寄存器1, 源寄存器2/立即数
],比如:
add x1, x2, x3
addi x1, x2, 10 # imm 立即数
addi x1, x2, -10 # 所以不需要subi
设计优势
- 模块化:RISC – V 除了基础的整数指令集 (I),还有标准扩展,如乘法/除法 (M)、原子操作 (A)、单/双精度浮点 (F/D)。可以根据应用场景(如微控制器或超级计算机)像搭积木一样组合需要的扩展。
- 开源:伟大,无需多盐
加载与储存
前面我们提及了寄存器,它与内存(Memory)的区别是
- 寄存器快但小
- 内存大但慢
现代CPU对于指令的执行速度已经远远高于内存了,所以直接在内存中寻找数据是极其不明智且不现实的,我们有必要将数据从内存加载(Load) 到寄存器中进行计算,然后再将结果存储(Store) 回内存。
当然 Register 和 Memory 之间显然是有“代沟”的,这会在之后讨论 Cache (缓存) 时再进行讨论
内存编址与字节序 (Endianness)
- 字节寻址:内存的基本可寻址单元是字节(Byte, 8位),而不是字(Word, 32位)
- 字地址:一个字占 4 个字节。字的地址是其起始字节的地址。
- 大小端(Endianness):定义多字节数据(如一个字)在内存中的存储顺序。
- 小端序 (Little-Endian):最低有效字节(LSB) 存放在最低的内存地址处。RISC-V 和 x86 架构采用小端序。
- 大端序 (Big-Endian):最高有效字节(MSB) 存放在最低的内存地址处。用于网络协议和一些老的架构(如 PowerPC)。
加载指令 (Load Instructions)
- 格式:
lw rd, offset(rs1)
- 操作:从内存地址
[rs1 + offset]
处加载一个完整的字(32位) 到目标寄存器rd
。 - 示例:
lw x10, 12(x15)
- 计算地址:
Address = contents_of(x15) + 12
- 操作:将内存
[Address]
到[Address+3]
的 4 个字节加载到寄存器x10
中。 - C 语言等价操作:
int temp = array[3];
(假设x15
指向array[0]
,int
为 4 字节)。
- 计算地址:
存储指令 (Store Instructions)
- 格式:
sw rs2, offset(rs1)
- 操作:将源寄存器
rs2
中的一个完整的字(32位) 存储到内存地址[rs1 + offset]
处。 - 示例:
sw x10, 40(x15)
- 计算地址:
Address = contents_of(x15) + 40
- 操作:将寄存器
x10
中的 32 位数据存入内存[Address]
到[Address+3]
。 - C 语言等价操作:
array[10] = temp;
- 计算地址:
字节操作指令
- 加载字节 (lb):
lb rd, offset(rs1)
- 从地址
[rs1 + offset]
加载单个字节,并将其符号扩展为 32 位后存入rd
。 - 用于加载
char
(有符号)类型数据。
- 从地址
- 加载无符号字节 (lbu):
lbu rd, offset(rs1)
- 从地址
[rs1 + offset]
加载单个字节,并将其零扩展为 32 位后存入rd
。 - 用于加载
unsigned char
类型数据。
- 从地址
- 存储字节 (sb):
sb rs2, offset(rs1)
- 将寄存器
rs2
的最低 8 位存储到内存地址[rs1 + offset]
。 - 高位字节被忽略。
- 将寄存器
分支与循环
默认情况下,CPU 按顺序一条接一条地执行指令(PC = PC + 4
),但当我们陈列条件时,PC
需要根据条件测试的结果,决定是继续顺序执行还是跳转到程序的其他位置执行。这是实现 if
, else
, while
, for
等高级语言控制结构的基础。
条件分支指令
- 分支 if equal (
beq
):beq rs1, rs2, label
- 如果
rs1 == rs2
,则跳转到label
处执行;否则,继续执行下一条指令。
- 如果
- 分支 if not equal (
bne
):bne rs1, rs2, label
- 如果
rs1 != rs2
,则跳转到label
处执行。
- 如果
- 大小比较分支:
blt rs1, rs2, label
: Branch if Less Than(有符号比较)bltu rs1, rs2, label
: Branch if Less Than, Unsigned(无符号比较)bge rs1, rs2, label
: Branch if Greater than or Equal(有符号比较)bgeu rs1, rs2, label
: Branch if Greater than or Equal, Unsigned(无符号比较)
- 注意:RISC-V 没有
bgt
(Branch if Greater Than) 或ble
(Branch if Less or Equal) 指令。这些条件需要通过交换操作数并使用blt
/bge
来实现。例如,if (a > b)
编译为blt b, a, label
。
bne x13, x14, Else # if (i != j)
add x10, x11, x12 # then-clause: f = g + h
j Exit # skip else-clause
Else: sub x10, x11, x12 # else-clause: f = g - h
Exit:
无条件跳转指令
- 跳转 (
j
):j label
- 无条件地立即跳转到
label
处执行。 - 用于实现
else
后的跳转、break
、continue
以及循环的结尾跳转。
- 无条件地立即跳转到
逻辑指令 (Logical Instructions)
- 用途:对寄存器中的位(Bits)进行逐位操作,常用于位掩码(Bitmasking)、打包/解包数据、设置或清除特定位。
- 指令格式:与算术指令类似,有寄存器-寄存器和寄存器-立即数两种格式。
- 核心指令:
- AND (
and
,andi
):按位与。1 & 1 = 1
,其他为0
。- 常见用法:掩码(Masking)。用
andi
和一个位模式(掩码)可以提取(保留)或清除特定位。- 例如:
andi x5, x6, 0xFF
提取x6
的最低 8 位(一个字节)。
- 例如:
- 常见用法:掩码(Masking)。用
- OR (
or
,ori
):按位或。0 | 0 = 0
,其他为1
。- 常见用法:设置特定位为
1
。
- 常见用法:设置特定位为
- XOR (
xor
,xori
):按位异或。相同为0
,不同为1
。- 常见用法:翻转特定位(与
1
异或),比较两个值是否相同。
- 常见用法:翻转特定位(与
- AND (
- 没有 NOT 指令:RISC-V 没有单独的按位取反指令。需要通过异或全
1
来实现:xori x5, x6, -1
(因为-1
的二进制补码表示是全1
)。
移位指令 (Shift Instructions)
- 用途:将寄存器中的位向左或向右移动。
- 核心指令:
- Shift Left Logical (
sll
,slli
):向左移动指定位数,右侧空出的位用0
填充。- 效果:每左移 1 位,相当于对无符号数进行
* 2
操作。常用于快速计算$a\times 2^n$ 。
- 效果:每左移 1 位,相当于对无符号数进行
- Shift Right Logical (
srl
,srli
):向右移动指定位数,左侧空出的位用0
填充。- 效果:每右移 1 位,相当于对无符号数进行
/ 2
(向下取整)操作。
- 效果:每右移 1 位,相当于对无符号数进行
- Shift Right Arithmetic (
sra
,srai
):向右移动指定位数,左侧空出的位用原数的符号位填充。- 效果:用于对有符号数进行除以 2 的幂运算。但它实现的是向下取整,而非 C 语言标准的向零取整。
- 关键区别:对于负数,
srai
和/
的结果可能不同。例如-25 / 16
在 C 语言中结果是-1
(向零取整),但srai
的结果是-2
(向下取整)。
- Shift Left Logical (
程序执行与指令编码
程序计数器 (Program Counter – PC)
- 定义:一个特殊的 CPU 内部寄存器,永远保存着下一条将要被执行的指令的内存地址。
- 默认行为:在大多数指令执行后,
PC = PC + 4
(因为每条 RISC-V 指令是 4 字节长),从而顺序执行下一条指令。 - 改变执行流:
jal
,jalr
,j
, 以及所有条件分支指令 (beq
,bne
等) 会通过给PC
赋予一个新地址来改变程序的执行顺序。
2. 从汇编到机器码
- 汇编器 (Assembler):将人类可读的汇编代码(
.s
文件)翻译成机器可执行的二进制指令(目标文件.o
)。 - 链接器 (Linker):将多个目标文件(和库文件)合并成一个最终的可执行文件(
a.out
),其主要任务之一是解析指令中的地址(例如jal
后的标签地址)。
函数调用 (Function Calls)
这是这部分的核心和难点,至少我代码看起来有些吃力
函数调用的六个步骤
- 传递参数 (Put arguments in a place):将调用者(Caller)的参数放入被调用函数(Callee)可以访问的位置。
- 转移控制 (Transfer control):跳转到被调用函数的代码开始处。
- 获取本地存储 (Acquire storage):为被调用函数的局部变量分配内存空间(通常在栈上)。
- 执行函数体 (Perform task):运行函数的具体代码。
- 存放返回值 (Put return value):将函数的返回值放入调用者可以访问的位置。恢复任何被修改的寄存器,释放第 3 步分配的本地存储。
- 返回控制 (Return control):跳转回调用者调用函数之后的位置继续执行
RISC-V 函数调用约定 (Calling Conventions)
这是一套软硬件协同的规则,确保函数能正确协作。
- 参数传递:使用寄存器
a0
–a7
(即x10
–x17
)来传递前 8 个参数。更多参数通过栈传递。 - 返回值:使用寄存器
a0
和a1
(即x10
,x11
)来返回最多 2 个值。 - 返回地址:使用专用寄存器
ra
(即x1
)来存储函数调用完成后应该返回的地址。 - 保留寄存器 (Saved Registers):寄存器
s0
–s11
(即x8
,x9
,x18
–x27
)是被调用者的责任。如果一个函数要使用它们,必须先在栈上保存其原始值,并在返回前恢复。这保证了调用者的这些寄存器值不会被意外修改。 - 这一部分在课程 lab 中会被反复使用
函数调用指令
- 跳转并链接 (
jal
):jal rd, Label
- 操作:
- 将
PC + 4
(即下一条指令的地址)存入目标寄存器rd
- 跳转到
Label
处执行
- 将
- 最常见用法:
jal ra, sum
。这条指令一举完成了步骤 2 (转移控制) 和为步骤 6 (返回控制) 保存返回地址这两件大事。
- 操作:
- 跳转并链接寄存器 (
jalr
):jalr rd, offset(rs1)
- 操作:
- 将
PC + 4
存入rd
。 - 跳转到地址
[rs1 + offset]
处执行。
- 将
- 更通用,可用于实现函数指针调用和
ret
伪指令。
- 操作:
- 跳转寄存器 (
jr
):jr rs
-> 这是一个伪指令,等价于jalr x0, 0(rs)
。- 作用:跳转到
rs
寄存器所保存的地址。 - 最常见用法:
jr ra
,用于从函数返回(步骤 6)。它跳回到当初jal
指令保存的地址。
- 作用:跳转到
伪指令 (Pseudo-Instructions)
汇编器提供的便捷写法,最终会被翻译成一条或多条真实指令。
mv rd, rs
->addi rd, rs, 0
(寄存器拷贝)li rd, imm
->addi rd, x0, imm
(加载小立即数)ret
->jr ra
->jalr x0, 0(ra)
(函数返回)nop
->addi x0, x0, 0
(空操作,用于对齐或延迟)- 其余可见手册
至此,RISC – V 的第一阶段告一段落
发表回复