汇编语法介绍
一条典型的 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 在执行算术逻辑运算时所操作的数据必须直接来自寄存器。
- 32个通用寄存器,
- 内存
- Hart可以执行在寄存器和内存之间的数据读写操作;
- 读写操作使用字节 (Byte) 为基本单位进行寻址;
- RV32可以访问最多
2^32
个字节的内存空间。
编码格式
指令长度:32bit,本文讨论的都是 RV32 指令集
指令对齐:指令加载到内存是以 32bit 对齐
funct3
、funct7
和opcode
一起决定指令类型,funct3
表示占 3bit,funct7
占 7bit。
opcode
映射关系:
- [1:0] 永远为 11
- [4:2] 为下图横轴
- [6:5] 为下图纵轴,三部分决定指令的类型。
以BEQ
指令为例opcode=1100011
。[4:2]=000
,[6:5]=11
查表可得BEQ
指令类型为BRANCH
。
小端序
- 主机字节序 (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
10101010
11111111(-1)
-------- XOR
01010101
移位运算指令
算数移位
只有右移,没有左移。左移会把最高位覆盖。
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 名就是寄存器的别名。
这些寄存器其实都可以设置成被调用者保存,也就是在被调用函数中保存一遍为啥还要分这么多 答:因为保存一遍效率低
尾调用实例
# 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 语言嵌入汇编
下图中为简化写法