汇编程序被编码为纯文本文件,包含以下4项主要元素:
addi a0, a1, 1
字符串包含助记符addi
和三个操作数:a0
, a1
和1
.word 10
指示汇编器将32位的值(10)添加到程序中。汇编命令通常被编码为以下格式的字符串:点(.)+ 汇编命令名称 + 参数字如前所述,注释对汇编过程没有影响,汇编器会将其丢弃。这通常由预处理器执行,它会删除所有注释和额外的空格。一旦去掉注释和额外的空格,汇编程序就只包含三种元素:标签、汇编指令和汇编命令。假设<label>
、<instruction>
和<directive>
分别表示有效的标签、汇编指令和汇编指令,则在删除汇编语言的注释和额外的空格后,可以使用以下正则表达式来总结汇编语言的语法。
PROGRAM -> LINES
LINES -> LINE ['\n' LINES]
LINE -> [<label>] [<instruction>] |
[<label>] [<directive>]
上述规则的前两行表明汇编代码由一行或者多行组成,每行之间用换行符\n
分割。而最后一条规则表明:
<label>
、<instruction>
和<directive>
都是可选的下面的RV32I汇编代码包含了合法的行:
x:
sum: addi a0, a1, 1
ret
.section .data
y: .word 10
下面的RV32I汇编代码包含了非法的行:
x: z:
addi a0, a1, 1 sum:
li a0, 2 li a1, 1
.word 10 .word 20
.word 10 y:
addi a0, a1, 1 .word 12
.sdfoiywer 1
下面的RV32I汇编代码也是非法的,因为单个指令的所有元素(助记符、操作数)必须位于同一行。汇编命令也同样有这个要求。
addi
a0, a1, 1
GNU汇编器实际上是一系列汇编器的集合。它支持几种ISA,包括RV32I。虽然每种GNU汇编器处理的汇编语言是不同的(这是因为每种汇编语言的指令都和各自的机器指令相似且相互之间不同),但它们中的大多数使用相同的语法表示注释、标签和汇编命令。在这个场景下,一旦你学习了一种汇编语言,则容易学会另外一种新的汇编语言。
本书专门讨论能被GNU汇编器支持的RV32I汇编语言。接下来的章节会讨论RV32I中的注释、标签、汇编指令、汇编命令。
RV32I汇编程序可能包含单行或多行注释。在GNU汇编器上,单行注释由一个行注释符分隔,该字符与目标平台相关,即它依赖于某个ISA。RV32I GNU汇编器使用#
作为行注释符。在同一行中,#
之后的所有字符都被认为是注释的一部分,下面的汇编代码展示了行注释的示例。
x: .word 10 # This is a comment
foo: # My special function
addi a0, a1, 1 # Adds one to a1 and store on a0 #
# This is # another # comment ## #
因为汇编预处理器会抛弃注释,它相当于以下代码:
x: .word 10
foo:
addi a0, a1, 1
多行注释是内容占用多行的注释,在GNU 汇编器中用配对的/* */
符来分割它们
sum1:
/* This
is
a
multi-line
comment.
*/
addi a0, a1, 1
ret
它相当于以下代码
sum1:
addi a0, a1, 1
ret
汇编器将汇编指令转换成机器指令,它们通常编码为一个字符串,其中包含一个助记符和一系列参数,这些参数称为操作数。例如,汇编指令add x10, x11, x12
以17字节(每个字符一个字节)编码为纯文本,被汇编器转换为相应的机器指令,这个机器指令用4字节编码后的值为0x00c58533。
伪指令是这样一种汇编指令:它在ISA上没有对应的机器指令,但可以由汇编器自动翻译成一个或多个替代的机器指令,以达到相同的效果。例如, no操作指令(no operation instruction) 或 nop
是一个RV32I伪指令,它被汇编器转换为指令addi x0, x0, 0
。另一个例子是mv
指令,它将一个寄存器的内容复制到另一个寄存器中。这种情况将由伪指令mv a5, a7
模拟,该伪指令将a7
的内容复制到a5
中。而这个伪指令将被转换为指令addi a5, a7, 0
,它将a7
中的值加0,并将结果存储在寄存器a5
中。
附录A列出了大部分RV32I汇编指令,第二部分的各章讨论了如何使用这些指令实现程序结构,包括条件语句、循环和例程。
汇编指令的操作数可以是以下几种之一:
x0
, x1
,…, x31
。RV32I寄存器也可以通过别名识别,例如a0
、t1
、ra
等。附录A给出了RV32I寄存器及其别名的列表。在汇编语言中,立即数由 字母数字字符(alphanumeric characters) 序列表示。
0x
开头的序列:被解释为十六进制0b
开头的序列:被解释为二进制0
开头的序列:被解释为八进制0~9
开头的序列:被解释为十进制用单引号包住的字符会根据ASCII规范转换为数字,例如操作数'a'
被转换为值97。下列代码展示了使用立即数作为操作数的指令:
li a0, 10 # 将10这个值加载到寄存器a0
li a1, 0xa # 将10这个值加载到寄存器a1
li a2, 0b1010 # 将10这个值加载到寄存器a2
li a3, 012 # 将10这个值加载到寄存器a3
li a4, '0' # 将48这个值加载到寄存器a4
li a5, 'a' # 将95这个值加载到寄存器a5
要表示负整数,只需要加上负号-
前缀即可。例如:
li a0, -12 # 将-12这个值加载到寄存器a0
li a1, -0xc # 将-12这个值加载到寄存器a1
li a2, -0b1100 # 将-12这个值加载到寄存器a2
li a3, -014 # 将-12这个值加载到寄存器a3
li a4, -'0' # 将-48这个值加载到寄存器a4
li a5, -'a' # 将-97这个值加载到寄存器a5
程序符号是与数字相关联的“名称”,而符号表则是将每个程序符号映射到其值的数据结构。标签由汇编器自动转换为符号。此外,程序员或编译器也可以使用汇编命令.set
显式地创建符号。
译注:注意符号(symbol)是ELF中的概念,而标签(label)则是汇编语言中的概念,因此汇编过程中会将标签转换为符号
符号的名称由一系列字母数字字符和下划线(_
)组成。但第一个字符不能是数字字符。以下是有效的符号名称示例:
x
var1
z12345
_x
_
_1
_123
_a12b
以下是无效字符的名称示例:
1
1var
z@12345
x-y
-var
a+b
下列代码展示了使用符号名称作为操作数的指令示例(第4行和第5行)。汇编命令.set
(第1行)创建了符号max_temp
并赋值100。li
(load immediate)指令(第4行)将符号max_temp
的值加载到寄存器t1
中。如果寄存器a0
中的值小于或等于寄存器t1
中的值,则ble
(branch less equal )指令跳转到符号temp_ok
表示的代码位置(由标签temp_ok
自动定义)
.set max_temp, 100 # 设max_temp的最大限制
check_temp: # check_temp例程
li t1, max_temp # 将max_temp加载到t1
ble a0, t1, temp_ok # 如果a0 <= max_temp, 则跳转到temp_ok例程
jal alarm # 否则调用alarm例程
temp_ok:
ret # 返回
如3.2.1节所述,标签是表示程序位置的“标记”。它们可以被指令和汇编命令引用,并在汇编和链接过程中被转换为地址。
GNU汇编器通常接受两种标签:符号标签和数字标签。符号标签作为符号存储在符号表中,通常用于标识全局变量和例程。它们标签名称 + 冒号(:)组成。标签名称的命名规则与前一节的符号名称相同。下面的代码包含两个符号标签:age:
和get_age:
。
age: .word 42
get_age:
la t1, age
lw a0, (t1)
ret
数字标签由一个十进制数字后跟一个冒号(:)定义。它们用于本地引用,不会包含在可执行文件的符号表中。此外,它们可以在同一个汇编器中被重复定义。
引用数字标签时需要一个后缀,表示该引用是位于引用之前(用b作为后缀)还是之后(用f作为后缀)的数字标签。下面的代码包含了数字标签及其引用的示例。这段代码有一个符号标签pow
和两个数字标签(都命名为1:
)。第一个数字标签位于第7行,表示属于循环的指令序列的开始。位于第11行的跳转指令跳回到这个标签。注意1b
这个引用,它指向的是位于当前位置之前的数字标签:1
。第二个数字标签(位于第12行)标记了位于循环之后的指令的位置,即执行流离开循环时必须执行的指令。当寄存器a1
的值等于0时,第8行的指令跳转到这个数字标签。注意1f
这个引用,它指向位于当前位置之后的数字标签1:
。
# Pow 函数 -- 计算 a^b
# 输入: a0=a, a1=b
# 输出: a0=a^b
pow:
mv a2, a0 # 将a0保存到a2
li a0, 1 # 将a0设为1
1:
beqz a1, 1f # 如果a1 = 0则退出
mul a0, a0, a2 # 否则相乘
addi a1, a1, -1 # 计数器减一
j 1b # 重复(跳转到前面的数字标签)
1:
ret
位置计数器是一个内部的汇编计数器,用于在程序组装时跟踪地址。更确切地说,它保存了下一个可用内存位置的地址。每个节都有自己的位置计数器,而 活跃位置计数器(active location counter) 就是 活跃节(active section)的位置计数器。 为了讨论位置计数器在整个汇编过程中是如何使用和更新的,我们将逐步汇编以下程序:
sum42:
addi a0, a0, 42
ret
在开始时,GNU汇编器清空节和符号表的内容,将所有位置计数器初始化为0,并选择.text
节作为 active section 。图4.1说明了内部汇编器数据结构在启动时的状态。
图4.1
一旦内部的数据结构初始化了,汇编器就按顺序逐行读入程序中的代码,根据标签、指令、汇编命令在程序中出现的顺序逐个去处理它们。
我们的汇编程序中的第一个元素是一个名为sum42:
的标签。在处理标签时,汇编器在符号表中将其注册为符号,并将其与表示程序当前位置的地址关联起来。当前位置由 active location counter 表示。图4.2说明了在汇编器处理sum42:
标签时符号表是如何更新的。请注意,汇编器将名称sum42
注册到符号表(❶),并将其关联到地址0(❷),即 active location counter 的地址。
我们汇编器中的下一个元素是汇编指令addi a0, a0, 42
。在这种情况下,汇编器将其转换为一条机器指令(❶),将其添加到 active section 的某个位置(❷),这个位置的地址存于 active location counter 中,最后更新 active location counter 使其指向下一个可用地址(❸)。在这种情况下,active location counter 的值增加4,因为添加到 active section 的RV32I指令需要4个内存字。图4.3说明了这个过程
图4.3
汇编程序中的最后一个元素也是汇编指令。同样,汇编器将其转换为一条机器指令(❶),将其添加到 active section 的某个位置(❷),这个位置的地址存于 active location counter 中,最后更新 active location counter 使其指向下一个可用地址(❸)。这个过程如图4 - 4所示。
图4.4
在处理完输入文件的所有元素之后,汇编器将节的内容、符号表和其他相关信息(如重定位记录)写到目标文件上。
汇编命令是用来控制汇编器的。例如,.section
.data
命令指示汇编程序将.data
节转换为 active section ,而.word 10
命令指示汇编器汇编一个32位的值(10)并将其添加到 active section 中 。
汇编命令通常编码为字符串,包含命令名称及其参数的。在GNU汇编器中,指令名以点(.
)开头。下面几节将讨论一些用于实现汇编程序的最常见指令。
表4.1包含了能向汇编程序中添加值的汇编命令
汇编命令名称 | 参数 | 描述 |
---|---|---|
.byte |
expression [, expression]* | 生成1个或多个8 bit的、用逗号隔开的的字(word) |
.half |
expression [, expression]* | 生成1个或多个16 bit的、用逗号隔开的的字(word) |
.word |
expression [, expression]* | 生成1个或多个32 bit的、用逗号隔开的的字(word) |
.dword |
expression [, expression]* | 生成1个或多个64 bit的、用逗号隔开的的字(word) |
.string |
字符串 | 生成用NULL 结尾的字符串 |
.asciz |
字符串 | 作用同.string 命令,是它的别名 |
.ascii |
字符串 | 生成字符串,但末尾不包含NULL
|
表4.1
表4.1中的所有命令都将值添加到 active section 。.byte
、.half
、.word
和.dword
命令向 active section 添加一个或多个值。它们的参数可以表示为立即数(4.3中讨论过立即数),符号(在汇编和链接过程中会被替换为它们的值),或由将两者结合起来的算术表达式。下面的代码展示了这些命令的有效参数示例。
.byte
向 active section 添加了4个8 bit的值(10、12、97和10).word
命令将与符号x
相关联的32 bit值添加到.string、。ascii和。ascii指令将字符串添加到活动部分。该字符串编码为一个字节序列,将在2.3节讨论。.string和。asciz指令还在字符串后面添加了一个值为0的额外字节。在向程序添加以空字符结尾的字符串时,它们很有用。
为了说明前面指令的用法,我们组装如下程序,它将值添加到。data部分:x
关联的值是分配给标签x:
的地址word
命令也向 active section 添加了一个32 bit的值,但在这里,这个值是在符号y
的值(即分配给标签y:
的地址)再加4x: .byte 10, 12, 'A', 5+5
y: .word x
z: .word y+4
i: .word 0
j: .word 1
.string
、.asciz
和.ascii
指令将字符串添加到 active section 。该字符串编码为一个字节序列(如2.3讨论的那样)。.string
和.asciz
命令还在字符串后面添加了一个值为0的额外字节。如果需要生成以NULL结尾的字符串时,这两个命令很有用。
为了说明前面命令的用法,我们汇编如下程序,将值添加到.data
节:
.section .data
msg: .ascii "hello"
x: .word 10
在4.6节讨论过,GNU汇编器首先清除节和符号表的内容,将所有位置计数器初始化为0,并选择.text
节作为active section。然后,它开始处理输入文件。输入文件中的第一个程序集元素是.section .data
命令,它指示汇编程序将.data
作为活动节。图4.5说明了这个过程。
图4.5
汇编程序中的下一个元素是msg:
标签。在这种情况下,汇编器在符号表中注册名为msg
的符号(❶),将其与表示程序当前位置的一个地址关联起来(❷),该地址由 active location counter 表示,即.data
节的位置计数器。图4.6说明了这个过程。
图4.6
汇编器中的下一个元素是.ascii "hello"
命令,它指示汇编程序向 active section 添加一个字符串。假设我们的输入文件是使用ASCII标准编码的,那么汇编器将字符串编码为基于ASCII标准的字节序列(❶),将这些字节添加到 active section 的下一个可用地址(❷),更新位置计数器(❸)。图4.7说明了这个过程
图4.7
程序中下一个元素是x:
标签。在这种情况下,汇编器在符号表中注册名为x
的符号,并将其与表示程序当前位置的地址关联起来,即关联到active location counter中的地址。
最后,程序中的最后一个元素是.word 10
命令,它指示汇编器向active section添加一个32位值。在这种情况下,汇编器将32位值编码为一个4字节的序列(❶),使用小端法将这些字节存储在active section上(❷),然后更新位置计数器(➌)。图4.8说明了这个过程。
图4.8
.section
命令如3.3节所讨论的,汇编程序、目标文件和可执行文件是按节组织的。另外,默认情况下,GNU汇编器将信息添加到.text
节。为了指示汇编器将已汇编的信息添加到其他节中,程序员(或编译器)可以使用.section secname
指令。在4.6节讨论过,在汇编程序时,汇编器对汇编程序中元素编码,并将其添加到 active section。.section secname
将 active section 更改为名称是secname
的节,因此在该命令之后,汇编器把处理的所有信息都添加到secname
节。
程序指令应该放在.text
节中,而常量,即只读数据,必须放在.rodata
节中。此外,初始化的全局变量必须放在.data
节,而未初始化的全局变量应该放在.bss
节。
下面的汇编代码展示了如何使用.section
命令将程序指令添加到.text
节,将程序变量添加到.data
和.rodata
节。
.section .text
set_x:
la t1, x
sw a0, (t1)
ret
get_msg:
la a0, msg
ret
.section .data
x: .word 10
.section .rodata
msg: .string "Assembly rocks!"
RV32I GNU 汇编器有
.text
、.data
和.bss
命令,分别是.section .text
、.section .data
和.section .bss
这三个命令的别名。
.bbs
上分配变量.bss
节专门存储未初始化的全局变量。这些变量需要在内存上分配,但它们不需要在程序执行时由加载器初始化。因此,其初始值不需要存储在可执行文件或目标文件中。
由于在对象和可执行文件中,.bss
节没有存储任何信息,因此GNU汇编程序不允许汇编程序向.bss
节添加数据。为了说明这种情况,让我们考虑以下代码,并假设它存储在名为data-on-bss.s
的文件中
.section .bss
x: .word 10
.section .text
set_x:
la t1, x
sw a0, (t1)
ret
这段代码试图使用.word 10
命令向.bss
节添加一个32位的值。但是在处理.word 10
指令时,GNU汇编器会停止汇编代码,并发出以下错误消息:
$ riscv64-unknown-elf-as -march=rv32im data-on-bss.s -o data-on-bss.o
data-on-bss.s: Assembler messages:
data-on-bss.s: Warning: end of file not at end of a line; newline inserted
data-on-bss.s:2: Error: attempt to store non-zero value in section `.bss'
要在.bss
节上分配变量,只需声明一个标识变量的标签,并按变量所需字节数向前移动.bss
的位置计数器,这样就可以在其他地址上分配更多的变量。
.skip N
命令将位置计数器向前移动N个单位,可用于为.bss
节中的变量分配空间。下面的代码展示了如何结合.skip
命令为三个不同的变量分配空间:x
、V
和y
。在这个例子中,程序为变量x
和y
分配了4字节,为变量V
分配了80字节。因此,标签x
、V
和y
的地址分别是:0x0、0x4和0x54。
.section .bss
x: .skip 4
V: .skip 80
y: .skip 4
注:有些系统在将程序加载到主存中执行时,用0初始化
.bss
节中的变量。尽管如此,程序员不应该假定.bss
节中的变量会初始化为0。
.set
和.equ
命令.set name, expression
命令在符号表中添加一个符号。这个命令接受name
和expression
(一个表达式)参数,计算expression
的值,并将name
和值存储到符号表中。下面的代码展示了如何使用.set
命令给符号赋值,并在程序中使用它们。在本例中,程序首先定义了名为max_value
的符号,并将其与值42关联起来(第1行)。然后在li
指令上使用max_value
(第4行)
.set max_value, 42
truncates_value_to_max:
li t1, max_value
ble a0, t1, ok
mv a0, t1
ok:
ret
.equ
的作用和.set
一样。
.global
命令如3.2.4节所述,符号可以分为局部符号和全局符号。默认情况下,标签自动创建的符号,或者由程序使用.set
或.equ
指令显式创建的符号,都作为局部符号存储在符号表中。.globl
命令可以用来将局部符号转换为全局符号。下面的代码展示了如何通过.globl
命令将start
和max_temp
符号转换为全局符号。
.globl max_value
.globl start
.set max_value, 42
start:
li a0, max_value
jal process_temp
ret
.align
命令一些ISA要求指令或多数据存储在一个给定数字的整数倍的地址上。例如RV32I ISA要求将指令存储在4的倍数的地址上。
GNU汇编器不会自动验证RV32I指令分配的地址是否为4的倍数。例如GNU汇编器在汇编以下代码时候不会出错。此时j next
指令存储在地址0x0、0x1、0x2和0x3, 值0xa
存储在地址0x4,而ret
指令存储在地址0x5、0x6、0x7和0x8。
.text
foo:
j next
.byte 0xa
next:
ret
尽管前一个程序是由汇编器汇编的,但RV32I CPU在执行ret
指令时会失败,因为它要求所有指令的地址都是4的倍数。
程序员(或编译器)负责保持RV32I指令是4字节对齐的,即地址是4的倍数。在在前面的例子中,可以在将8位的值添加到程序后,将位置计数器的值加3(第4行)。在这种情况下,RV32I CPU将正确执行以下代码
.text
foo:
j next
.byte 0xa
.skip 3 #让位置计数器的值增加3,这是一个很糟糕的、让位置计数器的值进行4字节对齐的方法
next:
ret
如前面的例子所示,手动跟踪位置计数器是一项繁琐的任务,可能会导致一些问题。例如,试图跟踪并手动更改位置计数器以确保RV32I指令对齐到4字节,可能会导致汇编器生成无效代码,但不会发出警告或错误消息。
确保位置计数器对齐的正确方法是使用.align N
命令。该指令检查位置计数器是否为 $2^N$ 的倍数,如果是,则对程序没有影响,否则,它将位置计数器的值增加到 $2^N$ 的整数倍。
编译器通常在例程标签之前插入一个.align 2
指令,以确保例程指令从4的倍数的地址开始。下列代码展示了一个汇编代码,它使用.align 2
指令在每个例程之前对齐位置计数器。
.text
.align 2
func1:
addi a0, a0, 2
ret
.align 2
func2:
addi a0, a0, 42
ret
请注意,如果位置计数器已经是 $2^N$ 的倍数,那么.align N
命令对位置计数器不起作用。由于前一个例子中的代码从地址0开始,并且每个汇编元素都是一条汇编指令,占用4字节,所以.align N
对程序没有影响。
RV32I ISA允许程序在非对齐的内存地址上加载和存储数据,但是,出于性能原因,RISC-V指令集手册建议将16位、32位和64位值分别存储在2、4和8的倍数的地址上。下面的代码展示了可以使用.align N
命令在内存中对齐多字节变量。
.data
.align 1
i: .half 1 # 16位的变量初始化为1
.align 2
x: .word 9 # 32位的变量初始化为9
.align 3
y: .dword 11 # 64位的变量初始化为11
.bss
.align 3
z: .skip 8 # 64位的变量(未初始化)
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。