[笔记二] 学习开发一个RISC-V上的操作系统
资料来源
RISC-V 汇编语言
- 汇编语言(Assembly Language)是一种"低级"语言
- 汇编语言的缺点:难读、难写、难移植
- 汇编语言的优点:灵活、强大
- 汇编语言的应用场景
- 需要直接访问底层硬件的地方
- 需要对性能执行极致优化的地方
汇编语言语法介绍(GNU版本)
- 一个完整的 RISC-V 汇编程序有多条语句(statement)组成
- 一条典型的 RISC-V 汇编语句由 3 部分组成:
- [label:] [operation] [comment]
- label(标号):GNU汇编中,任何以冒号结尾的标识符都被认为是一个标号
- operation可以有以下多种类型:
- instruction(指令):直接对应二进制机器指令的字符串
- pseudo-instruction(伪指令):为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令
- directive(指示/伪操作):.通过类似指令的形式(以"."开头),通知汇编器如何控制代码的产生等,不对应具体的指令
- macro:采用.macro/.endn自定义宏
- comment(注释):常用形式,"#"开始到当前行结束
RISC-V 汇编指令操作对象
- 寄存器
- 32个通用寄存器,x0~x31(RV32I的通用寄存器组)
- 在RISC-V中,Hart在执行算数逻辑运算时所操作的数据必须直接来自寄存器
- 内存
- Hart可以执行在寄存器和内存之间的数据读写操作
- 读写操作使用字节(Byte)为基本单位进行寻址
- RV32可以访问最多2^32个字节的内存空间
- x0比较特殊,只读,返回永远是0,写没有任何意义
RISC-V 汇编指令总览
RISC-V 汇编指令编码格式
- 指令长度:ILEN1 = 32 bits(RV32I)
- 指令对齐:IALIGN = 32 bits(RV32I)
- 32个bit划分成不同的"域(field)"
- funct3/funct7和opcode一起决定最终的指令类型
- 没必要去记每条指令,只要知道在哪查即可
- 指令在内存中按照小端序排列
- 主机字节序(HBO - Host Byte Order)
- 一个多字节整数 在计算机内存中存储的字节顺序称主机字节序或本地字节序
- 不同类型CPU的HBO不同,这与CPU的设计有关,分为大端序(Big-Endian)和小端序(Little-Endian)
- 大端(Big-Endian):数据的高位字节存放在内存的低地址
- 小端(Little-Endian):数据的低位字节存放在内存的低地址
- 6种指令格式:
- R-type:(Register),有三个fields,用于指定3个寄存器参数
- I-type:(Immediate),两个寄存器参数外+一个立即数参数(宽度为12bits)
- S-type:(Store),两个寄存器参数+一个立即数参数(宽度为12bits,但fields的组织形式不同于I-type)
- B-type:(Branch),除了带有两个寄存器参数外,还有一个立即数参数(宽度为12bits,但取值为2的倍数)
- U-type:(Upper),一个寄存器参数+一个立即数参数(宽度为 20bits,用于表示一个立即数的高20位)
- J-type:(Jump),一个寄存器参数+一个立即数参数(宽度为20bits)
RISC-V 汇编指令详解
算术运算指令(Arithmetic Instructions)
- add、sub、addi(ADD immediate)、LUI(Load Upper Immediate)
指令 语法 描述 eg ADD ADD RD, RS1, RS2 RS1和RS2的值相加,结果保存到RD add x5, x6, x7 SUB SUB RD, RS1, RS2 RS1的值减去RS2的值,结果保存到RD sub x5, x6, x7 ADDI ADDI RD, RS1, IMM RS1的值和IMM相加,结果保存到RD addi x5, x6, 100 LUI LUI RD, IMM 构造一个32位的数,高20位存放IMM,低12位清零。结果保存到RD lui x5, 0x12345 AUIPC AUIPC RD, IMM 构造一个32位的数,高20位存放IMM,低12位清零。结果和PC 相加后保存到RD auipc x5, 0x12345
- 有符号数在计算机中的表示:二进制补码(two’s complement),eg:-4(2b’1111100)
- 符号拓展(sign extension) vs 零扩展(Zero extension)
- 基于算术运算指令实现其他伪指令
伪指令 语法 等价指令 指令描述 eg LI LI RD, IMM LUI和ADDI的组合 将立即数IMM加载到RD中 li x5, 0x12345678 LA LA RD, LABEL AUIPC和ADDI的组合 为RD加载一个地址值 la x5 label NEG NEG RD,RS SUB RD,x0,RS 对RS中的值取反并将结果存放在RD中 neg x5,x6 MV MV RD,RS ADDI RD,RS,0 将RS中的值拷贝到RD中 mv x5,x6 NOP NOP ADDI x0,x0,0 什么也不做 nop
- 利用LUI+ADDI来为寄存器加载一个大数据0x12345FFF
- lui x1,0x12346
- addi x1,x1,-1
- LI(Load Immediate):构造出的为绝对地址
- AUIPC:构造出的为相对地址,相比LI + PC
- LA(Load Address):函数或变量指针赋值
逻辑运算指令(Logical Instructions)
- and、or、xor(eXclusive OR)、andi、ori、xori
指令 格式 语法 描述 eg AND R-type AND RD,RS1,RS2 RD=RS1&RS2 and x5,x6,x7 OR R-type OR RD,RS1,RS2 RD=RS1|RS2 or x5,x6,x7 XOR R-type XOR RD,RS1,RS2 RD=RS1^RS2 xor x5,x6,x7 ANDI I-type ANDI RD,RS1,IMM RD=RS1&IMM andi x5,x6,20 ORI I-type ORI RD,RS1,IMM RD=RS1|IMM or x5,x6,20 XORI I-type XORI RD,RS1,IMM RD=RS1^IMM xor x5,x6,20 伪指令 语法 等价指令 描述 eg NOT NOT RD,RS XORI RD,RS,-1 对RS的值按位取反,结果存放在RD中 not x5,x6
移位运算指令(Shifting Instructions)
逻辑移位
指令 格式 语法 描述 eg SLL R-type SLL RD,RS1,RS2 逻辑左移(Shift Left Logical)RD=RS1<<RS2 sll x5,x6,x7 SRL R-type SRL RD,RS1,RS2 逻辑右移(Shift Right Logical)RD=RS1>>RS2 逻辑左移立即数(Shift Left Logical Immediate)RD=RS1<<IMM SLLI I-type SLLI RD,RS1,IMM 逻辑左移立即数(Shift Left Logical Immediate)RD=RS1<<IMM slli x5,x6,3 SRLI I-type SRLI RD,RS1,IMM 逻辑右移立即数(Shift Right Logical Immediate)RD=RS1>>IMM srli x5,x6,3
- 无论是逻辑左移还是右移,补足的都是0
算术移位
指令 格式 语法 描述 eg SRA R-type SRA RD,RS1,RS2 算术右移(Shift Right Arithmetic)RD=RS1>>RS2 sra x5,x6,x7 SRAI I-type SRAI RD,RS1,IMM 算术右移立即数(Shift Right Arithmetic Immediate)RD=RS1>>IMM srai x5,x6,3
- 算术右移时按照符号位值补足,没有算术左移
内存读写指令(Load and Store Instructions)
- 内存读指令:Load,将数据从内存读入寄存器
- 内存写指令:Store,将数据从寄存器写出到内存
指令 格式 语法 描述 eg LB I-type LB RD,IMM(RS1) Load Byte,从内存中读取一个8bits的数据到RD中,内存地址=RS1+IMM,数据在保存到RD之前会执行sign-extended(符号扩展) lb x5,40(x6) LBU I-type LBU RD,IMM(RS1) Load Byte Unsigned,从内存中读取一个8bits的数据到RD中,内存地址=RS1+IMM,数据在保存到RD之前会执行zero-extended(零扩展) lbu x5,40(x6) LH I-type LH RD,IMM(RS1) Load Halfword,从内存中读取一个16bits的数据到RD中,内存地址=RS1+IMM,数据在保存到RD之前会执行sign-extended lh x5,40(x6) LHU I-type LHU RD,IMM(RS1) Load Halfword Unsigned,从内存中读取一个16bits的数据到RD中,内存地址=RS1+IMM,数据在保存到RD之前会执行zero-extended lhu x5,40(x6) LW I-type LW RD,IMM(RS1) Load Word,从内存中读取一个32bits的数据到RD中,内存地址=RS1+IMM lw x5,40(x6) SB S-type SB RS2,IMM(RS1) Store Byte,将RS2寄存器中低8bits的数据写出到内存中,内存地址=RS1+IMM sb x5,40(x6) SH S-type SH RS2,IMM(RS1 Store Halfword,将RS2寄存器中低16bits的数据写到内存中,内存地址=RS1+IMM sh x5,40(x6) SW S-type SW RS2,IMM(RS1) Store word,将RS2寄存器中低32bits的数据写到内存中,内存地址=RS1+IMM sh x5,40(x6)
- IMM给出的偏移量范围为[-2^11,2^11-1]
- 为何对于load要区分无符号方式和有符号方式,而store不区分?
条件分支指令(Condition Branch Instructions)
指令 格式 语法 描述 eg BEQ B-type BEQ RS1,RS2,IMM Branch if EQual。比较RS1和RS2的值,如果相等,则执行路径跳转到一个新的地址 beq x5, x6, 100 BNE B-type BNE RS1,RS2,IMM Branch if Not Equal。比较RS1和RS2的值,如果不相等,则执行路径跳转到一个新的地址 bne x5, x6, 100 BLT B-type BLT RS1,RS2,IMM Branch if Less Than。按照有符号方式比较RS1和RS2的值,如果RS1<RS2,则执行路径跳转到一个新的地址 blt x5, x6, 100 BLTU B-type BLTU RS1,RS2,IMM Branch if Less Than (Unsigned)。按照无符号方式比较RS1和RS2的值,如果RS1<RS2,则执行路径跳转到一个新的地址 bltu x5, x6, 100 BGE B-type BGE RS1,RS2,IMM Branch if Greater than or Equal。按照有符号方式比较RS1和RS2的值,如果RS1>=RS2,则执行路径跳转到一个新的地址 bge x5, x6, 100 BGEU B-type BGEU RS1,RS2,IMM Branch if Greater than or Equal (Unsigned)。按照无符号方式比较RS1和RS2的值,如果RS1>=RS2,则执行路径跳转到一个新的地址 bgeu x5, x6, 100
- 跳转的目标地址计算方法:先将 IMM x 2,符号扩展后和 PC 值相加得到最终的目标地址,所以跳转范围是以 PC 为基准,+/- 4KB 左右 ([-4096, 4094])
- 具体编程时,不会直接写 IMM,而是用标号代替,交由链接器来最终决定 IMM 的值
伪指令 语法 等价指令 描述 BLE BLE RS, RT, OFFSET BGE RT, RS, OFFSET Branch if Less & Equal,有符号方式比较,如果 RS <= RT,跳转到OFFSET BLEU BLEU RS, RT, OFFSET BGEU RT, RS, OFFSET Branch if Less or Equal Unsigned,无符号方式比较,如果 RS <= RT,跳转到OFFSET BGT BGT RS, RT, OFFSET BLT RT, RS, OFFSET Branch if Greater Than,有符号方式比较,如果 RS > RT,跳转到OFFSET BGTU BGTU RS, RT, OFFSET BLTU RT, RS, OFFSET Branch if Greater Than Unsigned,无符号方式比较,如果 RS > RT,跳转到OFFSET BEQZ BEQZ RS, OFFSET BEQ RS, x0, OFFSET Branch if EQual Zero, 如果RS == 0,跳转到OFFSET BNEZ BNEZ RS, OFFSET BNE RS, x0, OFFSET Branch if Not Equal Zero, 如果RS != 0,跳转到OFFSET BLTZ BLTZ RS, OFFSET BLT RS, x0, OFFSET Branch if Less Than Zero, 如果RS < 0,跳转到OFFSET BLEZ BLEZ RS, OFFSET BGE x0, RS, OFFSET Branch if Less or Equal Zero, 如果RS <= 0,跳转到OFFSET BGTZ BGTZ RS, OFFSET BLT x0, RS, OFFSET Branch if Greater Than Zero,如果RS > 0,跳转到OFFSET BGEZ BGEZ RS, OFFSET BGE RS, x0, OFFSET Branch if Greater or Equal Zero, 如果RS >= 0,跳转到OFFSET
无条件跳转指令(Unconditional Jump Instructions)
JAL(Jump And Link)
语法 例子 JAL RD, LABEL jal x1, label
- JAL 指令使用 J-type 编码格式
- JAL 指令用于调用子过程 (subroutine/function)
- 子过程的地址计算方法:首先对 20 bits 宽的 IMM x 2 后进行 sign-extended,然后将符号扩展后的值和 PC 的值相加。因此该函数跳转的范围是以 PC 为基准,上下~+/- 1MB
- JAL 指令的下一条指令的地址写入 RD,保存为返回地址
- 实际编程时,用 label 给出跳转的目标,具体 IMM 值由编译器和链接器最终负责生成
JALR(Jump And Link Register )
语法 例子 JALR RD, IMM(RS1) jalr x0, 0(x5)
JALR 指令使用 I-type 编码格式
JALR 指令用于调用子过程 (subroutine/function)
子过程的地址计算方法:首先对 12 bits 宽的 IMM 进行 sign-extended,然后将符号扩展后的值和 RS1 的值相加,得到最终的结果后将其最低位设置为 0(确保地址按 2 字节对齐)。因此该函数跳转的范围是以 RS1 为基准,上下~+/- 2KB
JALR 指令的下一条指令的地址写入 RD,保存为返回地址
如何解决更远距离的跳转?
- AUIPC x6,IMM-20
- JALR x1,x6,IMM-12
如果跳转后不需要返回,可以利用 x0 代替 JAL 和 JALR 中的 RD
伪指令 语法 等价指令 例子 J J OFFSET JAL X0, OFFSET j leap JR JR RS JALR X0, 0(RS) jr x2
RISC-V指令寻址模式总结
寻址即指令中定位操作数(oprand)或者地址的方式
寻址模式 解释 例子 立即数寻址 操作数是指令本身的一部分 addi x5, x6,20 寄存器寻址 操作数存放在寄存器中,指令中指定访问的寄存器从而获取该操作数 add x5, x6, x7 基址寻址 操作数在内存中,指令中通过指定寄存器(基址base)和立即数(偏移量offset),通过base + offset 的方式获得操作数在内存中的地址从而获取该操作数 sw x5, 40(x6) PC相对寻址 在指令中通过PC 和指令中的立即数相加获得目标地址的值 beq x5, x6, 100
RISC-V 函数调用的编程约定(Calling Conventions)
Caller 调用者 vs Callee 被调用者
- 有关寄存器的编程约定
寄存器名 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
- 函数跳转和返回指令的编程约定
伪指令 等价指令 描述 例子 jal offset jal x1, offset 跳转到offset 制定位置,返回地址保存在x1 (ra) jal foo jalr rs jalr x1, 0(rs) 跳转到rs 中值所指定的位置,返回地址保存在x1 (ra) jalr s1 j offset jal x0, offset 跳转到offset 制定位置,不保存返回地址 j loop jr rs jalr x0, 0(rs) 跳转到rs 中值所指定的位置,不保存返回地址 jr s1 call offset auipc x1, offset[31 : 12] + offset[11] jalr x1, offset[11:0](x1) 长跳转调用函数 call foo tail offset auipc x6, offset[31 : 12] + offset[11] jalr x0, offset[11:0](x6) 长跳转尾调用 tail foo ret jalr x0, 0(x1) 从Callee 返回 ret 函数调用过程中实现被调用函数的编程约定
- 函数起始部分(Prologue)
- 减少sp 的值,根据本函数中使用saved 寄存器的情况以及local 变量的多少开辟栈空间
- 将saved 寄存器的值保存到栈中
- 如果函数中还会调用其他的函数,则将ra 寄存器的值保存到栈中
- 函数执行体
- 函数退出部分(Epilogue)
- 从栈中恢复saved 寄存器
- 如果需要的话,从栈中恢复ra 寄存器
- 增加sp 的值,恢复到进入本函数之前的状态
- 调用ret 返回
RISC-V 汇编 与 C 混合编程
遵守 ABI (Abstract Binary Interface)的规定• 数据类型的大小,布局和对齐
- 函数调用约定(Calling Convention)
- 系统调用约定
- …
RISC-V 函数调用约定规定:
- 函数参数采用寄存器a0 ~ a7 传递
- 函数返回值采用寄存器a0 和a1 传递
C 函数中嵌入 RISC-V 汇编
asm [volatile] (
“汇编指令”
: 输出操作数列表(可选)
: 输入操作数列表(可选)
: 可能影响的寄存器或者存储器(可选)
);
int foo(int a, int b)
{
int c;
asm volatile (
"add %[sum], %[add1], %[add2]"
:[sum]"=r"(c)
:[add1]"r"(a), [add2]"r"(b)
);
return c;
}
int foo(int a, int b)
{
int c;
asm volatile (
"add %0, %1, %2"
:"=r"(c)
:"r"(a), "r"(b)
);
return c;
}
- 汇编指令用双引号括起来,多条指令之间用";" 或者"\n" 分隔
- “输出操作数列表” 和“输入操作数列表” 用于将需要操作的C 变量和汇编指令的操作数对应起来,多个操作数之间用“,” 分隔
- “可能影响的寄存器或者存储器” 用于告知编译器当前嵌入的汇编语句可能修改的寄存器或者内存,方便编译器执行优化