1.汇编语句组成

- label:标签
- operation:真正的语句
- comment:
注释

_first:
# First RISC-V Assemble Sample
.macro do_nothing # directive (指示/伪操作:以.开头,通知汇编器如何控制代码的产生,不产生实际的指令)
nop # pseudo-instruction
nop # pseudo-instruction
.endm # directive [.macro 到 .endm 之间是定义宏]
.text # directive [.text指示把这里的写到汇编的.text节]
.global _start # directive [.global类似于标识全局变量,让外部可见]
_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 (告诉汇编器程序在这结束,.end后的指令不会执行)
2.RISC-V 汇编指令总览



3.RISC-V 指令详解
1.算术运算指令
ADD加法

SUB减法
ADDI 加立即数


符号扩展,指的是:原来立即数的12位,最高位是1的话,扩展成32位时,前面的全部为1,这样就保证了数值相同

addi的立即数只有12位,如果需要给寄存器赋值给大的数怎么办?
- 如上..
这个设置高20位的命令是LUI
,如下介绍
LUI(给寄存器的低12位前的高位赋值)(rd=立即数<<12)

这个命令让数左移12位,所以低12位是0.
看一个低12位是正数的情况

这个简单,看一个低12位是负数的情况


所以可以看到,给寄存器赋值还是一个略微复杂的操作,所以提供了一个易用的li
命令
LI(给寄存器赋值)

相关伪指令

AUIPC(rd=立即数<<12+PC)
Add Upper Immediate Programing Counter

经常用于构造相对地址.
LA(LoadAddress)

LA:
是用一个地址赋值给寄存器,通常用auipc和其他指令结合,相对寻址去得到一个地址
可以看到,通常la是用一个lable去赋值给寄存器

- 执行完第一个语句后,t0=0x80000000,也就是这个lable的地址,即当前时刻PC的值
- 第二个语句等于没执行一样
- 第三个指令是跳转到t0=0x80000000的绝对地址,也就是回到第一个语句去执行...

我把上面的第一个语句的_start变成stop,看看是啥情况:

可以看到,这里的la x5,stop
变成了两个语句
auipc t0,0x0
,是将t0 = PCaddi t0,t0,12
,是将t0 += 12,12是相对地址
所以,la x5,stop ,是将stop地址用一种相对寻址的方式得到一个绝对地址
总结
2.逻辑运算指令
RISC-V中提供了AND,OR,XOR,没有提供NOT
NOT是一个伪指令:是RD = RS XOR 0b111111111111.... ,这样就是把0的变成1,把1的变成0
3.移位运算指令
逻辑移位(用0补齐)(SLL,SRL,SLLI,SRLI)
算术移位(没有算术右移,只有算术左移,右移时用符号位补齐)(SRA)(SRAI)
比如1000-1000右移1位 ->> 1100-0100
所以可见,负数算术右移动时,负数位的值变成原来的一半,正数位的值变成原来的一半
4.内存读写指令
Load和Store是站在CPU的角度,所以读是读到寄存器,写是从寄存器写到内存.
内存读到寄存器(LB,LBU,LH,LHU,LW)
Word是4字节
-
LB:Byte一个字节 (有符号->所以是符号扩展)
-
LBU:Byte一个字节(无符号->所以是0扩展)
-
LH:Half Word 两个字节
-
LHU:Half Word 两个字节(无符号)
-
LW:Word 四个字节
为什么四字节的没有一个LWU?因为把一个寄存器直接塞满了,不用扩展了...
寄存器写到内存(SB,SH,SW)
- SB:Byte 1字节
- SH:Half Word 2字节
- SW:Word 4字节
store不区分有无符号,是因为CPU直接把寄存器的这个低位直接赋值给内存
5.条件分支指令(BEQ等等)
这个图片写的很清楚了.
立即数*2的原因是,由于地址要对齐,2字节对齐,所以地址的最低一位永远是0,这样就浪费了一个比特,所以 *2
6.无条件跳转指令(JAL,JALR,J,JR)
JAL实际上是做了两件事
- 将当前指令的下一条指令的地址写入RD (作为返回地址)
- 跳转到lable的地址去执行
通常是调用子函数的时候,跳转到子函数,并把当前下一条指令作为返回地址写入一个寄存器,子函数return的时候就跳转到这个返回地址去执行即可。
int a = 1;
int b = 1;
void sum()
{
a = a + b;
}
void _start()
{
sum();
}
这段代码还是很有趣的,_start中,用jal
将返回地址保存到x5,然后跳转到sum去执行。 _sum中,完成a=a+b的任务后,用jalr
跳转到x5寄存器存着的地址中

x0寄存器读和写永远都保持0,所以可以做这样的伪指令
7.寻址模式总结
4.汇编函数调用约定
4.1 函数调用过程概述
4.2 汇编编程时为何需要制定函数调用约定
Caller
:调用者Callee
:被调用者
同一个人写的汇编,或者编译过来的汇编,那是没有问题的,就可以指定某个寄存器存返回地址,指定一些确定的东西来存参数,这样就可以成功调用。
但是,很多情况下,不同的函数是不同的人写的,或者是不同的.c文件单独编译的,这样就一定需要一些公共的约定去遵守!
4.3 函数调用过程中有关寄存器的编程约定

4.3.1 有关寄存器的编程约定
- x1 =
ra
(return address) , 存放函数的返回地址
,调用者保存这个 - x2 =
sp
(stack pointer),存放栈指针
,被调用者(子函数)保存这个 . - x5
x7,x28x31 =t0~t6
,t表示temporary临时,存放临时寄存器
,调用者保存,之所以叫临时寄存器,就是因为他是调用者保存的,所以被调用者(子函数)可以随便改这些寄存器,而调用者如果要用到这些寄存器,就需要先保存到栈中,再调用子函数,再pop - x8,x9,x18~x27 =
s0~s11
,s表示saved保存,存放保存寄存器
,被调用者保存。所以,如果子函数会修改这些寄存器,那么需要开始的时候先入栈,结束的时候pop.
小小的总结:临时寄存器和保存寄存器的
临时和保存
是针对子函数
来说的,看子函数是不用维护,还是要保存.
- x10,x11 =
a0,a1
,a表示argument,存放参数寄存器
,用于在函数调用过程中保存第一个和第 二个参数,以及在函数返回时传递返回值。 [调用者保存] - x12~x17 =
a2~a7
,a表示argument,存放参数寄存器
,如果函数调用时需要传递更多的参数, 则可以用这些寄存器,但注意用于传递参数的寄存器最多只有 8 个 (a0 ~ a7),如果还有更多的参数则要利用栈。
![]()
x3
-gp
-global pointer
x4
-tp
-thread pointer
s0
-fp
-frame pointer
4.3.2 函数跳转和返回指令的编程约定
- 要存返回地址的时候,用
x1 (ra)
存返回地址 ret
:jalr x0,0(x1)
,即跳转到x1的地址中去执行
4.3.3 实现被调用函数的编程约定
-
函数执行体前:
- 减少sp的值,即开辟了一个栈帧
- 将saved寄存器(callee保存)的值先压到栈中
- 由于
ra
是caller保存的,所以如果还会调用其他的函数,那么需要将ra寄存器的值保存到栈中
-
函数执行体
-
函数执行体后:
- 从栈中恢复saved寄存器
- 如果中间调用过别的函数,那么就说明事先ra保存再栈中了,那么就要将ra给pop
- 增加sp的值,恢复到本函数之前的状态
- 调用ret返回
例子1:cc_leaf (尾调用):
void _start()
{
square(3);
}
int square(int num)
{
return num * num
}
可以看到,尾调用的特点是,子函数中不会保存RA了.
例子2:cc_nested
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;
}
汇编:
看vmware-asm
5.汇编与C混合编程
5.1 汇编调用C函数

test.c
/*
* a0 --> a
* a1 --> b
* c <-- a0
*/
int foo(int a, int b)
{
int c = a + b;
return c;
}
test.s
# 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
# .global foo 相当于c的extern的作用,告诉他外面定义了foo了
_start:
la sp, stack_end # prepare stack for calling functions
# RISC-V uses a0 ~ a7 to transfer parameters
li a0, 1 # 参数1 = 1
li a1, 2 # 参数2 = 2
call foo # 调用foo函数
# 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
5.2 C函数中嵌入汇编
- asm 后可以跟volatile也可以不跟
[如果跟:不允许编译器优化]
int foo(int a,int b)
{
int c;
asm volatile(
"add %0,%1,%2" //汇编指令
:"=r"(c) //输出操作数说明
:"r"(a),"r"(b) //输入操作数说明
)
return c
}
-
r
:表示与寄存器关联起来 -
m
(这里没有用这个): 表示与栈内存关联起来 -
0,1,2:是按照写的顺序去指代,先是c,再a,再b
实际具体选什么寄存器,往往是留给编译器自己去选择,我们需要告诉他把c和add得到的寄存器关联起来,把a和b与相加的两个寄存器关联起来.