Skip to content

Latest commit

 

History

History
429 lines (319 loc) · 18.8 KB

File metadata and controls

429 lines (319 loc) · 18.8 KB

本章讨论如何用汇编语言实现高级语言中的条件、循环语句,例如C/C++中的if-thenif-then-else语句,以及whilefor循环。本章也讨论了函数的调用和返回。

7.1 条件语句

7.1.1 if-then语句

下面的代码是一个用C语言编写的if-then语句。在这段代码中,如果x的值大于或等于10,就将x的值赋给变量yif-then语句包含两个主要部分:

  • 条件(第1行)
  • then块(第2~4行)

条件包含一个必须首先计算的布尔表达式,then块包含一组在布尔表达式为真时必须执行的语句

if (x >= 10)
{
    y = x;
}

假设x是一个有符号整数(int),映射到寄存器a3y映射到寄存器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行)之间。

7.1.2 比较符号数 vs 比较无符号数

前面的代码使用blt指令来检查变量x的值是否小于10。如6.7.1节所述,这是正确的,因为变量x是一个有符号变量。如果变量x是一个无符号整数(C中的unsigned),程序员(或编译器)必须使用bltu指令来执行比较。

注意:重要的是要知道处理器不会自动推断寄存器(或内存)的内容是有符号值还是无符号值。程序员(或编译器)必须选定正确的指令让处理器进行比较。在前面的例子中,有符号变量比较用blt指令,无符号变量比较用bltu指令。

7.1.3 if-then-else语句

下面的代码是一个用C编写的if-then-else语句。在这段代码中,如果x的值大于或等于10,则变量y的值加1,否则将x的值赋值给变量yif-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代码。

  1. 首先,代码将常量10加载到临时寄存器t3中(第1行)。
  2. 然后,如果寄存器a1(变量x)的内容小于10,它就跳转到标签else来执行 else块
  3. 否则,它执行下一条指令(第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行)之间。

7.1.4 处理非平凡的布尔表达式

在某些情况下,条件语句中的布尔表达式可能包含多个操作。下面的代码展示了一个包含多个操作布尔表达式。在这种情况下,只有当变量x的内容大于或等于10且变量y的内容小于20时,才能执行 then块

if ((x>=10) && (y<20)){
    x = y
}

假设xy是一个有符号整型变量(C中的int类型),分别映射到寄存器a1a2,下面的代码展示了如何用汇编语言实现前面的C代码。

  1. 首先,代码将常量10加载到临时寄存器t1中(第1行)。
  2. 然后,如果寄存器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
}

假设xy是有符号整型变量(C中的int类型),分别存到寄存器a1a2,则下面的代码展示了如何用汇编语言实现前面的C代码。

  1. 首先,该代码将常量10加载到临时寄存器t1(第1行)。
  2. 然后,如果寄存器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:

7.1.5 嵌套的if语句

嵌套的if-thenif-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语句开始,可以很容易地将前面的代码转换为汇编代码。假设xy分别是映射到寄存器a1a2的变量,下面的代码给出了外部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标签)。在这种情况下,由于两个标签表示相同的地址(注意,两个标签之间没有任何指令或数据),我们可以使用单个跳过标签来简化代码。但这种简化并不影响汇编程序生成的代码,因为标签仅仅是地址的标记。

7.2 循环语句

循环语句用于重复一组语句。在本节中我们将讨论如何实现汇编中最常见的循环语句。

7.2.1 while循环

下面的代码展示了C语言中的while循环。while循环主要由两部分组成:

  • 循环条件(第2行)
  • 循环体(第3~6行)。

循环条件由一个布尔表达式组成,必须在每次while循环迭代之前,即每次执行 循环体 之前进行计算。如果 循环条件 的计算结果为true,则必须执行 循环体 。在这种情况下,在完成 循环体 的执行后,执行跳回到循环的开始,并再次计算 循环条件 。如果 循环条件 的计算结果为false,则循环执行结束,接着执行循环体后的代码。

int i=0;
while (i < 20)
{
    y = y+3;
    i = i+1;
}

假设变量iy分别放在寄存器a1a2,下面的代码展示了如何用汇编语言实现前面的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:

7.2.2 do-while循环

下面的代码展示了一个用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);

假设变量iy分别存到寄存器a1a2,下面的代码展示了如何用汇编语言实现前面的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标签

7.2.3 for循环

下面的代码展示了一个用C语言编写的for循环。for循环包含4个主要部分:

  1. 初始化代码(initialization code )
  2. 循环条件(loop condition)
  3. 更新代码(update code)
  4. 循环体(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;
}

假设变量iy分别存到寄存器a1a2,下面的代码展示了前面的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:

7.2.4 提升循环不变性代码

循环不变性代码(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:

7.3 调用例程(Routines)以及从例程中返回

例程在汇编语言中由一个标签和一个代码片段定义。标签定义了例程的入口。标签的名称通常也是例程的名称,而代码片段包含实现该例程的指令。下面的代码展示了一个名为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。

  1. 首先,它将值42加载到寄存器a0中,
  2. 然后使用跳转和链接(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

7.3.1 从函数中返回值

函数是一种可以接受参数,并返回一个或多个值的例程。 过程是一种可以接受参数,但不返回任何值的例程。

从函数返回值是一种惯例,通常由ABI定义。RISC-V ABI定义了函数必须将返回值存储在寄存器a0上。8.3节会讨论ABI及其对软件组装(software composition )的重要性。

7.4 示例

7.4.1 查找数组中的最大值

译注:以下代码使用了最新版(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