CS61C – 2 RISC-V Assembly Language(1)

这一部分对应的是 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 需要根据条件测试的结果,决定是继续顺序执行还是跳转到程序的其他位置执行。这是实现 ifelsewhilefor 等高级语言控制结构的基础。

条件分支指令

  • 分支 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 后的跳转、breakcontinue 以及循环的结尾跳转。

逻辑指令 (Logical Instructions)

  • 用途:对寄存器中的位(Bits)进行逐位操作,常用于位掩码(Bitmasking)、打包/解包数据、设置或清除特定位。
  • 指令格式:与算术指令类似,有寄存器-寄存器和寄存器-立即数两种格式。
  • 核心指令
    • AND (andandi):按位与。1 & 1 = 1,其他为 0
      • 常见用法掩码(Masking)。用 andi 和一个位模式(掩码)可以提取(保留)或清除特定位。
        • 例如:andi x5, x6, 0xFF 提取 x6 的最低 8 位(一个字节)。
    • OR (orori):按位或。0 | 0 = 0,其他为 1
      • 常见用法:设置特定位为 1
    • XOR (xorxori):按位异或。相同为 0,不同为 1
      • 常见用法:翻转特定位(与 1 异或),比较两个值是否相同。
  • 没有 NOT 指令:RISC-V 没有单独的按位取反指令。需要通过异或全 1 来实现:xori x5, x6, -1(因为 -1 的二进制补码表示是全 1)。

移位指令 (Shift Instructions)

  • 用途:将寄存器中的位向左或向右移动。
  • 核心指令
    • Shift Left Logical (sllslli):向左移动指定位数,右侧空出的位用 0 填充。
      • 效果:每左移 1 位,相当于对无符号数进行 * 2 操作。常用于快速计算$a\times 2^n$ 。
    • Shift Right Logical (srlsrli):向右移动指定位数,左侧空出的位用 0 填充。
      • 效果:每右移 1 位,相当于对无符号数进行 / 2(向下取整)操作。
    • Shift Right Arithmetic (srasrai):向右移动指定位数,左侧空出的位用原数的符号位填充。
      • 效果:用于对有符号数进行除以 2 的幂运算。但它实现的是向下取整,而非 C 语言标准的向零取整
      • 关键区别:对于负数,srai 和 / 的结果可能不同。例如 -25 / 16 在 C 语言中结果是 -1(向零取整),但 srai 的结果是 -2(向下取整)。

程序执行与指令编码

程序计数器 (Program Counter – PC)

  • 定义:一个特殊的 CPU 内部寄存器,永远保存着下一条将要被执行的指令的内存地址
  • 默认行为:在大多数指令执行后,PC = PC + 4(因为每条 RISC-V 指令是 4 字节长),从而顺序执行下一条指令。
  • 改变执行流jaljalrj, 以及所有条件分支指令 (beqbne等) 会通过给 PC 赋予一个新地址来改变程序的执行顺序。

2. 从汇编到机器码

  • 汇编器 (Assembler):将人类可读的汇编代码(.s 文件)翻译成机器可执行的二进制指令(目标文件 .o)。
  • 链接器 (Linker):将多个目标文件(和库文件)合并成一个最终的可执行文件(a.out),其主要任务之一是解析指令中的地址(例如 jal 后的标签地址)。

函数调用 (Function Calls)

这是这部分的核心和难点,至少我代码看起来有些吃力

函数调用的六个步骤

  1. 传递参数 (Put arguments in a place):将调用者(Caller)的参数放入被调用函数(Callee)可以访问的位置。
  2. 转移控制 (Transfer control):跳转到被调用函数的代码开始处。
  3. 获取本地存储 (Acquire storage):为被调用函数的局部变量分配内存空间(通常在上)。
  4. 执行函数体 (Perform task):运行函数的具体代码。
  5. 存放返回值 (Put return value):将函数的返回值放入调用者可以访问的位置。恢复任何被修改的寄存器,释放第 3 步分配的本地存储。
  6. 返回控制 (Return control):跳转回调用者调用函数之后的位置继续执行

RISC-V 函数调用约定 (Calling Conventions)

这是一套软硬件协同的规则,确保函数能正确协作。

  • 参数传递:使用寄存器 a0a7(即 x10x17)来传递前 8 个参数。更多参数通过栈传递。
  • 返回值:使用寄存器 a0 和 a1(即 x10x11)来返回最多 2 个值。
  • 返回地址:使用专用寄存器 ra(即 x1)来存储函数调用完成后应该返回的地址
  • 保留寄存器 (Saved Registers):寄存器 s0s11(即 x8x9x18x27)是被调用者的责任。如果一个函数要使用它们,必须先在栈上保存其原始值,并在返回前恢复。这保证了调用者的这些寄存器值不会被意外修改。
  • 这一部分在课程 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 的第一阶段告一段落

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注