对C语言编译成RISC-V汇编代码后执行步骤的分析

小白 2022-05-17 13:32:46

 

    经过对x86和ARM6两种经典指令集架构的学习,对指令集架构的框架有了一点基础的了解,遂做了一点调研和学习,关于RISC-V。

    RISC-V是一个基于精简指令集(RISC)原则的开源指令集架构(ISA),被设计成可以支持丰富的定制化和特殊化。最基础的指令是RV32I,即32位的指令,同时还存在其他不同的拓展,如RV64I(64位基础指令扩展)、RV32M(乘法指令扩展)、RV32F(单精度浮点扩展)、RV32D(双精度浮点扩展)等。本文的分享集中于RV32I。

    文章内容分为两个主要部分:寄存器和指令集。

1. 寄存器

    RV32I 基础指令集总共定义了 32 个 32 位的通用寄存器,这一点与x86和ARM64有所不同。

    它们分别被标记为x0 ~ x31。其中零号寄存器 x0 是只读寄存器,其值永远为零。在用汇编语言编写时,这 32 个寄存器的名称也根据其在调用约定中的职能而被重新命名。

img

     其中19个寄存器被划分为两组:x5-x7、x28-x31为临时寄存器,在过程调用中不被被调用者保存;x8-x9、x18-x27为保存寄存器,在过程调用中被保存。同时设置x10-x17参数寄存器,用于传递参数或返回值,x1一个返回地址寄存器,用于返回到起始点。

    除32个通用寄存器外,RSIC-V还定义一个PC寄存器,该寄存器指向当前正在执行的指令的内存地址。

2. 指令集

    与我们熟悉的x86和ARM64不同,RISC-V指令被设计为定长32位,且只有6种类型,简化了指令解码。

    指令在存储器中必须在边界(4字节为单位)对齐。当发生一个条件分支或者无条件转移时,如果目标地址没有对齐,将产生一个指令地址不对齐异常。

    具体六种指令格式如下:

  • R类型:寄存器-寄存器操作;

  • I类型:短立即数和访存load操作;

  • S类型:访存store操作;

  • B类型:条件跳转操作;

  • U类型:长立即数;

  • J类型:无条件跳转。

其中R、I、S和U类型是四种基础指令,而B和J类型都只包含一条指令,具体指令格式如下:

 

img

3. 常用指令介绍

    RISC-V汇编指令分类主要有以下几种:

        算术运算指令

        逻辑运算指令

        位移运算指令

        内存读写指令

        分支与跳转指令

    如下图所示:

 

3.1 算术运算

  • dd rd,rs1,rs2
    :将寄存器rs1与rs2的值相加并写入寄存器rd。
  • sub rd,rs1,rs2
    :将寄存器rs1与rs2的值相减并写入寄存器rd。
  • addi rd,rs1,imm
    :将寄存器rs1的值与立即数imm相加并存入寄存器rd。
  • mul rd,rs1,rs2
    :将寄存器rs1与rs2的值相乘并写入寄存器rd。
  • div rd,rs1,rs2
    :将寄存器rs1除以寄存器rs2的值,向零舍入并写入寄存器rd。
  • rem rd,rs1,rs2
    :将寄存器rs1模寄存器rs2的值并写入寄存器rd。

    发生溢出时会自动截断高位,将低32位写入寄存器中。

3.2 逻辑运算

  • and rd,rs1,rs2
    :将寄存器rs1与rs2的值按位与并写入寄存器rd。
  • andi rd,rs1,imm
    :将寄存器rs1的值与立即数imm的值按位与并写入寄存器rd。
  • or rd,rs1,rs2
    :将寄存器rs1与rs2的值按位或并写入寄存器rd。
  • ori rd,rs1,imm
    :将寄存器rs1的值与立即数imm的值按位或并写入寄存器rd。
  • xor rd,rs1,rs2
    :将寄存器rs1与rs2的值按位异或并写入寄存器rd。
  • xori rd,rs1,imm
    :将寄存器rs1的值与立即数imm的值按位异或并写入寄存器rd。

3.3 移位运算

  • sll rd,rs1,rs2
    :将寄存器rs1的值左移寄存器rs2的值这么多位,并写入寄存器rd。
  • slli rd,rs1,imm
    :将寄存器rs1的值左移立即数imm的值这么多位,并写入寄存器rd。
  • srl rd,rs1,rs2
    :将寄存器rs1的值逻辑右移寄存器rs2的值这么多位,并写入寄存器rd。
  • srli rd,rs1,imm
    :将寄存器rs1的值逻辑右移立即数imm的值这么多位,并写入寄存器rd。
  • sra rd,rs1,rs2
    :将寄存器rs1的值算数右移寄存器rs2的值这么多位,并写入寄存器rd。
  • srai rd,rs1,imm
    :将寄存器rs1的值算数右移立即数imm的值这么多位,并写入寄存器rd。

    左移在右边补0;逻辑右移在最高位添0,算数右移在最高位添加符号位。

3.4 数据传输指令

  • lb rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读一个字节,符号扩展后存入rd
  • lh rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读半个字,符号扩展后存入rd
  • lw rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读一个字,符号扩展后存入rd
  • lbu rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读一个无符号的字节,零扩展后存入rd
  • lhu rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读半个无符号的字,零扩展后存入rd
  • lwu rd,offset(rs1)
    :从地址为寄存器rs1的值加offset的主存中读一个无符号的字,零扩展后存入rd
  • sb rs1,offset(rs2)
    :把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的8位
  • sh rs1,offset(rs2)
    :把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的16位
  • sw rs1,offset(rs2)
    :把寄存器rs1的值存入地址为寄存器rs2的值加offset的主存中,保留最右端的32位

3.5 比较指令

有符号数:

  • slt rd,rs1,rs2
    :若rs1的值小于rs1的值,rd置为1,否则置为0
  • slti rd,rs1,imm
    :若rs1的值小于立即数imm,rd置为1,否则置为0

无符号数:

  • sltu rd,rs1,rs2
    :若rs1的值小于rs1的值,rd置为1,否则置为0
  • sltiu rd,rs1,imm
    :若rs1的值小于立即数imm,rd置为1,否则置为0

3.6 条件分支指令

  • beq rs1,rs2,lable
    :若rs1的值等于rs2的值,程序跳转到lable处继续执行
  • bne rs1,rs2,lable
    :若rs1的值不等于rs2的值,程序跳转到lable处继续执行
  • blt rs1,rs2,lable
    :若rs1的值小于rs2的值,程序跳转到lable处继续执行
  • bge rs1,rs2,lable
    :若rs1的值大于等于rs2的值,程序跳转到lable处继续执行

3.7 无条件跳转指令

  • j label
    :程序直接跳转到lable处继续执行
  • jal rd,label
    :用于调用函数,把下一条指令的地址保存在rd中(通常用x1),然后跳转到label处继续执行
  • jalr rd,offset(rs)
    :可用于函数返回,把下一条指令的地址存到rd中,然后跳转到rs+offset地址处的指令继续执行。

4.RSIC-V 汇编代码分析

4.1 C语言代码段

#include <stdio.h>

void swap(int *a, int *b){
    int c = *a;
    *a = *b;
    *b = c;
}


int main()
{
    int a= 101;
    int b= 202;
    swap(&a, &b);
    printf("a = %d\n", a);
    printf("b = %d\n", b);
    return 0;
}

4.2 汇编代码分析

void swap(int *a, int *b){
    103e:   7179                    addi    sp,sp,-48       // 继续给栈开辟48字节的空间
    1040:   f406                    sd  ra,40(sp)           // 将ra保存至栈中
    1042:   f022                    sd  s0,32(sp)           // 将s0保存至栈中
    1044:   1800                    addi    s0,sp,48        // s0此时只想栈的高地址
    1046:   fea43423                sd  a0,-24(s0)          // 将a0寄存器的地址,到s0-16 ~ s0-24中
    104a:   feb43023                sd  a1,-32(s0)          // 将a1寄存器的地址,保存到s0-24 ~ s0-32中   
    int c = *a;
    104e:   fe843503                ld  a0,-24(s0)          // 从s0-16 ~ s0-24中读取八个字节到a0中,该地址为a0传入的地址,地址中存储的值为101
    1052:   4108                    lw  a0,0(a0)            // 将a0地址中的值加载到a0寄存器中,此时a0为101
    1054:   fca42e23                sw  a0,-36(s0)          // 将a0的值加载到s0-32 ~ s0-36中,此处存入的为一个值
    *a = *b;
    1058:   fe043503                ld  a0,-32(s0)          // 将s0-24 ~ s0-32的地址读取到a0中
    105c:   4108                    lw  a0,0(a0)            // 将a0的值加载给a0,此时a0为202
    105e:   fe843583                ld  a1,-24(s0)          // 将s0-16 ~ s0-24的八字节地址给a1
    1062:   c188                    sw  a0,0(a1)            // 将a0的值加载到a1的地址中,此时a1的地址,即s0-16 ~ s0-24中的值为202
    *b = c;
    1064:   fdc42503                lw  a0,-36(s0)          // 将s0-32 ~ s0-36存的值加载给a0,此处的值为101
    1068:   fe043583                ld  a1,-32(s0)          // 将s0-24 ~ s0-32的八字节地址读取到a1中
    106c:   c188                    sw  a0,0(a1)            // 将a0的值读取到a1中,原来s0-

0000000000001076 <main>:


int main()
{
    1076:   7139                    addi    sp,sp,-64       // 给栈开辟64字节的,此处sp为栈指针
    1078:   fc06                    sd  ra,56(sp)           // 将返回地址ra保存到栈中
    107a:   f822                    sd  s0,48(sp)           // 将s0(fp)帧指针保存到栈中
    107c:   0080                    addi    s0,sp,64        // sp指向的是栈底,s0此时的值为sp+64的值,也就是基地址

000000000000107e <.LBB1_3>:
    107e:   00001517                auipc   a0,0x1          // 将0x1取31位到12位,然后左移12位+pc的地址,结果写入到a0寄存器
    1082:   19a53503                ld  a0,410(a0) # 2218 <__stack_chk_guard@LIBC>  // 将a0偏移410字节的值给a0,此步骤是为了防止堆栈溢出添加的检测保护       
    1086:   610c                    ld  a1,0(a0)            // 将a0的八字节赋给a1
    1088:   feb43423                sd  a1,-24(s0)          // 将a1寄存器的值保存到栈中,保存至s0-16 ~ s0-24的位置
    108c:   4581                    li  a1,0                // 将a1寄存器赋值为0,此处的作用为初始化一个寄存器为0
    108e:   fcb42e23                sw  a1,-36(s0)          // 将a1寄存器的值保存到栈中表示的地址(s0-32 ~ s0-36的位置)
    1092:   06500593                li  a1,101              // 将a1寄存器赋值为101
    int a= 101; 
    1096:   feb42223                sw  a1,-28(s0)          // 由于int在类型为四个字节,此时将101存储到s0-24 ~ s0-28所表示的地址中
    109a:   0ca00593                li  a1,202              // 还是将a0寄存器进行操作,赋值为202
    int b= 202;
    109e:   feb42023                sw  a1,-32(s0)          // 将202存储到s0-28 ~ s0-32的地址中
    10a2:   fe440593                addi    a1,s0,-28       // 将s0-28的地址给到a1
    10a6:   fe040613                addi    a2,s0,-32       // 将s0-32中地址给到a2
    swap(&a, &b);
    10aa:   fca43823                sd  a0,-48(s0)          // 将a0的地址保存到s0-40 ~ s0-48中

00000000000010d2 <.LBB1_5>:
    10e4:   060080e7                jalr    96(ra) # 1140 <printf@plt>
    10e8:   fd043583                ld  a1,-48(s0)          // 将s0-40 ~ s0-48的地址赋值给a1,之前存入的是a0的地址
    10ec:   6190                    ld  a2,0(a1)            // 将a1的地址复制给a2
    10ee:   fe843683                ld  a3,-24(s0)          // 将s0-16 ~ s0-24的八字节赋给a3,也就是之前存入的a0的地址
    10f2:   00d61963                bne a2,a3,1104 <.LBB1_2>    //判断此处的栈是否溢出了

00000000000010fa <.LBB1_1>:
    10fa:   4501                    li  a0,0                // 将a0寄存器赋值为0
    return 0;
    10fc:   7442                    ld  s0,48(sp)           // 恢复s0的值
    10fe:   70e2                    ld  ra,56(sp)           // 恢复ra的值
    1100:   6121                    addi    sp,sp,64        // 恢复sp的值
    1102:   8082                    ret                     // 整个函数退出

5. 堆栈框架

    根据上述分析,可以对RSIC-V函数调用栈的框架有一点了解和认识:

 

参考:https://zhuanlan.zhihu.com/p/496767749

        https://www.jianshu.com/p/5babd0d13dfc

学号:438

...全文
580 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧