RISCV汇编讲解

2024-08-25 14:12
文章标签 讲解 汇编 riscv

本文主要是介绍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指令。

特权级别
levelencodingname
000user/applicationU
101supervisorS
210reserved
311machineM

        当运行在用户态时,就是说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-2524-2019-1514-1211-76-0
funct7rs2rs1funct3rdopcodeR-type
imm[]imm[]rs1funct3rdopcodeI-type
imm[]rs2rs1funct3imm[4:0]opcodeS-type
imm[]rs2rs1funct3imm[4:1[11]]opcodeB-type
imm[]imm[]imm[]imm[]rdopcodeU-type
imm[]imm[]imm[]imm[]rdopcodeJ-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是小端序编指令。

算数指令

基于算术运算指令实现的其他伪指令
伪指令语法等价指令指令描述例子
NEGNEG RD, RSSUB RD, X0, RS对RS中的值取反并将结果存放在RD中neg x5, x6
MVMV RD, RSADDI RD, RS, 0将RS中的值拷贝到RD中mv x5, x6
NOPNOPADDI x0, x0, 0什么也不做nop
LUI(Load Upper Immediate)
语法LUI RD, IMM
例子lui x5, 0x12345x5 = 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, 0x12345x5 = 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汇编讲解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1105779

相关文章

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

Python基础文件操作方法超详细讲解(详解版)

《Python基础文件操作方法超详细讲解(详解版)》文件就是操作系统为用户或应用程序提供的一个读写硬盘的虚拟单位,文件的核心操作就是读和写,:本文主要介绍Python基础文件操作方法超详细讲解的相... 目录一、文件操作1. 文件打开与关闭1.1 打开文件1.2 关闭文件2. 访问模式及说明二、文件读写1.

C# WinForms存储过程操作数据库的实例讲解

《C#WinForms存储过程操作数据库的实例讲解》:本文主要介绍C#WinForms存储过程操作数据库的实例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、存储过程基础二、C# 调用流程1. 数据库连接配置2. 执行存储过程(增删改)3. 查询数据三、事务处

C++快速排序超详细讲解

《C++快速排序超详细讲解》快速排序是一种高效的排序算法,通过分治法将数组划分为两部分,递归排序,直到整个数组有序,通过代码解析和示例,详细解释了快速排序的工作原理和实现过程,需要的朋友可以参考下... 目录一、快速排序原理二、快速排序标准代码三、代码解析四、使用while循环的快速排序1.代码代码1.由快

Java集合中的List超详细讲解

《Java集合中的List超详细讲解》本文详细介绍了Java集合框架中的List接口,包括其在集合中的位置、继承体系、常用操作和代码示例,以及不同实现类(如ArrayList、LinkedList和V... 目录一,List的继承体系二,List的常用操作及代码示例1,创建List实例2,增加元素3,访问元

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

Python itertools中accumulate函数用法及使用运用详细讲解

《Pythonitertools中accumulate函数用法及使用运用详细讲解》:本文主要介绍Python的itertools库中的accumulate函数,该函数可以计算累积和或通过指定函数... 目录1.1前言:1.2定义:1.3衍生用法:1.3Leetcode的实际运用:总结 1.1前言:本文将详

Redis的Zset类型及相关命令详细讲解

《Redis的Zset类型及相关命令详细讲解》:本文主要介绍Redis的Zset类型及相关命令的相关资料,有序集合Zset是一种Redis数据结构,它类似于集合Set,但每个元素都有一个关联的分数... 目录Zset简介ZADDZCARDZCOUNTZRANGEZREVRANGEZRANGEBYSCOREZ

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步