RISC-V嵌入式开发入门篇2:RISC-V汇编语言程序设计(上)



原文出处:https://mp.weixin.qq.com/s/jyI-SSm_5Gg-KQyjKsIj5Q

随着国内第一本RISC-V中文书籍《手把手教你设计CPU——RISC-V处理器篇》正式上市,越来越多的爱好者开始使用开源的蜂鸟E203 RISC-V处理核,很多初学者留言询问有关RISC-V工具链使用的问题,因此本公众号将开始陆续发表若干篇有关RISC-V软件工具链使用的文章,包括:

在本号之前发表的文章《编译过程简介》中介绍了C/C++语言如何被编译成为汇编语言,而本文将介绍如何直接使用RISC-V架构的汇编语言进行程序设计。

《RISC-V汇编语言程序设计》——分成上中下三篇,本篇是上篇。继续关注公众号可查询中下两篇的内容。

注:本文节选自《RISC-V架构与嵌入式开发快速入门》(即将出版)。

1.1 汇编语言简介

汇编语言(Assembly Language)是一种“低级”语言,汇编语言一听就不高兴了:凭什么说我是低级语言,我哪里低级了。

其实此“低级”非彼“低级”,之所以说汇编语言是一种低级的语言,是因为其面向的是最底层的硬件,直接使用的是处理器的基本指令。因此,相对于抽象层次更高的C/C++语言,汇编语言确实是一门“低级”的语言,此“低级”是指其抽象层次比较低。

由于汇编语言的“低级”属性,它有如下缺点:

  • 由于汇编语言直接接触最底层的硬件,需要对底层硬件非常熟悉才能编写出高效的汇编程序,因此,汇编语言是一门比较难以使用的语言,故而有“汇编语言不会编”的说法。

  • 由于汇编语言的抽象层次很低,因此使用汇编语言设计程序无法像高级语言那样写出灵活多样的程序,并且其程序代码很难阅读和维护。

  • 由于汇编语言使用的是处理器的基本指令,而处理器指令与其处理器架构一一对应,所以,不同架构处理器的汇编程序必然是无法直接移植,所以汇编程序的可移植性和通用性很差。

但是每一枚硬币皆有其两面,汇编语言也有其优点:

  • 由于汇编的过程即为汇编器将汇编指令直接翻译成二进制的机器码(处理器指令),因此程序员可以对生成的二进制代码进行完全的掌控,不会受到编译器的影响。

  • 由于汇编语言直接面向最底层的硬件,因此其可以对处理器进行直接的控制,可以最大化的挖掘硬件的特性和潜能,可以开发出最佳优化的代码。

综上,虽然现在大多数的程序设计已经不再使用汇编语言,但是在一些特殊的场合,譬如底层驱动、引导程序、高性能算法库等领域,汇编语言还经常扮演着重要的角色。尤其对于嵌入式软件开发人员而言,即便无法娴熟地编写复杂的汇编语言,但是能够阅读理解并且编写简单地汇编程序可以说是嵌入式软件人员必备的技能。

1.2 RISC-V汇编程序概述

汇编程序的最基本元素是指令,指令集是处理器架构的最基本要素。因此RISC-V汇编语言的最基本元素自然是一条条的RISC-V指令。

除了指令之外,由于本号之前发表文章《RISC-V嵌入式开发入门篇1:RISC-V GCC工具链的介绍》介绍的RISC-V工具链是GCC工具链,因此,一般的GNU汇编语法也能够被GCC的汇编器识别,所以GNU汇编语法中定义的伪操作、操作符、标签等语法规则均可以在RISC-V汇编语言中使用。因此,一个完整的RISC-V汇编程序由RISC-V指令和GNU汇编规则定义的伪操作、操作符、标签等组成。

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

在这里插入图片描述

  • 标签:表示当前指令的位置标记,请参见第1.5.1节了解具体使用实例 。

  • 操作码:操作码可以是如下任意一种:

    1. RISC-V指令的指令名称,譬如addi指令、lw指令等。有关RISC-V指令的完整列表和详情请参见中文书《手把手教你设计CPU——RISC-V处理器篇》附录A。
    2. 汇编语言的伪操作,请参见第1.4节了解更多信息。
    3. 用户自定义的宏,请参见第1.5.2节了解具体使用实例。
    4. 操作数:操作数是操作码所需的参数,与操作码之间以空格分开。
    5. 操作数:可以是符号、常量、或者由符号和常量组成的表达式。
    6. 注释:注释即为了使的程序代码便于理解而添加的信息,注释并不发挥实际功能,仅仅起到注解作用。注释是可选的,如果添加注释,需要:
    7. 以“;”或者“#”作为分隔号,以分隔号开始的本行之后部分到本行结束都会被当做注释。
    8. 或者使用类似C语言的注释语法//和/* */对单行或者大段程序进行注释。

一段典型的RISC-V汇编程序如下所示:
在这里插入图片描述

上述汇编程序中使用到的汇编语法将在本文后续章节分别进行介绍。

1.3 RISC-V汇编指令

汇编程序的最基本元素是指令,指令集是处理器架构的最基本要素。因此RISC-V汇编语言的最基本元素便是一条条的RISC-V指令。请参见中文书《手把手教你设计CPU——RISC-V处理器篇》的附录A了解RISC-V指令集的详细信息。

除了普通的指令,RISC-V还定义了伪指令以便于用户编写汇编程序,本文第1.5.3和1.5.4节给出了使用伪指令的汇编程序实例。请参见中文书《手把手教你设计CPU——RISC-V处理器篇》的附录A.15节了解RISC-V伪指令的详细信息。

1.4 RISC-V汇编程序伪操作

在汇编语言中,有一些特殊的操作助记符,这些操作的助记符通常被称为伪操作(Pseudo Ops),伪操作在汇编程序中的作用是指导汇编器处理汇编程序的行为,这些伪操作仅在汇编过程中起作用,一旦汇编结束,伪操作的使命就此结束。

由于本文介绍的RISC-V工具链是GCC工具链,因此,一般的GNU汇编语法中定义的伪操作均可在RISC-V汇编语言中使用。经过不断的增加,目前GNU汇编中定义的伪操作数目众多,感兴趣的读者可以自行查阅完整的GNU汇编语法手册了解详情。本节将仅简介若干常见的伪操作。

  • .file filename

.file伪操作用指示汇编器该汇编程序的逻辑文件名。

  • .global symbol_name或者.globl symbol_name

.global和.globl伪操作用于定义一个全局的符号,使得链接器能够全局识别它,即一个程序文件中定义的符号能够被所有其他程序文件可见。

  • .local symbol_name

.local伪操作用于定义局部符号,使得此符号不能够被其他程序文件可见。

  • .weak symbol_name

    1. 在汇编程序中,符号的默认属性为强(strong),.weak伪操作则用于设置符号的属性为弱(weak),如果此符号之前没有定义过,则同时创建此符号并定义其属性为weak。

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

  • .type name , type description

.type伪操作用于定义符号的类型。譬如“.type symbol,@function”即将名为symbol的符号定义为一个函数(function)。

  • .align integer

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

  • .balign integer

.balign伪操作用于将当前PC地址推进到“integer个字节”对齐的位置。

  • .zero integer

.zero伪操作将从当前PC地址处开始分配integer个字节空间并且用0值填充。譬如“.zero 3”即表示分配三个字节的0值。

  • .byte expression [, expression]*

.byte伪操作将从当前PC地址处开始分配若干个字节(byte)的空间,每个字节填充的值由分号分隔开的expression指定。

  • .2byte expression [, expression]*

.2byte伪操作将从当前PC地址处开始分配若干个双字节(2 bytes)的空间,每个双字节填充的值由分号分隔开的expression指定。空间分配的地址可以与双字节非对齐。

  • .4byte expression [, expression]*

.4byte伪操作将从当前PC地址处开始分配若干个四字节(4 bytes)的空间,每个四字节填充的值由分号分隔开的expression指定。空间分配的地址可以与四字节非对齐。

  • .8byte expression [, expression]*

.8byte伪操作将从当前PC地址处开始分配若干个八字节(8 bytes)的空间,每个八字节填充的值由分号分隔开的expression指定。空间分配的地址可以与八字节非对齐。

  • .half expression [, expression]*

.half伪操作将从当前PC地址处开始分配若干个半字(half-word)的空间,每个半字填充的值由分号分隔开的expression指定。空间分配的地址一定与半字对齐(half-word aligned)。

  • .word expression [, expression]*

.word伪操作将从当前PC地址处开始分配若干个字(word)的空间,每个字填充的值由分号分隔开的expression指定。空间分配的地址一定与字对齐(word aligned)。

  • .dword expression [, expression]*

.dword伪操作将从当前PC地址处开始分配若干个双字(double-word)的空间,每个双字填充的值由分号分隔开的expression指定。空间分配的地址一定与双字对齐(double-word aligned)。

  • .string “string”

.string伪操作将从当前PC地址处开始分配若干个字节空间用于存放“string”字符串。字节的个数取决于字符串的长度。

  • .float 或者 .double expression [, expression]*

    1. .float伪操作将从当前PC地址处开始分配若干个单精度浮点数(32位)的空间,每个单精度浮点数填充的值由分号分隔开的expression指定。空间分配的地址一定与32位对齐。

    2. .double伪操作将从当前PC地址处开始分配若干个双精度浮点数(64位)的空间,每个双精度浮点数填充的值由分号分隔开的expression指定。空间分配的地址一定与64位对齐。

    3. 若干.float和.double伪操作的示例如下:

在这里插入图片描述

  • .comm或者.common name, length

.comm和.common伪操作用于声明一个名为name的未初始化存储区间,区间大小为length个字节。由于是未初始化存储区间,在链接阶段会将其链接到.bss段中。有关链接后ELF文件常见段.text、.data、.rodata、.bss请参见本号之前发表文章《编译过程简介》节了解更多信息。

  • .option {rvc,norvc,push,pop}
  1. .option伪操作用于设定某些架构特定的选项,使得汇编器能够识别此选项并按照选项的定义采取相应的行为。

  2. rvc、norvc是RISC-V架构特有的选项,用于控制是否生成16位宽的压缩指令:

    • “.option rvc”伪操作表示接下来的汇编程序可以被汇编生成16位宽的压缩指令。

    • “.option norvc”伪操作表示接下来的汇编程序不可以被汇编生成16位宽的压缩指令。

  3. push、pop用于临时性的保存或者恢复.option伪操作指定的选项:

    • “.option push”伪操作暂时将当前的选项设置保存起来,从而允许之后使用.option伪操作指定新的选项;而“.option pop”伪操作将最近保存的选项设置恢复出来重新生效。

    • 通过“.option push”和“.option pop”的组合,便可以在汇编程序中在不影响全局选项设置的情况下,为其中嵌入的某一段代码特别地设置不同的选项。

  • .section name [, subsection]

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

  1. “.section .text”伪操作将接下来的代码汇编链接到.text段。

  2. “.section .data”伪操作将接下来的代码汇编链接到.data段。

  3. “.section .rodata”伪操作将接下来的代码汇编链接到.rodata段。

  4. “.section .bss”伪操作将接下来的代码汇编链接到.bss段。

  • .text

.text伪操作基本等效于“.section .text”。

  • .data

.data伪操作基本等效于“.section .data”。

  • .rodata

.rodata伪操作基本等效于“.section .rodata”。

  • .bss

.bss伪操作基本等效于“.section .bss”。

  • .pushsection name 和.popsection

    1. .pushsection伪操作将之前的段设置保存起来,并且将当前的段设置改为名为name的段。即,指明将接下来的代码汇编链接到名为name的段中。

    2. .popsection伪操作将最近保存的段设置恢复出来。

    3. 通过“.pushsection”和“.popsection”的组合,便可以在汇编程序的编写过程中,在某一个段的汇编代码中特别的插入另外一个段的代码。这种编写方式在某些情况下会给代码编写带来极大的方便,示例代码如下:

在这里插入图片描述

  • .macro和.endm

    1. .macro和.emdm伪操作用于将一串汇编代码定义成为一个宏。

    2. “.macro name arg1 [, argn]”用于定义名为name的宏,并且可以传入若干由分号分隔的参数。

    3. “.endm”用于结束宏定义。

  • .equ name, value

.equ伪操作用于将名为name的符号赋值为value的值。

《RISC-V汇编语言程序设计》——分成上中下三篇,本篇是上篇。继续关注公众号可查询中下两篇的内容。

(未完待续:请参见《RISC-V嵌入式开发入门篇2:RISC-V汇编语言程序设计(中)、《RISC-V嵌入式开发入门篇2:RISC-V汇编语言程序设计(下)》)

更多信息

感兴趣的读者可以通过下面二维码关注公众号“硅农亚历山大”,了解Verilog、IC设计、CPU、RISC-V和人工智能AI相关的更多设计技巧和经验分享,注意:由于干货太多,请自备茶水。

在这里插入图片描述