本文主要是介绍RISCV汇编讲解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
第一章 引言
为什么要讲riscv?
riscv的特点:
-诞生于顶尖学术机构:诞生于加州大学伯克利分校的体系结构研究院。吸引了大批的顶尖企业参与(e.g. 谷歌、华为、高通、阿里巴巴为rsicv的发展提供了大量的资金支持和贡献了技术和人才)
-精简指令集:指令相对精简,指令格式规整,易于实现和理解。只包含了最基本和常用的指令,避免了复杂和特殊用途的指令,使得处理器的硬件设计更加简洁高效。比如说长度固定(32位/64位)。
-模块化设计:可以根据不同的应用需求选择不同的模块进行组合。这使得riscv可以适应从嵌入式系统到高性能服务器等不同的应用场景,具有很高的灵活性。
-开源:例如有多个开源的riscv编译器(如gcc和llvm)可供选择。
-低功耗:指令集设计间接,处理器的硬件实现相对简单,有助于降低功耗。
命名规范
RV[###][abc......xyz]
[###]表示的是寄存器的位宽,后面的字母标识处理器支持的指令集模块集合。
例如:RV32MA、RV64GC。
模块化
模块化ISA:由1个基本整数指令集+多个可选的指令集组成。基础指令是固定的,永远不会改变。设计cpu的可以像插件一样去堆模块。
RISC-V ISA = 1个基本整数指令集 + 多个可选的扩展指令集
基本整数指令集:唯一强制要求实现的基础指令集,其他指令集都是可选的扩展模块。
扩展模块指令集:RISC-V允许在视线中以可选的形式实现其他标准化和非标准化的指令集扩展。特定组合“IMAFD”被称为“通用”组合,用英文字母G表示。
扩展指令集 | 描述 |
M | 整式乘法与除法指令集 |
A | 存储器原子指令集 |
F | 单精度浮点指令集 |
D | 双精度浮点指令集 |
C | 压缩指令集 |
...... | 其它标准化和非标准化指令集 |
HART
hart = hardware thread(个人理解为超线程)
一个hart就是一个虚拟的cpu。一个hart可以不受外界干扰的自主地去获取和执行risc-v指令。
特权级别
level | encoding | name | |
0 | 00 | user/application | U |
1 | 01 | supervisor | S |
2 | 10 | reserved | |
3 | 11 | machine | M |
当运行在用户态时,就是说cpu运行在user级别,进入到内核的时候就是supervisor级别。固件就是就是machine级别。运行在machine模式是不开虚拟地址的,全部运行在物理地址。
risc-v芯片一上电首先是进入到machine模式,再进入到supervisor模式,此时也叫保护模式,虚拟地址打开。
进程的实现依赖于虚拟地址,虚拟地址的实现需要MMU硬件支持。
Control and Status Register(CSR)
不同的特权级别下时分布对应各自的一套CSR,用于控制和获取响应level下的处理器工作状态。
高级别的特权可以访问低级别的CSR。比如说machine级别可以访问user级别的csr。
rsicv定义了专门用于操作csr的指令。
如果是用户程序,不太需要跟csr打交道。
异常和中断
异常:主动触发,cpu给你一次改过自新的机会,去执行一段挽救程序;当执行到非法指令的时候,cpu会停掉此指令流跳到一个特殊的地址去执行一段特殊的程序(自己写的程序,想对异常做的处理),执行完之后回到之前的指令再次运行;比如说除0异常;
中断:被动触发;cpu停掉当前程序,跳转到执行中断处理程序,执行完返回到下一条指令去执行;比如说外设通知你发生一件什么事情,跑去执行别的指令,执行完再回到下条指令,就像中断没有发生过一样。
ELF介绍
ELF(Executable Format)是一种unix-like系统上的二进制文件格式标准。
ELF标准中定义的采用ELF格式的文件分为4类:
ELF文件类型 | 说明 | 实例 |
可重定位文件(relocatable file) | 内容包含了代码和数据,可以被链接成可执行文件或共享目标文件 | linux上的.o文件 |
可执行文件 | 可以直接执行的文件 | linux上的a.out文件 |
共享目标文件 | 内容包含了代码和数据,可以作为链接器的输入,在链接阶段和其他的relacatable file或者shared object file一起链接成新的object file;或者在运行阶段,作为动态链接器的输入,和可执行文件结合,作为进程的一部分来运行。 | linux上的.so文件 |
核心转储文件(core dump file) | 进行意外终止时,系统可以将该进程的部分内容和终止时的其他状态信息保存到该文件中以供调试分析。 | linux上的core文件 |
ELF文件格式:
ELF Header | |
Program Header Table | 运行视图 |
.text | 程序指令 |
.init | 做初始化的一些指令 |
.data | 数据:全局变量等 |
.bss | |
...... | |
Section Header Table | 从链接角度去描述了这个文件的内容 |
.text和.init等这些信息放置的时候都会section(节)对齐,但是在运行的时候会合在一起以节省空间,于是有了segment(段)概念,segment的信息存放于program header table中。segment fault就是因为出错的时候在内存中失败。
ELF文件处理相关工具:
Binutils官网地址:https://www.gnu.org/software/binutils/
ar:归档文件,将多个文件打包成一个大文件。
as:被gcc调用,输入汇编文件,输出目标文件工链接器ld连接。
ld:gnu链接器,被gcc调用,它把目标文件和各种库文件结合在一起,重定位数据,并链接符号引用。
objcopy:执行文件格式转换。
objdump:显示ELF文件的信息。
readelf:显示更多ELF格式文件的信息(包括DWARF调试信息)
示例1:
gcc -c hello.c
readelf -h hello.o // 查看头信息readelf -S hello.o // 查看section信息
readelf -SW hello.o
实例2:
gcc -g -c hello.c
objdump -S hello.o // 显示汇编指令
嵌入式开发
嵌入式开发是一种比较综合性的技术,它不单指纯粹的软件开发技术,也不单是一种硬件配置技术;它是在特定的硬件环境下针对某款硬件进行开发,是一种系统级别的与硬件结合比较紧密的软件开发技术。程序并不是运行在本地,而是运行在特殊的硬件上。
参与编译和运行的机器根据其角色可以分成以下三类:
-build系统:生成编译器可执行程序的计算机。编译器在build系统上编译出来的。
-host系统:运行编译器可执行程序,编译链接应用程序的计算机系统。
-target系统:运行应用程序的计算机系统。
根据build/host/target的不同组合我们可以得到如下的编译方式分类:
-本地(native)编译:build==host==target
-交叉(cross)编译:build==host!=target
QEMU
QEMU是一套由(Fabrice Bellard)编写的以GPL许可证分发源码的计算机系统模拟软件,在GNU/Linux平台上使用广泛。
QEMU,支持多种体系架构。譬如:IA-32(x86),AMD 64, MIPS 32/64,RISC-V 32/64等等。
QEMU有两种主要运作模式:
-user mode: 直接运行应用程序 (比如说hello.o运行直接输出“hello”)
-system mode:模拟整个计算机系统,包括中央处理器及其他周边设备。
第二章 汇编语言编程
基本组成
汇编文件一般后缀为.S或.s,.S包含了预处理的语句,.s就是纯粹的汇编语句。
一个完整的RISC-V汇编程序有多条语句(statement)组成。一条典型的RISC-V汇编语言由3部分组成:
[label:] [operation] [comment]
打方括号表示可选。
-label表示一个标号,必须以":"结尾。label相当于一个地址,给这个地址起了个名字。是这条指令存放在内存的地址。
-operation可以由以下多种类型:
-instruction(指令):直接对应二进制机器指令的字符串
-preudo-instruction(伪指令):为了提高编写代码的效率,可以用一条伪指令指示汇编器产生多条实际的指令(instruction)。(要在汇编器的手册里查看定义)
-directive(指示/伪操作):通过类似指令的形式(以“.”开头),通知汇编器如何控制代码的产生等,不对应具体的指令。属于汇编器自己定义的一些语法。(要在汇编器的手册里查看定义)
-macro:采用.macro/.endm自定义的宏
例子:
.macro do_nothing #directivenop #preudo-instructionnop #preudo-instruction
.endm.text #directive 告诉大家生成的指令要放到text的section中.global _start #directive _start是个全局变量,外部可见,有点像extern_start: #labelli x6, 5 #preudo-instructionli x7, 4 #preudo-instructionadd x5, x6, x7 #instructiondo_nothing #calling macro.end #End of file
RISC-V汇编指令操作对象
寄存器:
-32个通用寄存器,x0~x31;
-在RISC-V中,Hart在执行算术逻辑运算时所操作的数据必须直接来自寄存器。
内存:
-Hart可以执行在寄存器和内存之间的数据读写操作;
-读写操作使用字节(Byte)为基本单位进行寻址;
-RV32可以访问最多2^32个字节的内存空间。
XLEN指的是寄存器的长度32/64。
x0寄存器是zero寄存器,里面读出来永远是0,只读不写。
pc寄存器是外界不可见的。
RISC-V汇编指令类型
参考riscv-spec-20191213.pdf文件中的第24章
-指令长度;ILEN1=32 bits(RV32I)
-指令对齐:IALIGN=32bits(RV32I),指的是在内存中对齐。地址对齐32byte。
-32个bit划分成不同的“域(field)”
-funct3/funct7和opcode一起决定最终的指令类型。
-指令在内存中按照小端序排列。
31-25 | 24-20 | 19-15 | 14-12 | 11-7 | 6-0 | |
funct7 | rs2 | rs1 | funct3 | rd | opcode | R-type |
imm[] | imm[] | rs1 | funct3 | rd | opcode | I-type |
imm[] | rs2 | rs1 | funct3 | imm[4:0] | opcode | S-type |
imm[] | rs2 | rs1 | funct3 | imm[4:1[11]] | opcode | B-type |
imm[] | imm[] | imm[] | imm[] | rd | opcode | U-type |
imm[] | imm[] | imm[] | imm[] | rd | opcode | J-type |
R-type:(register),每条指令中有三个fields,用于指定3个寄存器参数。
I-type:(Immediate),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为12bits)。
S-type: (Store),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为12bits,但fields的组织方式不同于I-type)。(用来访问内存的指令)
B-type: (Branch),每条指令除了带有两个寄存器参数外,还带有一个立即数参数(宽度为12bits,但取值为2的倍数)。(跟分支跳转有关)
U-type:(Upper),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为20bits,用于表示一个立即数的高20位)。
J-type:(Jump),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为20bits)。
小端序的概念
-主机字节序(HBO - Host Byte Order),默认小端序。
-一个多字节整数在计算机内存中存储的字节顺序称为主机字节序(HBO-Host Byte Order,或者叫本地字节序);
-不同类型CPU的HBO不同,这与CPU的设计有关。分为大端序和小端序。
riscv是小端序编指令。
算数指令
基于算术运算指令实现的其他伪指令
伪指令 | 语法 | 等价指令 | 指令描述 | 例子 |
NEG | NEG RD, RS | SUB RD, X0, RS | 对RS中的值取反并将结果存放在RD中 | neg x5, x6 |
MV | MV RD, RS | ADDI RD, RS, 0 | 将RS中的值拷贝到RD中 | mv x5, x6 |
NOP | NOP | ADDI x0, x0, 0 | 什么也不做 | nop |
LUI(Load Upper Immediate)
语法 | LUI RD, IMM | |
例子 | lui x5, 0x12345 | x5 = 0x12345 << 12 |
LUI指令会构造一个32bits的立即数,这个立即数的高20位对应指令中的imm,低12位清零。这个立即数作为结果存放在RD中。
例子
-利用LUI+ADDI来为寄存器加载一个大数0x12345678
lui x1, 0x12345 # x1 = 0x12345000
addi x1, x1, 0x678 # x1 = 0x12345678
-利用LUI+ADDI来为寄存器加载一个大数0x12345FFF
由于addi里的立即数会被符号扩展,所以不能直接加上FFF。
lui x1, 0x12346 # x1 = 0x12346000
addi x1, x1, -1 # x1 = 0x12345FFF
LI(Load Immediate)
AUIPC
语法 | AUIPC RD, IMM | |
例子 | auipc x5, 0x12345 | x5 = 0x12345 << 12 + PC |
auipc指令采用U-type
和LUI指令类似,AUIPC指令也会构造一个32bits的立即数,这个立即数的高20位对应指令中的imm,低12位清零。但和LUI不同的是,AUIPC会先将这个立即数和PC值相加,将相加的结果放在RD中。
应用场景:动态库地址的加载。
LA(Load Address)
语法 | LA RD, LABEL | |
例子 | la x5, foo |
LA是一个伪指令
具体编程时给出需要加载的label,编译器会根据实际情况利用auipc和其他指令自动生成正确的指令序列。
常用语加载一个函数或者变量的地址。
例子
_start:la x5, _start # x5 = _startjr x5
反汇编出来很可能就是一条auipc指令。
逻辑运算指令
移位运算指令
内存读写指令
条件分支指令
x1寄存器用来保存返回地址。
指令寻址模式总结
函数调用过程概述
当caller和callee不是一个人写的,这个时候就需要制定一套规定
当然也可以在调用函数的时候把所有寄存器都存到栈内,但这样效率太低了,于是需要分批。
栈帧的大小是编译阶段就确定了的?函数起始部分和函数退出部分都是编译器帮我们实现的。
例子(尾调用):
例子(非尾调用):
函数调用的汇编代码由三部分组成:开场代码,主体代码,退场代码。 大概得逻辑就是父函数调用子函数,参数传递用a0~ax寄存器,返回值默认用a0寄存器。通过a字号寄存器做交互。函数的中间值保存在s字号寄存器。所以编译器每次都要评估此函数会用到多少个s号寄存器,把需要用的s号寄存器先sw起来,如果不是尾调用还要sw ra寄存器。子函数调用完,结果写到a0,复原s号寄存器,照着返回地址返回到父函数。记住,函数与函数之间是通过寄存器交互的。
stack_start和stack_end之间是对栈的定义
RISC-V编程与C混合编程
遵循ABI(Abstract Binary Interface)的规定
-数据类型的大小,布局和对齐
-函数调用约定(calling convention)
-系统调用约定
RISC-V函数调用约定规定:
-函数参数采用寄存器a0~a7传递
-函数返回值采用a0和a1传递
例子 (汇编调用C)
例子(C调用汇编),方括号内的内容是可选的,加个volatile意思是让编译器不要优化。
简化版本,用顺序来表示映射关系
这篇关于RISCV汇编讲解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!