一文学懂risc-v汇编操作
汇编指令
文章目录
- 汇编指令
- 常见寄存器
- 常见指令操作
- 算术运算
- 访存运算 (核心)
- 先上表
- 核心解释
- 最后上代码
- 逻辑运算
- 移位操作
- 分支指令(核心: 条件与循环)
- 使用分支实现if语句
- 汇编if的技巧
- 使用分支实现循环语句
- 汇编循环的技巧:
- 跳转指令 (核心:函数的调用)
- 使用汇编写完整的程序
- 伪指令
- 数组
- 栈指针
- 保存寄存器与临时寄存器
- 参数寄存器和返回值寄存器
- 案例
- 下一步:设计一个risc-v 的cpu
常见寄存器
risc-v 有32个通用寄存器(简写 reg),标号为x0 - x31
寄存器名 | 别名 | 功能 | 备注 |
---|---|---|---|
x0 | 硬连线接0 | 任何对x0的写入操作都无效,x0的值恒定为0 | |
x1 | ra | 返回地址 | return address |
x2 | sp | 栈指针 | 栈指针始终指向栈顶 |
x3 | gp | 全局指针 | 一般用不到,指向全局变量和静态变量 |
x4 | 略 | 线程指针 | 略 |
x5-x7,x28-x31 | t0-t6 | 临时寄存器 | temporary的首字母 t , 共有7个 |
x8-x9, x18-x27 | s0-s11 | 保存寄存器 | saved 的首字母 s, 共有11个,其中s0与s1是返回值reg |
x10-x17 | a0-a7 | 参数寄存器 | arg 的首字母a , 共有8个 |
x8 | fp | 帧指针 | 使用时需要用栈指针初始化,默认情况下不使用 |
对于有别名的寄存器,优先使用别名,更达意,更好记
先认识即可,后续会逐渐认识它们的作用。为啥寄存器只有32个?寄存器的运算和读取速度是最快的,太少了显然不好,但事实证明,寄存器数目如果太多了会导致访问寄存器的速度下降,也会造成速度下降。32个寄存器是risc-v设计者、实践者的选择。
上述寄存器都是64位的,也就是说上述寄存器可以存放64bits的数据
常见指令操作
在risc-v 中绝大多数指令的目的寄存器是第一个寄存器,第二第三个寄存器是源寄存器
算术运算
指令 | 实例 | 含义 | 备注 |
---|---|---|---|
add | add t0, t1, t2 | t0 = t1 + t2 | 将t1+t2的值赋值给t0 |
sub | sub t1, t2, t3 | t1 = t2 + t3 | 将t1-t2的值赋值给t0 |
addi | addi t0, t1, -2 | t0 = t1 + (-2) | 将t1-2的值赋值给t0 |
上述指令中第一个寄存器,是被赋值、被操作的对象,我们称之为目的寄存器(target reg), 而后面的寄存器则称之为源寄存器,并且相应的第二个寄存器一定是第一个操作数,第三个寄存器一定是第二个操作数,顺序不可以改变。
没有subi指令,因为addi可以加负数,从而精简指令的个数。
为啥会有addi指令?
在汇编层面有三个主要的操作对象:寄存器,内存,立即数,它们是完全不同,不可以混淆,组织结构也不一样的不同对象,所以不能单纯拿针对寄存器的指令去处理内存和立即数。
立即数 immediate ,取首字母i。后续从寄存器扩展到立即数的指令都是这样的,在指令的最后加上i。
思考:如何让一个t0寄存器的值变为我们想要的一个任意常数(在计算机指令可表示范围内的数),比如说100?
addi t0, x0, 100 # now t0 = 100,在risc-v汇编中 ‘#’表示单行注释
当然这种表示方法太繁琐了,不过是每个新手的必经之路,刚开始都必须这样进行思考,后面将有简化的方法。
访存运算 (核心)
什么是访存:“访问内存中对应的值” 和 “将值存到内存中”,请记住在汇编中只有三个对象,内存,立即数,以及寄存器,寄存器只有32个数量有限,立即数无法进行存储功能,因此很多时候都需要把数据放到内存中以及从内存中读取相应数据。
运算与存储是计算机的两大核心而基本的功能。
几个将有帮助记忆的英语:
load : 加载 store: 存储
double word :双字,对应64位(1bits,位就是一比特)
word : 字,对应32位
half word : 16位
byte:8位(一个字节8比特)
unsigned: 无符号的
先上表
指令 | 实例 | 含义 | 备注 |
---|---|---|---|
ld | ld t0, 0(t1) | t0 = memory[t1 + 0] |
将t1的值加上0,将这个值作为地址, 取出这个地址所对应的内存中的值, 将这个值赋值给t0 |
lw | lw t2, 20(t3) | t2 = memory[20 + t3] |
lw 与 ld 的区别就在于 ld 是从内存取出64位数值, 而lw是取出32位数值。 |
lh | lh t4, 30(t5) | t4 = memory[30 + t5] | 从内存中取出16位数值 |
lb | lb t4, 30(t5) | t4 = memory[30 + t5] | 从内存中取出8位数值 |
sd | sd t0, 0(t1) | memory[0+t1] = t0 |
将t1的值加上0,将这个值作为地址, 将t0的值存储到上述地址所对应的内存中去 |
sw | sw t0, 0(t1) | memory[0+t1] = t0 |
与sd的区别在于sw只会将t0的低32位 数值存储到相应的内存。sd会将t0的64位都存入 |
sh | sh t0, 0(t1) | memory[0+t1] = t0 |
只将t0的低16位所对应的数值存入, 也就是一个half word大小 |
sb | sb t0, 0(t1) | memory[0+t1] = t0 | 只存入8位,一个byte大小 |
lwu | lwu t2, 20(t3) | t2 = memory[20 + t3] |
lw 与lwu的区别在于, 前者取出32位数值作符号扩展到64位, 而后者做无符号扩展到64位 |
lhu | lhu t4, 30(t5) | t4 = memory[30 + t5] | 略 |
lbu | lbu t4, 30(t5) | t4 = memory[30 + t5] | 略 |
核心解释
tip1:
所有指令都是前面那些英文的开头首字母的组合,何为汇编语言本质上就是一种机器语言的助记符,
自然越简单越好。
tip2:
操作系统有32位操作系统,64位操作系统,这个位指的是什么?
指的是地址的位数。
啥是地址?啥是内存?啥是值?
一个简单而有效的解释是:一块内存你可以想象是一个盒子,盒子里面的东西是值,而每个盒子都会有一个独一无二的编号,这个编号对应的就是内存的地址。我们并不关心内存地址,我们只关心内存的值,但为了能获得内存中的值,我们必须先知道内存的地址才能找到这个内存从而读取内存中的值。
既然每一个地址都有一个内存地址。从0开始,0,1,10,11,100,101…随着地址的增大,位数也越来越多,64位操作系统指的就是内存地址的位数总共有64位,所以0就是64个0,1就是高63位为0低1位为1,依次类推,所以你知道64位操作系统最多有多少地址空间了吗?
所有操作系统都是按字节寻址的,也就是一个内存地址是与内存中特定的1byte大小的盒子挂钩的。这1byte大小的盒子就可以用来放值。如果是1个double类型的变量,需要8个字节,也就是由内存中8个连续的盒子存储它的值,通常情况下,最低字节的内存地址,最小的那个地址,就是这个double变量的地址。
tip3:
为啥load操作需要符号扩展或者无符号扩展?它们是什么意思?
lw t0, 0(t1) : 将t1的值当作内存地址找到对应的内存,取出32位内存中(4个连续的字节地址)的值,赋值给t0,但是,因为t0是64位寄存器,t0的高32位怎么办?所以需要扩展到64位再赋值给t0。
符号扩展:高位全部扩展为符号位
无符号扩展:高位全部扩展为0
有符号数进行符号扩展能保证数值不会发生变化,无符号数进行无符号扩展数值不会发生变化(后面章节会简单证明,非常简单的,可以自己证明以下,现在记住就可以了)。
tip4:
为啥没有ldu指令,为啥没有store的无符号版本
ld 从内存中取出64位数值,寄存器就是64位的,还扩展啥?这时有符号数与无符号数的处理没有任何区别。对于早期的risc-v,寄存器是32位的,则ld和lw指令都没有无符号版本,因为无需扩展便指令的行为都是一样的,就无需区分有符号数和无符号数了。(其实无符号数出现的概率很小,实在不行先放着,以后再看就懂了。doge)
store将数据按位复制到内存中去,无需扩展。
最后上代码
int a1 = 2; // a1可以在内存中,也可以在寄存器中,但是后面有取址操作,所以a1只能在内存中
int* a1p = &a1; // a1p 存放了a1的地址,也就是a1p的值是a1的地址,假设a1p在t0寄存器中
int a2 = *a1p; // lw t1, 0(t0) ,其中t1寄存器是a2
*a1p = 8; // sw 8, 0(t0)unsigned int a3 = 2;
unsigned int * a3p = &a3;
int a4 = *a3p; // lwu t1,0(t0)
*a3p = 8; // sw 8, 0(t0)
逻辑运算
指令 | 实例 | 含义 | 备注 |
---|---|---|---|
and | and t1, t2, t3 | t1 = t2 & t3 | 按位与,不是&& |
or | or t1, t2, t3 | t1 = t2 | t3 | 按位或 |
xor | xor t1, t2, t3 | t1 = t2 ^ t3 | 按位异或 |
andi | andi t1, t2, 4 | t1 = t2 & 4 | 1&1 = 1 ,1&0 = 0, 0&0 = 0 |
ori | ori t1, t2, 4 | t1 = t2 | 4 | 只有0|0=0,其他结果为1 |
xori | xori t1, t2, 4 | t1 = t2 ^ 4 | a^0 = a, a^1 = ~a,xor 0不变,xor 1切换 |
为啥没有not 取反操作?
a xor -1 = ~a,这里的a表示任意位数的数字 。因为 -1的二进制表示是全一(位数可变,如 11, 111, 1111有符号数都表示-1)。
移位操作
指令 | 实例 | 含义 | 备注 |
---|---|---|---|
sll | sll t1, t2, t3 | t1 = t2 << t3 | t2左移t3位后赋值给t1 |
srl | srl t1, t2, t3 | t1 = t2 >> t3 | t2右移t3位,做无符号扩展后赋值给t1 |
sra | sra t1, t2, t3 | t1 = t2 >> t3 | t2右移t3位,做符号扩展后赋值给t1 |
slli | slli t1, t2, 4 | t1 = t2 << 4 | t2左移4位后赋值给t1 |
srli | srli t1, t2, 4 | t1 = t2 >> 4 | t2右移4位,做无符号扩展后赋值给t1 |
srai | srai t1, t2, 4 | t1 = t2 >> 4 | t2右移4位,做符号扩展后赋值给t1 |
首字母:
s:shift 移动
r: right 右 l:left 左
l: logical 逻辑的(无符号扩展,补零) a: arithmetic (这英文单词着实有难度)算术的(符号扩展,补符号位)
i:immediate立即数
为何左移只能逻辑扩展,右移有逻辑和算术两种,分别对应无符号和有符号扩展?
- 左移补1会很奇怪,而左移每补一个0相当于乘以一次2,后者非常有用,前者违背直觉(违背逻辑)
- 右移补位,如果是无符号数应该补0,有符号数应该补符号位,原理和上面差不多。
最后,或许现在可能很纠结数字的表示,但当在后面章节系统学习了计算机数字的表示以后,这些东西就会变得简单而富有智慧。现在记住:无符号数扩展补0, 有符号数补符号位。左移始终补0
分支指令(核心: 条件与循环)
按照惯例,先给出一些英文单词:
equal :相等 not: 不
less than: 小于 greater than : 大于
branch:分支
指令 | 实例 | 含义 | 备注 |
---|---|---|---|
beq | beq a1, a2, Lable | if(a1 == a2){goto Lable;} | Lable是任意自定义的标签 |
bne | bne a1, a2, Lable | if(a1 != a2){goto Lable;} | |
blt | blt a1, a2, Lable | if(a1 < a2){goto Lable;} | |
bgt | bgt a1, a2, 100 | if(a1 > a2){goto Lable;} | 100表示跳到 pc+100 * 2的位置 |
bge | bge a1, a2, 100 | if(a1 <= a2){goto Lable;} |
100与Label对应着相同的指令, 实际上在运行时Label会变成pc+xxx |
ble | ble a1, a2, 100 | if(a1 >= a2){goto Lable;} | 我Label好像拼错了,能看懂就行 doge |
实际上真正的risc-v指令只有blt 和 bge 而没有 bgt 和ble指令,后者是伪指令(相当于c语言的宏定义,对于程序员来说和risc-v指令没区别),ble操作可以通过将bge指令的两个操作数互相换位置实现,bgt操作同理。berkeley的人真是大神。
如何自定义标签
Loop: #只要加上一个冒号就是标签
addi t1, x0, 4
addi t2, x0, 4
beq t1, t2, Loop # 显然这是一个死循环 doge
使用分支实现if语句
if (a > b) // 假设a 在t0, b 在t1寄存器中
{a = 1;
}
else
{b = 1;
}
对应的汇编语句是
ble t0, t1, Else
addi t0, x0, 1
beq x0, x0, Then
Else:
addi t1, x0, 1
Then:
汇编if的技巧
一个非常非常有用的经验是,对于if语句中的条件表达式,我们往往在汇编中判断其反面条件,如果上面的例子没有else,则更能体现出其优点。其他条件表达式同理。
使用分支实现循环语句
int a = 0; //假设 a 在 t0
int i = 100;// 假设 i 在t1
while (i > 10)
{a += i; i++;
}
对应的汇编语句是
add t0, x0, x0
addi t1, x0, 100
addi t2, x0, 10
Loop:ble t1, t2, Endadd t0, t0, t1addi t1, t1, 1beq x0, x0, Loop
End:# This is outer of loop
汇编循环的技巧:
- 理解c语言中代码的执行顺序,显然while 中的条件表达式可以看做是循环体的一部分,且是最开头的一部分
- 所有循环一律用死循环的格式处理,在循环体中加入分支语句就可以了
- 条件表达式取反处理
再看一个for 循环
int sum = 0; // sum at t0 reg
for (int i = 0; i < 20; i++) // i at t1 reg
{sum += i;
}
汇编是
add t0, x0, x0
add t1, x0, x0
add t2, x0, 20
Loop:bge t1, t2, Endadd t1, t1, t0addi t0, t0, 1beq x0, x0, Loop
End:
将条件表达式和更新表达式当作是循环体的开头与结尾即可。别忘了更新表达式
一文学懂risc-v汇编操作相关推荐
- RISC V (RV32+RV64) 架构 整体介绍
文章目录 riscv 市场 芯片介绍 软件介绍 开发板介绍 PC介绍 riscv 架构 编程模型(指令集/寄存器/ABI/SBI) 运行状态 指令集 寄存器 riscv32和riscv64两者的区别 ...
- 汇编语言基础--汇编操作指令概述
本文是接续"汇编语言基础--机器级数据存储",主要介绍汇编指令的构造.寻址和指令主要分类. 操作指令 指令的基本要素: 在"计算机处理器(CPU)基础&quo ...
- 计组学习笔记2(RISC v版)
指令集解释 (规定:R[r]表示通用寄存器r的内容,M[addr]表示存储单元addr的内容,SEXT[imm]表示对imm进行符号扩展,ZEXT[imm]表示对imm进行零扩展) 整数运算类 -U型 ...
- 一文学懂过滤器和监听器
一文学懂过滤器和监听器
- pytorch自带网络_【方家之言】一篇长文学懂 pytorch
作为目前越来越受欢迎的深度学习框架,pytorch 基本上成了新人进入深度学习领域最常用的框架.相比于 TensorFlow,pytorch 更易学,更快上手,也可以更容易的实现自己想要的 demo. ...
- 一文搞懂C语言文件操作
一.文件分类 根据数据的组织形式,可以将文件分为文本文件和二进制文件.通俗讲,文本文件就是你能看懂的,而二进制文件是你看不懂的! 二.文件打开与关闭 假设已经定义了一个文件指针 FILE *fp; 有 ...
- 一文读懂 Traefik v 2.6 企业版新特性
Traefik Enterprise 是一种统一的云原生网络解决方案,将 API 管理.入口控制和服务网格整合到一个简单的控制平面中.Traefik Enterprise 为整个组织的开发人员和运营团 ...
- 一座小桥(最多只能承重两个人)横跨南北两岸,任意时刻同一方向只允许一个人过桥,南侧桥段和北侧桥段较窄只能通过一个人,桥中央一处宽敞,允许两个人通过或歇息。试用P(wait)V(signal)操作实现
设置一个num同步信号量,用于控制桥上的人数,初始值为2,表示桥上最多有2个人 南侧s.北侧n桥各设置一个互斥信号量,初始值为1,表示只允许一个人通过南侧桥或北侧桥 同步信号量起"通知&qu ...
- 一文学懂ansible
目录 1. 文件模块 1.1 fetch 1.2 copy 1.3 file 1.4 blockinfile 1.5 lineinfile 1.6 find 1.7 replace 2. 命令模块 2 ...
- 一文学懂Java泛型,详细而全面,值得收藏~
零基础Java学习之泛型 概述 泛型类 语法格式 泛型类的使用 代码示例 泛型接口 语法格式 泛型接口的使用 代码示例 类型变量的上限和下限 类型变量的上限 语法格式 代码示例 类型变量的下限 语法格 ...
最新文章
- PyTorch核心开发者灵魂发问:我们怎么越来越像Julia了?
- 令AI费解的图像层出不穷 计算机视觉远未达到完美
- metrics-server最新版本有坑,慎用
- java实现两个最大整数相加_JAVA-实现两个超大整数相加
- 【工具】更新最新esp8266库离线安装包3.0.1、ESP32库离线安装包1.0.6
- 树莓派:关于linux内核
- ajax异步日历,用AJAX自定义日历(示例代码)
- 入门级前端 简单的网页书写
- hdu1355The Peanuts
- LeetCode 1207. 独一无二的出现次数
- linux推箱子脚本,【编程例题】标准C语言实现推箱子游戏!附解析!
- mybatis系列-06-输入映射
- Error:No cached version of com.android.tools.build:gradle:2.0.0 available for offline mode
- oracle group by
- Android Studio中R无法找到res/raw文件夹
- BZOJ2716[Violet] 天使玩偶/SJY摆棋子
- java 快逸报表_数据填报 | 快逸报表工具-Java报表软件
- 三菱plc指令dediv_三菱plc指令tcmp的用法
- VS2003远程调试
- 微信聊天记录里的文件又失效了?试试这个文件同步开源项目吧
热门文章
- mac安装搜狗输入法
- centos7,php7.3使用pecl安装swoole,新手教学
- java.lang.NoClassDefFoundError:org.ksoap2.seri...
- 抽样与抽样分布——中心极限定理、点估计
- 台式计算机睡眠快捷键,电脑如何设置快捷方式迅速进入睡眠的状态?
- r语言 svycoxph_生存分析的Cox回归模型(比例风险模型)R语言实现及结果解读
- 【React自制全家桶】九、Redux入手
- java 人脸活体检测_人脸识别活体检测测试案例
- java代码对图片缩放
- java时间日期相减得到天数_java日期相减得到天数