本章讨论如何用汇编语言实现高级语言中的条件、循环语句,例如C/C++中的if-then
和if-then-else
语句,以及while
和for
循环。本章也讨论了函数的调用和返回。
下面的代码是一个用C语言编写的if-then
语句。在这段代码中,如果x
的值大于或等于10,就将x
的值赋给变量y
。if-then
语句包含两个主要部分:
- 条件(第1行)
- then块(第2~4行)
条件包含一个必须首先计算的布尔表达式,then块包含一组在布尔表达式为真时必须执行的语句
if (x >= 10)
{
y = x;
}
假设x
是一个有符号整数(int
),映射到寄存器a3
, y
映射到寄存器a4
,下面的代码展示了如何用汇编语言实现前面的C代码。首先,代码将常量10加载到临时寄存器t1
中(第1行)。然后,如果寄存器a3
(变量x
)的值小于10,则跳转到标签skip
,否则,执行下一条指令(第3行),这条指令对应于then块。
li t1, 10
blt a3, t1, skip # 如果x<10,跳转都skip标签
mv a4, a3 # 否则y = x
skip:
如果then块中有多个语句,它们可以放在blt
指令(第2行)和skip
标签(第4行)之间。
前面的代码使用blt
指令来检查变量x
的值是否小于10。如6.7.1节所述,这是正确的,因为变量x
是一个有符号变量。如果变量x
是一个无符号整数(C中的unsigned
),程序员(或编译器)必须使用bltu
指令来执行比较。
注意:重要的是要知道处理器不会自动推断寄存器(或内存)的内容是有符号值还是无符号值。程序员(或编译器)必须选定正确的指令让处理器进行比较。在前面的例子中,有符号变量比较用
blt
指令,无符号变量比较用bltu
指令。
下面的代码是一个用C编写的if-then-else
语句。在这段代码中,如果x
的值大于或等于10,则变量y
的值加1,否则将x
的值赋值给变量y
。if-then-else
语句包含三个主要部分:
- 条件(第1行)
- then块(第2~4行)
- else块(第6~8行)
条件 由布尔表达式组成,必须首先计算该表达式。then块 包含一组在布尔表达式求值为true时必须执行的语句,而 else块 包含一组在布尔表达式求值为false时必须执行的语句。
if (x >= 10) {
y = y + 1;
} else {
y = x;
}
假设x
是一个无符号整数(C中的unsigned
),存到寄存器a1
中, y
存到寄存器a2
中。下面的代码展示了如何用汇编语言实现前面的C代码。
- 首先,代码将常量10加载到临时寄存器
t3
中(第1行)。 - 然后,如果寄存器
a1
(变量x
)的内容小于10,它就跳转到标签else
来执行 else块 。 - 否则,它执行下一条指令(第3行),这条指令对应于then块 的第一条指令。
li t3, 10
bltu a1, t3, else # 如果x<10跳转到else标签
addi a2, a2, 1 # y = y + 1
j cont # 避开else标签
else:
mv a2, a1 # y = x
cont:
如果then块 中有多条语句,它们可以放在指令bltu
(第2行)和j cont
指令(第4行)之间。如果 else块 中有多条语句,它们可以放在标签else
(第5行)和标签cont
(第7行)之间。
在某些情况下,条件语句中的布尔表达式可能包含多个操作。下面的代码展示了一个包含多个操作布尔表达式。在这种情况下,只有当变量x
的内容大于或等于10且变量y
的内容小于20时,才能执行 then块。
if ((x>=10) && (y<20)){
x = y
}
假设x
和y
是一个有符号整型变量(C中的int
类型),分别映射到寄存器a1
和a2
,下面的代码展示了如何用汇编语言实现前面的C代码。
- 首先,代码将常量10加载到临时寄存器
t1
中(第1行)。 - 然后,如果寄存器
a1
(变量x
)的内容小于10,则跳转到标签skip
,即跳过then块的执行。注意,由于 与(and) 操作符(C中的&&
操作符),如果布尔表达式的第一部分为false,那么整个表达式就为false,因此无需检查第二部分。如果布尔表达式的第一部分(第1行和第2行)被求值为true,那么代码必须检查第二部分,即执行第3和4行中的指令。在本例中,如果变量y
的内容大于或等于20,则代码(第4行)跳过then块,跳转到skip
标签。否则,它执行下一条指令(第5行),它对应于then块的第一条指令。
li t1, 10
blt a1, t1, skip # 如果x>10调到skip标签
li t1, 20
bge a2, t1, skip # 如果y<=20调到skip标签
mv a1, a2 # x = y
skip:
下面的代码展示了一个包含 或 (C中的||
)操作符的布尔表达式的例子。
if ((x>=10) || (y<20)){
x = y
}
假设x
和y
是有符号整型变量(C中的int
类型),分别存到寄存器a1
和a2
,则下面的代码展示了如何用汇编语言实现前面的C代码。
- 首先,该代码将常量10加载到临时寄存器
t1
(第1行)。 - 然后,如果寄存器
a1
(变量x
)的内容大于或等于10,则跳转到标签then
执行then块。注意,由于或 操作符(C中的||
),如果布尔表达式的第一部分求值为true,那么整个表达式就为true,因此无需检查第二部分。如果布尔表达式的第一部分(第1行和第2行)计算结果为false,那么代码必须检查第二部分,即执行第3和4行中的指令。在这个例子中,如果变量y
的内容大于或等于20,那么代码(第4行)将跳过then块,跳转到skip
标签。否则执行下一个指令(第5行),该指令对应于C代码中的then块
li t1, 10
bge a1, t1, then # jumps to then if x >= 10
li t1, 20
bge a2, t1, skip # jumps to skip if y >= 20
then:
mv a1, a2 # x = y
skip:
嵌套的if-then
和if-then-else
是这样一种if语句:其中的 then块 或else块 包含其他if语句。下面的代码展示了一个嵌套if-then
语句的示例。注意,这里有两个if-then语句
:一个外部的(第18行)和一个内部的(第47行)。外部if-then
语句的 then块 包含两个语句:一个变量赋值语句(第3行)和内部的if-then
语句(第4~7行)。
if (x == 10)
{
x = 5;
if (y == 20)
{
x = 0;
}
}
从外部的if-then
语句开始,可以很容易地将前面的代码转换为汇编代码。假设x
和y
分别是映射到寄存器a1
和a2
的变量,下面的代码给出了外部if-then
语句的框架。
li t1, 10
bne a1, t1, skip # 如果x != 10,则跳转到skip标签
# <= 在这里插入then块的代码
skip:
当代码的框架生成后,下一步就是生成 then块 的代码,下面最终的代码:
li t1, 10
bne a1, t1, skip # 如果x != 10,则跳转到skip标签
li a1, 5 # x = 5
li t1, 20
bne a2, 20, skip_inner # 如果y != 20,则跳转到skip_inner
li a1, 0 # x = 0
skip_inner:
skip:
在前面的例子中,我们使用了两个不同的标签,一个跳过外部if语句的 then块 的执行(skip
标签),另一个跳过内部if语句的 then块 的执行(skip_inner
标签)。在这种情况下,由于两个标签表示相同的地址(注意,两个标签之间没有任何指令或数据),我们可以使用单个跳过标签来简化代码。但这种简化并不影响汇编程序生成的代码,因为标签仅仅是地址的标记。
循环语句用于重复一组语句。在本节中我们将讨论如何实现汇编中最常见的循环语句。
下面的代码展示了C语言中的while循环。while循环主要由两部分组成:
- 循环条件(第2行)
- 循环体(第3~6行)。
循环条件由一个布尔表达式组成,必须在每次while循环迭代之前,即每次执行 循环体 之前进行计算。如果 循环条件 的计算结果为true,则必须执行 循环体 。在这种情况下,在完成 循环体 的执行后,执行跳回到循环的开始,并再次计算 循环条件 。如果 循环条件 的计算结果为false,则循环执行结束,接着执行循环体后的代码。
int i=0;
while (i < 20)
{
y = y+3;
i = i+1;
}
假设变量i
和y
分别放在寄存器a1
和a2
,下面的代码展示了如何用汇编语言实现前面的C代码。
- 首先,它包含while循环之前的代码,在本例中是一条将常量零加载到寄存器
a1
的指令(第1行)。 - 然后,有一个定义循环的开始的标签(第2行)和检查循环条件的代码(第3行和第4行)。请注意,指令
bge
检查变量i
(寄存器a1
的值)是否大于或等于20。如果是,则跳转到标签skip
,离开循环。否则,继续执行下一条指令,也就是循环体的第一条指令。本例中的循环体由两条指令组成(第5行和第6行)。第一条实现了语句y=y+3
,第二条实现了语句i=i+1
。 - 在结束循环体的执行后,代码跳回到循环的开头(第7行),以便再次执行循环,即再次执行检查循环条件的指令。
li a1, 0 # i=0
while:
li t1, 20
bge a1, t1, skip # 如果 i>=20,跳转到skip标签退出循环
addi a2, a2, 3 # y = y+3
addi a1, a1, 1 # i = i+1
j while # 继续循环
skip:
下面的代码展示了一个用C语言编写的do-while循环。与while循环类似,do-while循环包含两个主要部分: 循环条件 (第6行)和 循环体 (第3-5行)。条件也由布尔表达式组成,但是对于do-while语句,条件必须在do-while循环的每次迭代之后计算,即在每次执行 循环体 之后。如果 循环条件 的计算结果为true,那么必须再次执行 循环体 。在这种情况下,在完成 循环体 的执行后,再次计算 循环条件 。如果 循环条件 的计算结果为false,则循环执行结束,将继续执行后续的代码。
int i=0;
do
{
y = y+2;
i = i+1;
} while (i < 10);
假设变量i
和y
分别存到寄存器a1
和a2
,下面的代码展示了如何用汇编语言实现前面的C代码。
- 首先,它包含while循环之前的代码——在本例中是一条将常量0加载到寄存器
a1
中的指令(第1行)。 - 然后,有一个标记循环开始的标签(第2行)和 循环体 ,它由两条指令(第5行和第6行)组成。第一个实现了语句
y=y+2
,第二个实现了语句i=i+1
。 - 在 循环体 之后,是检查 循环条件 的代码(第5行和第6行)。注意,
blt
指令检查变量i
(寄存器a1
的值)是否小于10。如果是,则跳回到标签dowhile
重复循环执行。否则,继续执行下一条指令,离开循环.
li a1, 0 # i=0
dowhile:
addi a2, a2, 2 # y = y+2
addi a1, a1, 1 # i = i+1
li t1, 10
blt a1, t1, dowhile # 如果i<10,跳回到dowhile标签
下面的代码展示了一个用C语言编写的for循环。for循环包含4个主要部分:
- 初始化代码(initialization code )
- 循环条件(loop condition)
- 更新代码(update code)
- 循环体(loop body)(第2~4行)
初始化代码(i=0
)必须在执行循环体之前执行一次。与while循环类似,循环条件(i<10
)由一个布尔表达式组成,必须在for循环的每次迭代之前,即在每次执行循环体之前计算它(第2~4行)。如果循环条件的计算结果为true,则必须执行循环体。在这种情况下,在执行完循环体后,会执行更新代码(i=i+1
),并跳转到循环的开始,以便再次计算循环条件。如果循环条件的计算结果为false,则循环执行结束,继续执行循环体后的代码。
for (i=0; i<10; i=i+1)
{
y = y+2;
}
假设变量i
和y
分别存到寄存器a1
和a2
,下面的代码展示了前面的C代码如何在汇编语言中实现。
- 首先,它包含 初始化代码 ,即
i=0
(第1行)。 - 然后,有一个标签定义循环的开始(第2行)和检查 循环条件 的代码(第3行和第4行)。请注意,
bge
指令检查变量i
(寄存器a1
的值)是否大于或等于10。如果是,则跳转到标签skip
,离开循环。否则,继续执行下一条指令,也就是 循环体 的第一条指令。这个例子中的 循环体 只有一条指令(第5行),它实现了语句y=y+2
。 - 更新代码 ,即
i=i+1
(第6行),放在 循环体 的后面。最后,在 更新代码 之后,代码跳回到循环的开始(第7行),以便可以再次执行循环,从再次验证 循环条件 开始。
li a1, 0 # i=0
for:
li t1, 10
bge a1, t1, skip # 如果i>=10,就跳转到skip,离开循环
addi a2, a2, 2 # y = y+2
addi a1, a1, 1 # i = i+1
j for
skip:
循环不变性代码(Loop-invariant code)总是生成相同的值,不需要每次循环时都重复执行。在下面的例子中,指令li t1, 10
一段循环不变性代码,不应该在每次循环时重复时执行。
li a1, 0 # i=0
for:
li t1, 10 # 如果i>=10则跳转
bge a1, t1, skip # 跳转到skip,离开循环
addi a2, a2, 2 # y = y+2
addi a1, a1, 1 # i = i+1
j for
skip:
在这种情况下,可以将指令提升(移动)到循环之前,以提高代码性能。这是一种被称为循环不变性代码移动(loop-invariant code motion, LICM)的优化,通常被编译器使用。下面展示了将LICM应用到前面的代码之后的结果。注意在这种情况下,每个循环重复只执行4条指令。
li a1, 0 # i=0
li t1, 10 # t1=10
for:
bge a1, t1, skip # 如果i>=10则跳转到skip,离开循环
addi a2, a2, 2 # y = y+2
addi a1, a1, 1 # i = i+1
j for
skip:
例程在汇编语言中由一个标签和一个代码片段定义。标签定义了例程的入口。标签的名称通常也是例程的名称,而代码片段包含实现该例程的指令。下面的代码展示了一个名为update_x
的例程的例子。这个例程将寄存器a0
的值更新到变量x
中,然后返回。
译注:例程是,【标签+指令片段】。之前的章节只是单独地介绍了标签和指令,没有例程这个概念
# 例程update_x
update_x:
la t1, x
sw a0, (t1)
ret
调用例程很简单,只需跳转到定义入口点的标签即可。但在调用(跳转到)例程之前,重要的是保存返回地址,以便例程在执行后可以返回调用点。
6.7.3节中讨论过,RV32I包含了一条特殊的跳转指令,用于在调用例程时保存返回地址。该指令,即jal lab
,被称为跳转和链接(jump and link),它将返回地址(即PC寄存器的值+4)存储在返回地址寄存器(ra
)中,然后跳转到lab
标签。
下面的代码片段展示了如何调用update_x
将变量x
的值更新为42。
- 首先,它将值42加载到寄存器
a0
中, - 然后使用跳转和链接(
jal
)指令调用update_x
.data
x: .skip 4
li a0, 42 # 将42加载到寄存器a0中
jal update_x # 调用 update_x
当update_x
例程执行完成后,它需要返回到调用的地方继续执行。这可以通过跳转到存储在ra
中的地址来实现,该地址是调用例程时由jal
指令设置的。伪指令ret
执行该操作(即跳转到调用例程的地方继续往下执行)。
注:如前所述,
jal
自动将返回地址存储在ra
中。该操作会破坏ra
之前的值,因此在调用例程之前,可能需要保存该寄存器的值,以便稍后恢复。在例程A中调用例程B时这一点特别重要,因为必须保存寄存器ra
的内容,以便A在自身执行完毕后能够返回它被调用的地方(继续往下执行)。第8章讨论了调用例程时如何保存和恢复ra
。
函数是一种可以接受参数,并返回一个或多个值的例程。 过程是一种可以接受参数,但不返回任何值的例程。
从函数返回值是一种惯例,通常由ABI定义。RISC-V ABI定义了函数必须将返回值存储在寄存器a0
上。8.3节会讨论ABI及其对软件组装(software composition )的重要性。
译注:以下代码使用了最新版(Ver May 31, 2024),而不是2022版,因为作者在最新版中更新了代码
下面的C语言代码展示了一个名为numbers
的全局数组和一个返回数组中最大值的函数
/* 全局数组 */
int numbers[10];
/* 返回数组中的最大值 */
int get_largest_number()
{
int largest = numbers[0];
for (int i=1; i<10; i++) {
if (numbers[i] > largest)
largest = numbers[i];
}
return largest;
}
.data
# 分配数组的内存空间 (10个整数需要40字节)
numbers: .skip 40
.text
get_largest_number:
la a5, numbers # a5 := &numbers
lw a0, (a5) # a0 := numbers[0] a0存放最大值
li a1, 1 # a1 := 1 a1存放i
li t4, 10
for:
bge a1, t4, end # 如果i>=10,调到end标签退出循环
slli t1, a1, 2 # t1 := i * 4
add t2, a5, t1 # t2 := &numbers + i*4
lw t3, (t2) # t3 := numbers[i]
blt t3, a0, skip # 如果numbers[i] < largest, 则跳转到skip标签
mv a0, t3 # 更新最大值
skip:
addi a1, a1, 1 # i := i + 1
j for
end:
ret
其它实现方案还有:
注:下面这个例子用到了RISCV汇编中的modifier,是属于汇编器as中依赖于RISCV的feature
get_largest_number:
lui a5,%hi(numbers)
lw a0,%lo(numbers)(a5)
addi a5,a5,%lo(numbers)
addi a3,a5,36
.L3:
lw a4,4(a5)
addi a5,a5,4
bge a0,a4,.L2
mv a0,a4
.L2:
bne a5,a3,.L3
ret
get_largest_number:
la a5, numbers # a5: 指向当前元素的地址
addi a6, a5, 40 # a6: 指向数组的末尾
lw a0, (a5) # a0:保存largest,初始化为指向数组首元素(number[0])
addi a5, a5, 4 # a5:指向下一个元素
do_while:
lw a4, (a5) # a4:加载当前元素(地址存在a5中)
bge a0, a4, skip # 如果largest >= current,跳转到skip
mv a0, a4 # 否则更新largest
skip:
addi a5, a5, 4 # a5指向下一个元素
bne a5, a6, do_while # 如果a5 != a6(即还没到数组末尾),跳转到do_while
ret