RISC-V (二)汇编语言编程

开篇

        riscv编译器安装参考:在Ubuntu上搭建RiscV交叉编译环境_ubuntu下交叉编译riscv程序的ide环境-CSDN博客

        示例:

$ riscv64-unknown-linux-gnu-gcc -static -o hello.o hello.c
$ qemu-riscv64 hello.o
hello world!

简介 

        汇编语言是一种“低级”语言。

        汇编语言的缺点:

         -难度

        -难写

        -难移植

        汇编语言的优点:

        -灵活

        -强大

        汇编语言的应用场景

        -需要直接访问底层硬件的地方

        -需要对性能执行极致优化的地方

汇编语言语法介绍(GNU版本)

基本组成

        汇编文件一般后缀为.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汇编指令操作对象

        寄存器:

        -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 = _start
    jr 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意思是让编译器不要优化。 

        简化版本,用顺序来表示映射关系

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

玟林禹

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

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

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

打赏作者

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

抵扣说明:

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

余额充值