RISC-V汇编学习(四)—— RISCV QEMU平台搭建(基于芯来平台)

发布于:2025-03-14 ⋅ 阅读:(17) ⋅ 点赞:(0)

0 前言

无论是x86架构还是ARM架构的汇编代码,都需要在对应架构的物理芯片或兼容的模拟环境中执行。这意味着任何机器码都必须在相应的处理器架构上运行,以实现对底层硬件的操作。RISC-V架构也不例外,因此我们需要搭建一个合适的执行平台。通常有两种主要的方法来实现:1、使用实际的RISC-V开发板 2、通过软件模拟器如QEMU创建虚拟环境。

幸运的是,现在有了更多选择。例如,博主手中就有一块兆易创新基于芯来科技N205 RISC-V核心的GD32VF103开发板。这块开发板不仅提供了高性能和低功耗的优势,还配备了丰富的外设资源,是探索RISC-V架构的理想选择。对于有兴趣的朋友来说,GD32VF103不仅是STM32F103的一个优秀替代品,同时也是一个非常适合RISC-V入门学习的开发板。不妨动手试试看,体验一下RISC-V带来的不同之处吧。

但是暂时没有这样一块RISCV开发板的同学也可以通过QEMU来学习RISCV架构指令集,甚至比有开发板更灵活,也能更专注于开发学习,而不用处理复杂的硬件连线和配置。接下来,将先介绍下基于芯来RISCV的QEMU实验平台搭建。

1 QEMU实验环境搭建

1.0 基础环境

virtualbox+ubuntu20.4(新版QEMU依赖20.4的lib库,之前16的版本踩过坑),按照典型配置来就可以,可以参考:VirtualBox安装Ubuntu20.04图文教程,这里不做赘述。(如有现成的linux环境,没有必要虚拟机,当然windows环境也是可以,看个人喜好啦)

1.1 交叉编译工具链

交叉编译是在一个平台(宿主)上生成另一个不同平台(目标)上运行的代码,我们这里就是要用芯来提供的工具链来编译调试。

  1. RISC-V GNU Toolchain (链接: https://nucleisys.com/download.php
  2. 选择嵌入式工具链(2025.2版本)、QEMU(2025.2版本)
    在这里插入图片描述
  3. 解压:
tar -zxvf nuclei-qemu-2025.02-linux-x64.tar.gz
tar -jxvf nuclei_riscv_newlibc_prebuilt_linux64_2025.02.tar.bz2

得到如图所示的gcc和qemu工具
在这里插入图片描述

这里就是芯来官方提供的gcc(还有clang、llvm,但我们只用了gcc)和qemu的工具链了
在这里插入图片描述
在这里插入图片描述

  1. 环境变量设置
    将解压后得工具链路径加入环境变量
    在这里插入图片描述

1.2 裸机sdk

github: git clone https://github.com/Nuclei-Software/nuclei-sdk.git
或者
gitee:  git clone https://gitee.com/Nuclei-Software/nuclei-sdk.git

工程文件环境变量设置
在这里插入图片描述

1.3 测试sdk和工具链

  1. 编译
make CORE=nx900fd SOC=evalsoc DOWNLOAD=ilm ARCH_EXT=_xxldsp dasm

进入*nuclei-sdk/application/freertos/demo/*路径下,编译一个freertos的demo elf出来
在这里插入图片描述
2. qemu运行测试

make CORE=nx900fd SOC=evalsoc DOWNLOAD=ilm ARCH_EXT=_xxldsp run_qemu

一个简单的demo就跑起来啦
在这里插入图片描述
3. 调试
一个终端运行gdb server

make CORE=nx900fd SOC=evalsoc DOWNLOAD=ilm ARCH_EXT=_xxldsp run_qemu_debug

另起一个终端,

riscv64-unknown-elf-gdb freertos_demo.elf

之后连接3333号端口,就进入了熟悉的gdb调试界面。就可以调试啦
在这里插入图片描述
另外更多的makefile说明,可以参考帮助和sdk中的makefile,这里不做过多说明。
关于QEMU的介绍、使用,感兴趣的小伙伴可以自行了解,使用命令和命令参数参考芯来QEMU说明

我这里介绍步骤也相对直接简单很多,不去介绍过多东西,让学习者有更多精力学习RISCV上,而不是在折腾环境。

至此,芯来基于RISCV裸机QEMU平台就搭建好了,你已经相当于拥有了一台RISC-V设备了,只不过它现在躺在虚拟机里,但是你可以把它当成是物理机看待。(对芯来linux感兴趣的同学可以在下面参考官网链接里自行搭建学习,后面博主应该也会玩下)

2 初识RISCV汇编

基于步骤1搭建的QEMU平台可以就可以进行RISCV汇编的学习,当然也可以学习芯来的cpu和接口ip,这里重点还是学习RISCV汇编。但这里仅仅是管中窥豹,看下汇编风格,简单体验下。

一般嵌入式开发见到的汇编最多的场合就是freeloader.S、裸机的startup.S以及文件调试编译*.S等
以上面跑到的eval开发板的startup_evalsoc.S为例,先来一窥下官方release的RISCV汇编究竟如何。
在这里插入图片描述
这里仅截取一部分来看下:

/**
 * Reset Handler called on controller reset
 */
_start:
    /* ===== Startup Stage 1 ===== */
    /* Disable Global Interrupt */
    csrc CSR_MSTATUS, MSTATUS_MIE

    /* If SMP_CPU_CNT is not defined,
     * assume that only 1 core is allowed to run,
     * the core hartid is defined via BOOT_HARTID.
     * other harts if run to here, just do wfi in __amp_wait
     */
#ifndef SMP_CPU_CNT
    /* take bit 0-7 for hart id in a local cluster */
    csrr a0, CSR_MHARTID
    andi a0, a0, 0xFF
    /* BOOT_HARTID is configurable in Makefile via BOOT_HARTID variable */
    li a1, BOOT_HARTID
    bne a0, a1, __amp_wait
#endif

    /* Initialize GP and TP and jump table base when zcmt enabled */
    .option push
    .option norelax
    la gp, __global_pointer$
    la tp, __tls_base
#if defined(__riscv_zcmt)
    la t0, __jvt_base$
    csrw CSR_JVT, t0
#endif
    .option pop

/* TODO if don't have SMP, you can remove the SMP_CPU_CNT related code */
#if defined(SMP_CPU_CNT) && (SMP_CPU_CNT > 1)
    /* Set correct sp for each cpu
     * each stack size is __STACK_SIZE
     * defined in linker script */
    lui t0, %hi(__STACK_SIZE)
    addi t0, t0, %lo(__STACK_SIZE)
    la sp, _sp
    csrr a0, CSR_MHARTID
    andi a0, a0, 0xFF
    li a1, 0
1:
    beq a0, a1, 2f
    sub sp, sp, t0
    addi a1, a1, 1
    j 1b
2:
#else
    /* Set correct sp for current cpu */
    la sp, _sp
#endif

当然有了前面RISCV汇编的基础,这些汇编看起来应该能大概看懂了,我们一起来看看下。

定义了一个复位处理函数 _start,它在控制器复位时被调用。这个函数主要负责初始化系统状态和设置栈指针等基础操作,以便程序能够正确地开始执行。
通过清除 CSR_MSTATUS 控制状态寄存器中的 MSTATUS_MIE(Machine Interrupt Enable)标志来禁用所有中断,确保在初始化过程中不会被中断打断。

csrc CSR_MSTATUS, MSTATUS_MIE 
#ifndef SMP_CPU_CNT
    /* take bit 0-7 for hart id in a local cluster */
    csrr a0, CSR_MHARTID
    andi a0, a0, 0xFF
    /* BOOT_HARTID is configurable in Makefile via BOOT_HARTID variable */
    li a1, BOOT_HARTID
    bne a0, a1, __amp_wait
#endif

这部分代码检查是否定义了 SMP_CPU_CNT(Symmetric Multiprocessing CPU Count)。如果没有定义,意味着只允许一个核心运行,并且该核心的 hartid(硬件线程ID)是通过 BOOT_HARTID 定义的。如果当前核心的 hartid 不等于 BOOT_HARTID,则跳转到 __amp_wait 标签处等待(即进入低功耗模式),否则继续执行

.option push
.option norelax
la gp, __global_pointer$
la tp, __tls_base
#if defined(__riscv_zcmt)
    la t0, __jvt_base$
    csrw CSR_JVT, t0
#endif
.option pop

这里使用 .option push 和 .option pop 来临时改变一些编译选项。la 指令用于加载地址到寄存器中,这里设置了全局指针 gp 和线程指针 tp。如果启用了 RISC-V 的零成本上下文切换扩展 (__riscv_zcmt),还会设置 CSR_JVT 寄存器指向跳转表基址。

#if defined(SMP_CPU_CNT) && (SMP_CPU_CNT > 1)
    /* Set correct sp for each cpu
     * each stack size is __STACK_SIZE
     * defined in linker script */
    lui t0, %hi(__STACK_SIZE)
    addi t0, t0, %lo(__STACK_SIZE)
    la sp, _sp
    csrr a0, CSR_MHARTID
    andi a0, a0, 0xFF
1:
    beq a0, a1, 2f
    sub sp, sp, t0
    addi a1, a1, 1
    j 1b
2:
#else
    /* Set correct sp for current cpu */
    la sp, _sp
#endif

此段代码根据是否定义了 SMP_CPU_CNT 并且其值大于1来决定如何设置每个CPU的核心栈指针。如果是多核配置,则为每个核心分配独立的栈空间,大小由链接脚本中定义的 __STACK_SIZE 决定。csrr 指令读取当前核心的 hartid,然后根据这个ID调整栈指针的位置。如果只有一个核心或没有定义 SMP_CPU_CNT,则直接将栈指针设置为 _sp 地址。

代码展示了如何在RISC-V架构上进行基本的系统初始化工作,包括中断管理、多核支持以及内存布局的设置。这些都是嵌入式开发中非常重要的步骤,确保操作系统或者应用程序能够在硬件上稳定运行,也就相当于BootLoader了

参考:
https://doc.nucleisys.com/nuclei_tools/qemu/intro.html
https://doc.nucleisys.com/nuclei_board_labs/hw/hw.html