汇编语法介绍

一条典型的 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个字节的内存空间。

编码格式

Responsive Image

指令长度: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

Responsive Image

Responsive Image

The RISC-V Instruction Set Manual

小端序

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

Responsive Image

指令分类

Responsive Image

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

Responsive Image

指令详解

算术运算指令

ADD

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

Responsive Image

Responsive Image

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

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

Responsive Image

Responsive Image

LUI Load Upper Immediate

Responsive Image

Responsive Image

Responsive Image

Responsive Image

LI

Responsive Image

AUIPC

Responsive Image

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

LA

Responsive Image

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

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

Responsive Image

逻辑运算指令

Responsive Image

NOT

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

移位运算指令

Responsive Image

算数移位

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

Responsive Image

10001000 >> 2
= 11100001

内存读写指令

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

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

Responsive Image

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

Responsive Image

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

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

条件分支指令

Responsive Image

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

Responsive Image

无条件跳转指令

Responsive Image

Responsive Image

Responsive Image

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

Responsive Image

RISC-V 指令寻址模式总结

Responsive Image

汇编函数调用约定

函数调用过程概述

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

Responsive Image

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

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

Responsive Image

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

Responsive Image

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

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

Responsive Image

Responsive Image

Responsive Image

尾调用实例

# 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

非尾调用实例

# 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 语言

# 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 语言嵌入汇编

下图中为简化写法

Responsive Image

Responsive Image