汇编指令

文章目录

  • 汇编指令
    • 常见寄存器
    • 常见指令操作
      • 算术运算
      • 访存运算 (核心)
        • 先上表
        • 核心解释
        • 最后上代码
      • 逻辑运算
      • 移位操作
      • 分支指令(核心: 条件与循环)
        • 使用分支实现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. 左移补1会很奇怪,而左移每补一个0相当于乘以一次2,后者非常有用,前者违背直觉(违背逻辑)
  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
汇编循环的技巧:
  1. 理解c语言中代码的执行顺序,显然while 中的条件表达式可以看做是循环体的一部分,且是最开头的一部分
  2. 所有循环一律用死循环的格式处理,在循环体中加入分支语句就可以了
  3. 条件表达式取反处理

再看一个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汇编操作相关推荐

  1. RISC V (RV32+RV64) 架构 整体介绍

    文章目录 riscv 市场 芯片介绍 软件介绍 开发板介绍 PC介绍 riscv 架构 编程模型(指令集/寄存器/ABI/SBI) 运行状态 指令集 寄存器 riscv32和riscv64两者的区别 ...

  2. 汇编语言基础--汇编操作指令概述

    本文是接续"汇编语言基础--机器级数据存储",主要介绍汇编指令的构造.寻址和指令主要分类. 操作指令 指令的基本要素:       在"计算机处理器(CPU)基础&quo ...

  3. 计组学习笔记2(RISC v版)

    指令集解释 (规定:R[r]表示通用寄存器r的内容,M[addr]表示存储单元addr的内容,SEXT[imm]表示对imm进行符号扩展,ZEXT[imm]表示对imm进行零扩展) 整数运算类 -U型 ...

  4. 一文学懂过滤器和监听器

    一文学懂过滤器和监听器

  5. pytorch自带网络_【方家之言】一篇长文学懂 pytorch

    作为目前越来越受欢迎的深度学习框架,pytorch 基本上成了新人进入深度学习领域最常用的框架.相比于 TensorFlow,pytorch 更易学,更快上手,也可以更容易的实现自己想要的 demo. ...

  6. 一文搞懂C语言文件操作

    一.文件分类 根据数据的组织形式,可以将文件分为文本文件和二进制文件.通俗讲,文本文件就是你能看懂的,而二进制文件是你看不懂的! 二.文件打开与关闭 假设已经定义了一个文件指针 FILE *fp; 有 ...

  7. 一文读懂 Traefik v 2.6 企业版新特性

    Traefik Enterprise 是一种统一的云原生网络解决方案,将 API 管理.入口控制和服务网格整合到一个简单的控制平面中.Traefik Enterprise 为整个组织的开发人员和运营团 ...

  8. 一座小桥(最多只能承重两个人)横跨南北两岸,任意时刻同一方向只允许一个人过桥,南侧桥段和北侧桥段较窄只能通过一个人,桥中央一处宽敞,允许两个人通过或歇息。试用P(wait)V(signal)操作实现

    设置一个num同步信号量,用于控制桥上的人数,初始值为2,表示桥上最多有2个人 南侧s.北侧n桥各设置一个互斥信号量,初始值为1,表示只允许一个人通过南侧桥或北侧桥 同步信号量起"通知&qu ...

  9. 一文学懂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 ...

  10. 一文学懂Java泛型,详细而全面,值得收藏~

    零基础Java学习之泛型 概述 泛型类 语法格式 泛型类的使用 代码示例 泛型接口 语法格式 泛型接口的使用 代码示例 类型变量的上限和下限 类型变量的上限 语法格式 代码示例 类型变量的下限 语法格 ...

最新文章

  1. PyTorch核心开发者灵魂发问:我们怎么越来越像Julia了?
  2. 令AI费解的图像层出不穷 计算机视觉远未达到完美
  3. metrics-server最新版本有坑,慎用
  4. java实现两个最大整数相加_JAVA-实现两个超大整数相加
  5. 【工具】更新最新esp8266库离线安装包3.0.1、ESP32库离线安装包1.0.6
  6. 树莓派:关于linux内核
  7. ajax异步日历,用AJAX自定义日历(示例代码)
  8. 入门级前端 简单的网页书写
  9. hdu1355The Peanuts
  10. LeetCode 1207. 独一无二的出现次数
  11. linux推箱子脚本,【编程例题】标准C语言实现推箱子游戏!附解析!
  12. mybatis系列-06-输入映射
  13. Error:No cached version of com.android.tools.build:gradle:2.0.0 available for offline mode
  14. oracle group by
  15. Android Studio中R无法找到res/raw文件夹
  16. BZOJ2716[Violet] 天使玩偶/SJY摆棋子
  17. java 快逸报表_数据填报 | 快逸报表工具-Java报表软件
  18. 三菱plc指令dediv_三菱plc指令tcmp的用法
  19. VS2003远程调试
  20. 微信聊天记录里的文件又失效了?试试这个文件同步开源项目吧

热门文章

  1. mac安装搜狗输入法
  2. centos7,php7.3使用pecl安装swoole,新手教学
  3. java.lang.NoClassDefFoundError:org.ksoap2.seri...
  4. 抽样与抽样分布——中心极限定理、点估计
  5. 台式计算机睡眠快捷键,电脑如何设置快捷方式迅速进入睡眠的状态?
  6. r语言 svycoxph_生存分析的Cox回归模型(比例风险模型)R语言实现及结果解读
  7. 【React自制全家桶】九、Redux入手
  8. java 人脸活体检测_人脸识别活体检测测试案例
  9. java代码对图片缩放
  10. java时间日期相减得到天数_java日期相减得到天数