RISC-V 入门-RISC-V 汇编语言编程

汇编语法介绍

一条典型的 RISC-V 汇编语句由三个部分组成[label:][operation][comment]
后缀.s.S区别:后者纯汇编。

  • label(标号)
  • operation 可以有以下多种类型:
    • instruction (指令) :直接对应二进制机器指令的宇符串
    • pseudo-instruction (伪指令) :为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令 (instructions)。
    • directive (指示/伪操作) :通过类似指令的形式(以”.”开头),通知汇编器如何控制代码的产生等,不对应具体的指令。
    • macro:采用.macro/.endm 自定义的宏
      例子
.macro do_nothing  # directive
  nop    # pseudo-instruction
  nop    # pseudo-instruction
.endm      # directive

  .text    # directive
  .global _start  # directive
_start:     # Label
  li x6, 5  # pseudo-instruction
  li x7, 4  # pseudo-instruction
  add x5, x6, x7  # instruction
  do_nothing  # Calling macro
stop:  j stop    # statement in one line

  .end    # End of file
  • comment(注释)以#开头到行尾

RISC-V 汇编指令总览

操作对象

  • 寄存器
    • 32个通用寄存器,x0 ~ x31(注意:本章节课程仅涉及RV32I的通用寄存器组);
    • 在 RISC-V 中,Hart 在执行算术逻辑运算时所操作的数据必须直接来自寄存器。
  • 内存
    • Hart可以执行在寄存器和内存之间的数据读写操作;
    • 读写操作使用字节 (Byte) 为基本单位进行寻址;
    • RV32可以访问最多2^32个字节的内存空间。

编码格式

指令长度:32bit,本文讨论的都是 RV32 指令集

指令对齐:指令加载到内存是以 32bit 对齐

funct3funct7opcode一起决定指令类型,funct3表示占 3bit,funct7占 7bit。

opcode映射关系:

  • [1:0] 永远为 11
  • [4:2] 为下图横轴
  • [6:5] 为下图纵轴,三部分决定指令的类型。

BEQ指令为例opcode=1100011[4:2]=000[6:5]=11查表可得BEQ指令类型为BRANCH

The RISC-V Instruction Set Manual

小端序

  • 主机字节序 (HBO-Host Byte Order)
  • 一个多字节整数在计算机内存中存储的字节顺序称主机字节序 (HBO- Host Byte Order,或者叫本地字节序)
  • 不同类型 CPU 的 HBO 不同,这与 CPU 的设计有关。分为大端序 (Big-Endian) 和小端序 (Little-Endian)

指令分类

rd(register destination)目标寄存器,rs(register source)源寄存器,大小都是 5bit,因为可以表示2^5=32寄存器。

指令详解

算术运算指令

ADD

算数指令只包含加减,不包含乘除,乘除运算有专门的扩展。

数据传送顺序是由后向前,和正常的编码习惯类似。

SUB Substract

练习

现知道某条 RISC-V 的机器指令在内存中的值为b3 05 95 00,从左往右为从低地址到高地址,单位为字节,请将其翻译为对应的汇编指令。

  • 确定字节序
    在 RISC-V 中存放是小端序,根据题意真正指令应该是00 95 05 b3
  • 转换二进制
    机器码是二进制,所以需要将上述指令值转换为二进制,可得0000000 01001 01010 000 01011 0110011
  • 查阅手册
    查阅The RISC-V Instruction Set Manual Volume I: Unprivileged ISA找到RV32/64G Instruction Set Listings指令表格,低 7 位是opcode,查表可得0110011对应操作码有多个SLLI SRAI SUB等等,此时再看最高位00000000,可以确定是ADD指令
  • 将分割的二进制转成十进制
    0000000 9 10 000 11 010011->ADD x11 x10 x9

ADDI ADD Immediate

LUI Load Upper Immediate

LI

AUIPC

经常用于构造一个相对地址。

LA

基于算术运算指令实现的其他伪指令

x0寄存器具有特殊含义,往里写数据没有意义
NOP指令主要为了占位,空转

逻辑运算指令

NOT

Assembly
10101010 11111111(-1) -------- XOR 01010101

移位运算指令

算数移位

只有右移,没有左移。左移会把最高位覆盖。

Assembly
10001000 >> 2 = 11100001

内存读写指令

加载,内存读,将数据从内存读入寄存器

Store,内存写,将数据从寄存器写出到内存

为何对 word 的 加载 不区分无符号和有符号方式 (RV32)?RV32 下寄存器是 4 字节,加载 word 也是 4 字节,自然不需要扩展。

为何 store 不区分有符号还是无符号?因为从目的地址只有 1 字节,不管是写 1 字节,2 字节,还是 4 字节,都只用到最低的 1 字节。不需要考虑符号

立即数分两个地方存,为了解码效率

条件分支指令

指令格式中的立即数 (imm) 存放有些奇怪,第 [1-4] 位和第 [11] 位放在一起,第 [5-10] 位和第 [12] 位放在一起。这是为了迎合硬件处理效率,编程时不需要考虑立即数存储方式。

无条件跳转指令

int a = 1;
int b = 1;

void sum()
{
  a = a+b;
  return;   // jalr x0 0(x5)  当前指令的下一条指令存到x0中,并跳转到(0 + x5),也就是sum的下一条指令
}

void _start()
{
  sum(); // jal x5 sum  把sum的下一条指令存到x5,然后跳转到sum
  ...
}

如何解决长距离跳转?使用 AUIPC 来构建一个大数,配合 JALR 使用。如 auipc x6,imm-20 jalr x1,x6,imm-12

RISC-V 指令寻址模式总结

汇编函数调用约定

函数调用过程概述

栈(stack)数据结构,在函数调用过程中会用来保存变量,函数地址等等。

栈帧里保存的变量是自动变量,会被内存自动释放。

为何要有调用者与被调用者保存的概念

函数调用过程中就会有参数和返回值的传递,自己写的函数可能由别人来调用,如果没有约定好某个参数存放位置,就不能够顺利执行函数。

因为寄存器需要经常在编程中使用,所以 ABI 名就是寄存器的别名。

这些寄存器其实都可以设置成被调用者保存,也就是在被调用函数中保存一遍为啥还要分这么多
答:因为保存一遍效率低

尾调用实例

Assembly
# Calling Convention # Demo to create a leaf routine # # void _start() # { # // calling leaf routine # square(3); # } # # int square(int num) # { # return num * num; # } .text # Define beginning of text section .global _start # Define entry _start _start: la sp, stack_end # prepare stack for calling functions li a0, 3 # pass 3 to square call square # call square # the time return here, a0 should stores the result stop: j stop # Infinite loop to stop execution # int square(int num) square: # prologue addi sp, sp, -8 # reserve space for local variables sw s0, 0(sp) # save s0 sw s1, 4(sp) # save s1 # `mul a0, a0, a0` should be fine, # programing as below just to demo we can contine use the stack mv s0, a0 # s0 = a0 mul s1, s0, s0 # s1 = s0 * s0 mv a0, s1 # a0 = s1 # epilogue lw s0, 0(sp) # restore s0 lw s1, 4(sp) # restore s1 addi sp, sp, 8 # release space for local variables ret # return from function # add nop here just for demo in gdb nop # allocate stack space stack_start: .rept 10 # reserve 10 words for stack .word 0 # fill with 0 .endr # end of repeat stack_end: .end # End of file

非尾调用实例

Assembly
# Calling Convention # Demo how to write nested routines # # void _start() # { # // calling nested routine # aa_bb(3, 4); # } # # int aa_bb(int a, int b) # { # return square(a) + square(b); # } # # int square(int num) # { # return num * num; # } .text # Define beginning of text section .global _start # Define entry _start _start: la sp, stack_end # prepare stack for calling functions # aa_bb(3, 4); li a0, 3 # load argument a li a1, 4 # load argument b call aa_bb # call aa_bb stop: j stop # Infinite loop to stop execution # int aa_bb(int a, int b) # return a^2 + b^2 aa_bb: # prologue addi sp, sp, -16 # decrement stack pointer by 16 bytes sw s0, 0(sp) # save s0 sw s1, 4(sp) # save s1 sw s2, 8(sp) # save s2 sw ra, 12(sp) # save ra # cp and store the input params mv s0, a0 # copy a to s0 mv s1, a1 # copy b to s1 # sum will be stored in s2 and is initialized as zero li s2, 0 # initialize s2 to zero mv a0, s0 # copy s0 to a0 jal square # call square add s2, s2, a0 # add a0 to s2 mv a0, s1 # copy s1 to a0 jal square # call square add s2, s2, a0 # add a0 to s2 mv a0, s2 # copy s2 to a0 # epilogue lw s0, 0(sp) # restore s0 lw s1, 4(sp) # restore s1 lw s2, 8(sp) # restore s2 lw ra, 12(sp) # restore ra addi sp, sp, 16 # increment stack pointer by 16 bytes ret # return from aa_bb # int square(int num) square: # prologue addi sp, sp, -8 # decrement stack pointer by 8 bytes sw s0, 0(sp) # save s0 sw s1, 4(sp) # save s1 # `mul a0, a0, a0` should be fine, # programing as below just to demo we can contine use the stack mv s0, a0 # copy a to s0 mul s1, s0, s0 # s1 = a * a mv a0, s1 # copy s1 to a0 # epilogue lw s0, 0(sp) # restore s0 lw s1, 4(sp) # restore s1 addi sp, sp, 8 # increment stack pointer by 8 bytes ret # return from square # add nop here just for demo in gdb nop # allocate stack space stack_start: .rept 10 # allocate 10 words of stack space .word 0 # initialize stack space to 0 .endr # end of stack allocation stack_end: .end # End of file

汇编与 C 混合编程

前提

遵守 ABI(Abstract Binary Interface)的规定

  • 数据类型大小,布局,对齐
  • 函数调用约定
  • 系统调用约定
    等等

RISC-V 函数调用约定规定

  • 函数参数采用寄存器a0-a7
  • 函数返回值采用寄存器a0,a1

汇编嵌入 C 语言

Assembly
# ASM call C .text # Define beginning of text section .global _start # Define entry _start .global foo # foo is a C function defined in test.c _start: la sp, stack_end # prepare stack for calling functions # RISC-V uses a0 ~ a7 to transfer parameters li a0, 1 li a1, 2 call foo #调用了C语言函数 # RISC-V uses a0 & a1 to transfer return value # check value of a0 stop: j stop # Infinite loop to stop execution nop # just for demo effect stack_start: .rept 10 .word 0 .endr stack_end: .end # End of file

call foo就是在调用 C 语言函数,foo
.global foo告诉编译器foo函数定义在外面。

C 语言嵌入汇编

下图中为简化写法