RISCV汇编编程讲解

第一章 引言

为什么要讲riscv?

         riscv的特点:

        -诞生于顶尖学术机构:诞生于加州大学伯克利分校的体系结构研究院。吸引了大批的顶尖企业参与(e.g. 谷歌、华为、高通、阿里巴巴为rsicv的发展提供了大量的资金支持和贡献了技术和人才)。

        -精简指令集:指令相对精简,指令格式规整,易于实现和理解。只包含了最基本和常用的指令,避免了复杂和特殊用途的指令,使得处理器的硬件设计更加简洁高效。比如说长度固定(32位/64位)。

        -模块化设计:可以根据不同的应用需求选择不同的模块进行组合。这使得riscv可以适应从嵌入式系统到高性能服务器等不同的应用场景,具有很高的灵活性。

        -开源:例如有多个开源的riscv编译器(如gcc和llvm)可供选择。

        -低功耗:指令集设计精简,处理器的硬件实现相对简单,有助于降低功耗。

命名规范

        RV[###][abc......xyz]。

        [###]表示的是寄存器的位宽,后面的字母标识处理器支持的指令集模块集合。

        例如:RV32MA、RV64GC。

模块化        

        模块化ISA vs 增量式ISA

        RISC-V ISA = 1个基本整数指令集 + 多个可选的扩展指令集。

        基本整数指令集:唯一强制要求实现的基础指令集,其他指令集都是可选的扩展模块。

        扩展模块指令集:RISC-V允许在视线中以可选的形式实现其他标准化和非标准化的指令集扩展。特定组合“IMAFD”被称为“通用”组合,用英文字母G表示。

扩展指令集描述
M整式乘法与除法指令集
A存储器原子指令集
F单精度浮点指令集
D双精度浮点指令集
C压缩指令集(16位长度)
......其它标准化和非标准化指令集

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就是因为出错的时候在内存中失败。

0a26db0bd61245a29c645fb07b36ce17.png

        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      #directive
        nop            #preudo-instruction
        nop            #preudo-instruction
.endm

    .text              #directive  告诉汇编器生成的指令要放到text的section中
    .global _start     #directive  _start是个全局变量,外部可见,有点像extern


_start:                 #label
    li x6, 5            #preudo-instruction
    li x7, 4            #preudo-instruction
    add x5, x6, x7      #instruction
    do_nothing          #calling macro


    .end                #End of file

RISC-V汇编指令操作对象

        寄存器:

        -在RISC-V中,Hart在执行算术逻辑运算时所操作的数据必须直接来自寄存器

        -32个通用寄存器,x0~x31;

 寄存器别名
x0读0寄存器,其值永远为0,写操作对其无效。
x1ra,用于存储函数调用后的返回地址。
x2sp,栈指针。
x3gp,全局指针。一半指向全局变量和静态变量。它可以帮助快速访问全局数据区域,提高对全局变量的访问效率。
x4tp,线程指针。主要用于多线程编程,指向当前线程的特定数据结构或信息。
x5-x7,x28-x31t0-t6,临时寄存器。通常用于临时存储或中间结果的保存。在程序执行过程中,可用于暂存数据、参与运算等。
x8-x9,x18-x27s0-s11,保存寄存器。用于在函数调用过程中保存寄存器的值,以便在函数返回时恢复这些值。
x10-x17a0-a7,参数寄存器。用于传递函数的参数。参数可以通过这些寄存器传递给被调用函数,方便函数内部对参数进行访问和处理。

 RISC-V汇编指令类型

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位)。(auipc、lui)

        J-type:(Jump),每条指令含有一个寄存器参数再加上一个立即数参数(宽度为20bits)。

小端序的概念

         riscv是小端序编指令。

算数指令

指令语法描述例子
ADDADD RD, RS1, RS2RS1和RS2的值相加,结果保存到RDadd x5, x6, x7
SUBSUB RD, RS1, RS2RS1的值减去RS2的值,结果保存到RDsub x5, x6, x7
ADDIADDI RD, RS1, RS2RS1的值和IMM相加,结果保存到RDaddi x5, x6, x7
LUILUI RD, IMM构造一个32位的数,高的20位存放IMM,低12位清零,结果保存到RDlui x5, 0x12345
AUIPCAUIPC RD, IMM构造一个32位的数,高20位存放IMM,低12清零。结果和PC相加保存到RD。auipc x5, 0x12345

基于算术运算指令实现的其他伪指令

伪指令语法等价指令指令描述例子
LILI RD, IMMLUI和ADDI的组合将立即数加载到RD中li x5, 0x12345678
LALA RD, LABELAUIPC和ADDI组合为RD加载一个地址值la x5,label
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

LI(Load Immediate)

        用算数指令构建一个大数  

#例子:构建一个大数

# imm is in range of [-2048, +2047]
# 0x80000
lui x5, 0x80
addi x5, x0, 0x80


# imm is NOT in the range of [-2048, +2047]
# and the most-significant-bit of "lower-12" is 0
# 0x12345001
lui x6, 0x12345
addi x6, x6, 0x001


# imm is NOT in the range of [-2048, +2047]
# and the most-significant-bit of "lower-12" is 1
# 0x12345FFF
lui x7 0x12346
addi x7, x7, -1

        用伪指令

li x7 xxxxxxxxx

LA(Load Address)

语法LA RD, LABEL 
例子la x5, foo 

        LA是一个伪指令

        具体编程时给出需要加载的label,编译器会根据实际情况利用auipc和其他指令自动生成正确的指令序列。

        常用语加载一个函数或者变量的地址。

        例子

_start:
    la x5, _start    # x5 = _start
    jr x5

        反汇编出来很可能就是一条auipc指令。

逻辑运算指令

指令格式语法描述例子
ANDR-typeAND RD, RS1, RS2RD = RS1 & RS2and x5, x6, x7
ORR-typeOR RD, RS1, RS2RD = RS1 | RS2or x5, x6, x7
XORR-typeXOR RD, RS1, RS2RD = RS1 ^ RS2xor x5, x6, x7
ANDII-typeANDI RD, RS1, IMMRD = RS1 & IMMandi x5, x6, 20
ORII-typeORI RD, RS1, IMMRD = RS1 | IMMor x5, x6, 20
XORII-typeXORI RD, RS1, IMMRD = RS1 ^ IMMxor x5, x6, 20

        所有的逻辑指令都是按位操作。

逻辑移位运算指令

指令格式语法描述例子
SLLR-typeSLL RD, RS1, RS2逻辑左移sll x5, x6, x7
SRLR-typeSRL RD, RS1, RS2逻辑右移srl x5, x6, x7
SLLII-typeSLLI RD, RS1, IMM逻辑左移立即数slli x5, x6, 3
SRLII-typeSRLI RD, RS1, IMM逻辑右移立即数srli x5, x6, 3

        无论是逻辑左移还是右移,补足的都是0。

算术移位运算指令​​​​​​​​​​​​​​

指令格式语法描述例子
SRAR-typeSRA RD RS1, RS2算术右移sra x5, x6, x7
SRAII-typeSRAI RD, RS1, IMM算术右移立即数srai x5, x6, 3

        算术右移时按照符号位值补足。

内存读写指令

5e8a39d3a31e42b1bfc6d93497f033f7.png

 

内存读

指令格式语法描述例子
LBI-type        LB RD, IMM(RS1)读取一个8bits的数据到RD中,内存地址=RS1 + IMM,数据保存到RD之前执行sign-extended。lb x5, 40(x6)
LBUI-type        LBU RD, IMM(RS1)数据在保存到RD之间会执行zero-extendedlbu x5, 40(x6)
LHI-type        LH RD, IMM(RS1)读取一个16bit的数据。lh x5, 40(x6)
LHUI-type         LHU RD, IMM(RS1)读取伊恩16bit的数据。lhu x5, 40(x6)
LWI-type         LW RD, IMM(RS1)读取一个32bit的数据。lw x5, 40(x6)

内存写

指令格式语法描述例子
SBS-type       SB SR2, IMM(RS1)将RS2寄存器中低8bits的数据写出到内存中,内存地址=RS1 + IMM。sb x5, 40(x6)
SHS-type        SH SR2, IMM(RS1) sh x5, 40(x6)
SWS-type         SW SR2, IMM(RS1) sw x5, 40(x6)

条件分支指令

指令格式语法描述例子
BEQB-typeBEQ RS1, RS2, IMM如果相等则跳转beq x5, x6, 100
BNEB-typeBNE RS1, RS2, IMM如果不相等则跳转bne x5, x6, 100
BLTB-typeBLT RS1, RS2, IMM按照有符号数方式比较,如果小于则跳转blt x5, x6, 100
BLTUB-typeBLTU RS1, RS2, IMM按照无符号数方式比较,如果小于则跳转bltu x5, x6, 100
BGEB-typeBGE RS1, RS2, IMM按照有符号方式比较,大于等于就跳转bge x5, x6, 100
BGEUB-typeBGEU RS1, RS2, IMM按照无符号方式比较,大小等于就跳转

bgeu x5, x6, 100

伪指令语法等价指令描述
BLEBLE RS, RT, OFFSETBGE RT, RS, OFFSET有符号方式比较,<=
BLEUBLEU RS, RT, OFFSETBGEU RT, RS, OFFSET 无符号方式比较,<=
BGTBGT RS, RT, OFFSETBLT RT, RS, OFFSET有符号比较,>
BGTUBGTU RS, RT, OFFSETBLTU RT, RS, OFFSET无符号比较,>
BEQZBEQZ RS, RT, OFFSETBNE RS, x0, OFFSET==0
BNEZBNEZ RS, RT, OFFSETBLT RS, x0, OFFSET!=0
BLTZBLTZ RS, RT, OFFSETBGE x0, RS, OFFSET< 0
BLEZBLEZ RS, RT, OFFSETBLT RT, R0, OFFSET<= 0
BGTZBGTZ RS, RT, OFFSETBGE RT, RS, OFFSET> 0
BGEZBGEZ RS, OFFSETBGE RT, RS, OFFSET>= 0

无条件跳转

JAL(Jump And Link)

语法JAL RD, LABEL
例子jal x1, label

        jal指令用于调用子过程,跳转到指定位置,并且把下一条指令的地址写入RD,保存为返回地址。

JALR(Jump And Link Register)

语法JALR RD, IMM(RS1)
例子jalr x0, 0(x5)

        jalr指令同样用于调用子过程,跳转到指定位置,并且把下一条指令的地址写入RD,保存为返回地址。

例子

int a = 1;
int b = 1;

void sum()
{
    a = a + b;
    return;       // jalr x0, 0(x5)   不需要保存下一条指令的地址
}

void _start()
{
    sum();        // jal x5, sum

    other codes
    ...
}

思考:

        如何解决更远距离的跳转?

AUIPC X6, IMM-20
JALR X1, X6, IMM-12

指令寻址模式总结

        所谓寻址指的是指令中定位操作数或者地址的方式

寻址模式解释例子
立即数寻址操作数是指令本身的一部分。addi x5,x6,20
寄存器寻址操作数存放在寄存器中,指令中指定访问的寄存器从而获取该操作数。add x5, x6, x7
基址寻址操作数在内存中,指令中通过指定寄存器(基址base)和立即数(偏移量offset),通过base+offset的方式获得操作数在内存中的地址从而获取该操作数。sw x5,40(x6)
PC相对寻址在指令中通过PC和指令中的立即数相加获得目的地址的值。beq x5,x6,100

第三章 函数的调用

函数调用过程概述

85f5cc99cd4c433b9839d1b177edb4bd.png

        当caller和callee不是一个人写的,这个时候就需要制定一套规定。​​​​​​​

f6b99cc24ca34a12aeab2cdc204d3bb4.png

函数调用的规则

寄存器名ABI名(编程用名)用途约定谁负责在函数调用过程中维护这些寄存器
x0zero读取时总为0,写入时不起任何效果N/A

x1

ra存放函数返回地址Caller
x2sp存放栈指针callee
x5-x7,x28-x31

t0-t2,t3-t6

临时寄存器,callee可能会使用这些寄存器,所以callee不保证这些寄存中的值在函数调用过程中保持不变。对于caller来说,如果需要的话应在调用前保存这些值。caller
x8,x9,x18-x27s0,s1,s2-s11保存寄存器。callee需要保证这些寄存器的值在函数返回后仍然维持函数之前的原值。callee
x10,x11a0,a1参数寄存器,用于在函数调用过程中保存第一个和第二参数。caller
x12-x17a2,a7参数寄存器。如果有更多的参数则要利用到栈。caller

ac621579c0ba45af9890755fcba6a742.png

        函数执行过程的三部曲:

        -保存现场

        -执行函数内容

        -恢复现场                

例子(尾调用)

2aae26c431724cf8a111f021a8a5cf19.png

例子(非尾调用)

915b7ecef85341888f0b9972aa37b17b.png

riscv编程与c混合编程

汇编调用c语言

5a5d6d208b394f21a1a5cd745e03b636.png

c语言调用汇编

int foo(int a, int b)
{
    int c;
    asm volatile(
        "add %[sum], %[add1], %[add2]"  // 汇编指令
        :[sum] = r'(c)                  // 输出操作数列表(可选)
        :[add1]"r"(a), [add2]"r"(b)     // 输入操作数列表(可选)
    );
    return c;
}

第四章 UART串行接口调用

状态寄存器读写

语法CSRRW RD, CSR, RS1 
例子csrrw t6, mscratch, t6

t6 = mscratch;mscratch = t6

语法CSRRS RD, CSR, RS1 
例子csrrs x5, mie, x6x5 = mie; mie |= x6

720546e5117f46da89523af5a385ab1c.png

UART(统一异步接收传输协议)

        通过异步收发传输器,是一种双全工、异步串行通信方式,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据,可以实现双全工传输和接收。

        UART串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收。

        UART在发送或接收过程中的一帧数据由4部分组成,起始位(低电平)、数据位、奇偶校验位和停止位(高电平)。

        起始位标志着一帧数据的开始,停止位标志着一帧数据的结束,数据位是一帧数据中的有效数据。

        

4f59758c99884d3a826d5e8a76c928d0.png

        信号与管脚定义

345f5663b7a5478b806564eb1b523ea8.png

        qemu 模拟了 ns16550a 芯片,通过查阅资料可知内存映射将对应的基地址映射到了 0x1000_0000 处。

bf8b914e6cf1433790b4c2db23d36cf4.png

        platform.h编写

/* This machine puts UART register here in physical memory. */
#define UARTO 0x10000000

        uart.c编写

#define UART_REG(reg) ((volatile uint8_t*)(UART0 + reg))
#define uart_read_reg(reg) (*(UART0 + reg))
#define uart_write_reg(reg, v) (*(UART0 + reg)) = (v)

         NS16550a编程接口介绍

917164c231044503a68844dddbcb3ec4.png

        (进入程序)

 

 

 

 

代码链接

https://github.com/plctlab/riscv-operating-system-mooc

https://gitee.com/unicornx/riscv-operating-system-mooc

 

 

 

 

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玟林禹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值