RISC-V MCU中文社区

RVMCU课堂「7」: 手把手教你玩转RVSTAR—汇编程序篇

发表于 2021-04-12 13:11:15
0
10401
1

相对于抽象层次更高的C/C++语言,汇编语言是一门抽象层次比较低的语言,面向的是最底层的硬件,直接使用处理器的基本指令。虽然现在大多数的程序设计已经不再使用汇编语言,但是在一些特殊的场合,譬如底层驱动、引导程序、高性能算法库等领域,汇编语言还经常扮演着重要的角色。


系统环境

Windows 10-64bit


软件平台

Nuclei Studio IDE 202102版



汇编语句组成


汇编程序的最基本元素是指令,指令集是处理器架构的最基本要素,因此RISC-V汇编语言的最基本元素自然是一条条的RISC-V指令。除了指令之外,由于此处所用RISC-V工具链是GCC工具链,因此一般的GNU汇编语法也能被GCC的汇编器识别,GNU汇编语法中定义的伪操作、操作符、标签等语法规则均可以在RISC-V汇编语言中使用。

一条典型的RISC-V汇编语句由4部分组成,包含如下字段:


[label:] opcode [operands] [;comment]
[标签:]   操作码   [操作数]     [;注释]



标签

表示当前指令的位置标记。


操作码

可以是如下任意一种:

  • RISC-V指令的指令名称,譬如addi指令、lw指令等。

  • 汇编语言的伪操作。

  • 用户自定义的宏。


操作数

操作码所需的参数,与操作码之间以空格分开,可以是符号、常量,或者由符号和常量组成的表达式。


注释

为了程序代码便于理解而添加的信息,注释并不发挥实际功能,仅起到注解作用。注释是可选的,如果添加注释,需要注意以下规则:

  • 以“;”或者“#”作为分隔号,以分隔号开始的本行之后部分到本行结束都会被当作注释。

  • 或者使用类似C语言的注释语法//和/* */对单行或者大段程序进行注释。







汇编程序伪操作


在汇编语言中,有一些特殊的操作助记符通常被称为伪操作(Pseudo Ops)

伪操作在汇编程序中的作用是指导汇编器处理汇编程序的行为,且仅在汇编过程中起作用,一旦汇编结束,伪操作的使命就此结束

此处所用的RISC-V工具链是GCC工具链,一般的GNU汇编语法中定义的伪操作均可在RISC-V汇编语言中使用。经过不断地增加,目前GNU汇编中定义的伪操作数目众多,感兴趣的读者可以自行查阅完整的GNU汇编语法手册。这里将仅简单介绍一些常见的伪操作。
.global  symbol_name 或者 .globl  symbol_name
.global和.globl伪操作用于定义一个全局的符号,使得链接器能够全局识别它,即一个程序文件中定义的符号能够被所有其他程序文件可见。

.weak  symbol_name

在汇编程序中,符号的默认属性为强 (strong),.weak伪操作则用于设置符号的属性为  弱 (weak),如果此符号之前没有定义过,那么同时创建此符号并定义其属性为 weak。
如果符号的属性为 weak,那么它无须定义具体的内容。在链接的过程中,另外一个属性为 strong的同名符号可以将此weak符号的内容强制覆盖。利用此特性, .weak伪操作常用于预留一个空符号,使得其能够通过汇编器语法检查,但是在后续的程序中定义符号的真正实体,并且在链接阶段将空符号覆盖并链接。

.align  integer

.align 伪操作用于将当前PC地址推进到 “2的integer次方字节” 对齐的位置。譬如 “.align 3” 表示将当前PC地址推进到8个字节对齐的位置处。

.section  name [, subsection]

.section 伪操作指明将接下来的代码汇编链接到名为 name的段 (Section)中,还可以指定可选的子段 (Subsection),常见的段有 .text、.data、.rodata、.bss。例如 ,“.section .text”伪操作将接下来的代码汇编链接到 .text段。


汇编程序定义标签


标签名称通常在一个冒号(:)之前,常见的标签分为文本标签和数字标签。

文本标签在一个程序文件中是全局可见的,因此定义必须使用独一无二的命名,文本标签通常被作为分支或跳转指令的目标地址,示例如下:



 loop:     //定义一个名为loop的标签,该标签代表了此处的PC地址
              ......
              j loop   //跳转指令跳转到标签loop所在的位置


数字标签为0~9之间的数字表示的标签,数字标签属于一种局部标签,需要时可以被重新定义。在被引用时,数字标签通常需要带上一个字母“f”或者“b”的后缀,“f”表示向前,“b”表示向后,示例如下:

      j 1f    //跳转到“向前寻找第一个数字为1的标签”所在的位置,即下一行
               //(标签为1)所在的位置
1:
      j 1b   //跳转到“向后寻找第一个数字为1的标签”所在的位置,即上一行
               //(标签为1)所在的位置



汇编程序定义宏


宏(macro)是将汇编语言中具有一组独立功能的汇编语句组织在一起,然后可以以宏调用的方式进行调用。示例如下:


.macro mac, a, b, c //定义一个名为mac的宏,参数为a、b、c
  
mul t0, b, c            // mul指令将b和c相乘得到乘积写入t0寄存器
  add a, t0, a           // add指令将a与t0相加,将乘累加结果写入a
.endm

//调用mac宏
mac x1, x2, x3



完整实例


为了便于理解汇编程序,我们以 RV-STAR工程的非向量中断处理汇编代码为实例,讲解汇编程序。

在 Nuclei Studio中新建一个 helloworld工程。芯来科技官网的文档与工具页面可以下载Nuclei Studio,下载后解压缩,在 Nuclei Studio解压缩的目录下双击 NucleiStudio.exe即可启动 IDE。

第一次启动 Nuclei Studio将会弹出对话框要求设置 Workspace目录路径,该目录将用于存放后续创建的项目工程文件。设置好 Workspace路径,再单击 “Launch”启动 Nuclei Studio。

启动后推荐打开Launch Bar功能,方便快速编译和调试。打开菜单栏“Window -> Preferences”,搜索“bar”,勾选第一个选项“Enable the Launch Bar”即可启用Launch Bar功能。



在菜单栏中,选择“File-> New -> C/C++ Project”开始新建工程,在弹窗中双击选择“C Managed Build”。

新的页面中“Project name”填写“iasm”,“Project type” 选择“Nuclei SDK Project For GD32VF103 SoC”和“RISC-V Cross GCC”,如下图,点击“Next”。新的页面不用修改,直接点击“Next”即可。


 

在选择模板工程页面修改“Project Example”选项为“baremetal_helloworld”,后续页面不需要修改,点击“Next”直到最后一页,点击“Finish”完成新建helloworld工程。

打开 “nuclei_sdk->SoC->gd32vf103->Common->Source->GCC->intexc_g32vf103.S” 文件,翻到第144行开始到184行结束就是非向量中断处理汇编代码。这里列出具体代码,逐行讲解各个汇编代码的作用。


.section .text.trap     //将接下来的代码汇编链接到text段的trap段中
.align 6                     //2的6次方字节位置对齐
.global exc_entry     //全局变量exc_entry
.weak exc_entry      //定义属性为弱的变量exc_entry
exc_entry:               //名为exc_entry的标签

    SAVE_CONTEXT                     //调用宏SAVE_CONTEXT,作用是保存上下文
    SAVE_CSR_CONTEXT            //调用宏SAVE_CSR_CONTEXT,作用是保存寄存器内容

    csrr a0, mcause                       //将寄存器mcause的值传入通用寄存器a0
    mv a1, sp                                 //将sp的值传入通用寄存器a1
    call core_exception_handler  //调用中断处理函数,将前面传的a0和a1作为函数的参数

    RESTORE_CSR_CONTEXT  //调用宏RESTORE_CSR_CONTEXT,作用是恢复保存的寄存器内容
    RESTORE_CONTEXT          //调用宏RESTORE_CONTEXT,作用是恢复保存的上下文

    mret //退出中断处理函数


在汇编中调用C/C++函数


除了在C/C++程序中内嵌汇编程序之外,还可以在汇编程序中调用C/C++函数。这种情形在实际的工程中也很常见,C/C++语言构造的函数非常普遍,前面的中断处理函数中正是调用C/C++的函数。

在介绍C/C++函数调用之前,先介绍应用程序二进制接口(Abstract Binary Interface,ABI),ABI描述了应用程序和操作系统之间、应用和它的库之间,以及应用的组成部分之间的接口。

ABI涵盖了如下细节:
  • 数据类型的大小、布局和对齐。
  • 函数调用约定(控制着函数的参数如何传送以及接受返回值),例如,是所有的参数都通过栈传递,还是部分参数通过寄存器传递;哪个寄存器用于哪个函数参数;通过栈传递的第一个函数参数是最先还是最后推到栈上。
  • 系统调用的编码和一个应用如何向操作系统进行系统调用。
  • 在一个完整的操作系统ABI中,目标文件的二进制格式、程序库等。


其中,函数调用约定决定了函数调用时参数传递和函数返回结果的规则,有关RISC-V架构ABI的函数调用约定,可以查看RISC-V的架构手册。

对于RISC-V汇编程序而言,在汇编程序中调用C/C++语言函数,必须遵照ABI所定义的函数调用规则,即函数参数由寄存器a0~a7传递,函数返回由寄存器a0~a1指定。上面汇编实例中调用的c函数代码如下:


uint32_t core_exception_handler(unsigned long mcause, unsigned long sp)
{
    uint32_t EXCn = (uint32_t)(mcause & 0X00000fff);
    EXC_HANDLER exc_handler;

    if ((EXCn < MAX_SYSTEM_EXCEPTION_NUM) && (EXCn >= 0)) {
        exc_handler = (EXC_HANDLER)SystemExceptionHandlers[EXCn];
    } else if (EXCn == NMI_EXCn) {
        exc_handler = (EXC_HANDLER)SystemExceptionHandlers[MAX_SYSTEM_EXCEPTION_NUM];
    } else {
        exc_handler = (EXC_HANDLER)system_default_exception_handler;
    }
    if (exc_handler != NULL) {
        exc_handler(mcause, sp);
    }
    return 0;
}

此函数有两个参数,分别为mcausesp,返回一个返回值。所以汇编中调用此函数前,分别向a0和a1寄存器中写入mcause和sp的值。

 









更多实验例程
可以到RVMCU社区
快速入门页面
获取教程资料

https://www.rvmcu.com/quickstart.html

欢迎登陆我们的RV社区:https://www.rvmcu.com/,更多精彩的RVMCU内容等您来~

喜欢1
用户评论
admin

admin 实名认证

专业RISC-V处理器IP找“芯来”

积分
问答
粉丝
关注
专栏作者
个人开发者
企业开发者
  • RV-STAR 开发板
  • RISC-V处理器设计系列课程
  • 培养RISC-V大学土壤 共建RISC-V教育生态
RV-STAR 开发板