• 沒有找到結果。

本章目标

2.7  子程序

在一个给定的程序中,常常需要对不同的数据多次执行同一个特定的任务。比较明智的 做法是把这个任务实现为一个指令块,每次要执行任务的时候就运行该指令块。这样的指令块 通常叫做子程序(subroutine)。例如,一个子程序可以求一个数学函数的值或者将一列数值按 照递增或递减的顺序排序。

虽然可以在程序中任何需要的地方复制构成子程序的指令块,但是为了节省空间,我们 只需在存储器中存放一份子程序的拷贝,当任何程序需要使用该子程序时,只要简单地跳转到 这个子程序的起始位置即可。当一个程序将控制转移到一个子程序时,我们称其为调用(call)

子程序。执行这个转移操作的指令叫做调用(Call)指令。

一个子程序被执行后,调用它的程序必须能够恢复执行紧跟在这条调用指令后面的指 令。我们将此称为子程序返回(return)到调用它的程序中,这可以通过在子程序中执行一条 Return 指令的方式来完成。因为子程序可能在一个调用程序的任何位置上被调用,所以必须准 备好返回到适当的位置上。调用程序重新恢复执行的位置是当执行Call 指令时被修改的程序 计数器(PC)所指出的位置。因此,为了能正确地返回到调用程序中,Call 指令必须保存 PC 中的内容。

这种能够在计算机中调用子程序并从子程序中返回的方法称为子程序链接(subroutine linkage)法。最简单的子程序链接法是将返回地址存储在一个指定的单元中,这个单元可以是 一个专门用于这种功能的寄存器,这种寄存器被称为链接寄存器(link register)。当子程序完 成了它的任务时,Return 指令通过链接寄存器的间接跳转返回到调用程序中。

Call 指令是一条特殊的转移指令,它执行以下操作:

将PC 中的内容存储到链接寄存器中。

转移到由Call 指令指定的目标地址中。

Return 指令也是一条特殊的转移指令,它执行的操作是:

转移到链接寄存器所保存的地址中。

图2-16 说明了 Call 和 Return 指令是如何影响 PC 和链接寄存器的。

Return Call

1000

204 204

Link PC

Return 1000

200

204 Call SUB

存储单元 调用程序 存储单元 子程序SUB

下一条指令

第一条指令

2-16 使用链接寄存器的子程序链接 56~

57

2.7.1 子程序嵌套及处理器堆栈

一个子程序调用另一个子程序叫做子程序嵌套(subroutine nesting)。在这种情况下,第二 个调用的返回地址也被存储在链接寄存器中,这会覆盖它原有的内容。因此,在调用其他子程 序之前,把链接寄存器中的内容存储到其他的单元里是十分重要的,否则第一个子程序的返回 地址将会丢失。

子程序的嵌套可以达到任何深度。最终,最后一个被调用的子程序完成它的计算并返回 到调用它的子程序中。第一次返回所需要的返回地址,是嵌套调用序列中最后一个生成的。也 就是说,返回地址的生成和使用是按照后进先出的顺序进行的。这意味着,与子程序调用有关 的返回地址应该被压入处理器栈中。

如果一个给定的子程序SUB1 在调用另一个子程序 SUB2 之前,把返回地址保存到栈中的 链接寄存器中(该链接寄存器是通过栈指针SP 访问的),那么就可以实现正确的嵌套调用顺 序。然后,在执行它自己的Return 指令之前,子程序 SUB1 必须从栈中弹出所保存的返回地 址,并将其装入链接寄存器中。

2.7.2 参数传递

当调用一个子程序时,程序必须要给子程序提供参数,也就是那些将在计算中使用的操 作数或是它们的地址。然后,子程序返回另外的一些参数,通常是计算的结果。这种在调用 程序和子程序之间的信息交换称为参数传递(parameter passing)。参数传递能够用多种方法实 现。参数可以放在寄存器或存储单元中,这些地方是能够被子程序访问到的。或者,可以将这 些参数放在处理器栈中。

通过处理器寄存器进行参数传递是简单有效的。图2-17 给出了图 2-8 中将一列数字相加 的程序是如何作为一个子程序(LISTADD),通过寄存器传递参数来实现的。存储在存储单元

Load R2, N Move R4, #NUM1 Call LISTADD Store R3, SUM

LISTADD: Subtract SP, SP, #4 Store R5, (SP) Clear R3 LOOP: Load R5, (R4)

Add R3, R3, R5 Add R4, R4, #4 Subtract R2, R2, #1 Branch_if_[R2]>0 LOOP Load R5, (SP) Add SP, SP, #4

n r u t e R ::

R5 的内容保存到栈中 将和初始化为0 获取下一个数 把这个数加到和中 将指针增加4 递减计数器 恢复R5 的内容 返回到调用程序 调用程序

子程序

参数1 为列表长度 参数2 为列表位置 调用子程序 保存结果

图2-17 将图 2-8 中的程序写成一个子程序;通过寄存器传递参数

58

N 中的列表大小 n 和第一个数的地址 NUM1 分别通过寄存器 R2 和 R4 传递。由子程序计算出 重写成的子程序LISTADD,这个子程序用处理器栈传递参数。该列表中第一个数的地址以及 列表中的项数被压入由寄存器SP 指向的处理器栈中。然后子程序被调用。在返回调用程序之 前,计算出的和被放到栈中。

Move R2, #NUM1 Subtract SP, SP, #4 Store R2, (SP) Load R2, N Subtract SP, SP, #4 Store R2, (SP) Call LISTADD Load R2, 4(SP) Store R2, SUM Add SP, SP, #8

LISTADD: Subtract SP, SP, #16 Store R2, 12(SP) Store R3, 8(SP) Store R4, 4(SP) Store R5, (SP) Load R2, 16(SP) Load R4, 20(SP) Clear R3 LOOP: Load R5, (R4)

Add R3, R3, R5 Add R4, R4, #4 Subtract R2, R2, #1 Branch_if_[R2]>0 LOOP Store R3, 20(SP) Load R5, (SP) Load R4, 4(SP) Load R3, 8(SP) Load R2, 12(SP) Add SP, SP, #16

图2-19 展示了这个例子中栈的表项。假设在子程序被调用之前,栈顶在第一级上。调用 程序将地址NUM1 和数值 n 压入栈中,并调用子

程序LISTADD。现在栈顶是在第二级上。子程序 在运行的时候使用了四个寄存器,因为这些寄存器 中可能包含属于调用程序的有效数据,所以在子程 序开始的时候,它们的内容应该被压入栈中进行保 存。现在栈顶是在第三级上。子程序使用变址寻址 方式从这个堆栈里访问参数 n 和NUM1,其中偏移 量的值是相对于新的栈顶(第三级)而言的。注意 这一操作不改变栈指针,因为有效的数据项仍然是 在这个栈的栈顶。数值 n 被装入R2 中作为计数的 初始值,并且地址NUM1 被装入 R4 中作为扫描这 个列表表项的一个指针。

在计算结束时,寄存器R3 中包含着计算出来

的和。在子程序返回到调用程序之前,R3 的内容被插入到这个栈中,替换了参数 NUM1,因 为这个参数不再需要了。然后子程序所使用的这四个寄存器的内容从栈中恢复。同样,增加栈 指针以指向子程序被调用时已存在的栈顶,即第二级中的参数 n。子程序返回后,调用程序将 结果存在单元SUM 中,再通过将 SP 增加 8 的方式来将栈顶降到它原来的级别上。

观察图2-18 中的子程序 LISTADD 可以发现,我们没有使用这对指令 Subtract SP, SP, #4

Store Rj, (SP)

去把每个寄存器的内容压到栈中。由于我们必须保存四个寄存器,所以需要八条指令。而通 过立即调整SP 使其指向四个寄存器都保存后即生效的栈顶,我们则只需要五条指令。然后,

用变址方式来保存寄存器的内容。在从子程序返回前恢复寄存器的时候,我们使用同样的优 化方式。

我们还应该注意到一些计算机有特殊的指令用于装载和保存多个寄存器。例如,可以使 用如下指令将图2-18 中的四个寄存器保存在栈中:

StoreMultiple R2-R5, -(SP)

源寄存器通过范围R2-R5 说明。记号 -(SP) 说明了栈指针必须相应地调整。前面的减号表明 在把每个寄存器的内容放到栈中之前,必须将SP 减 4。

同样,指令

LoadMultiple R2-R5, (SP)+

以相反的顺序将保存在栈中的值加载到寄存器R2、R3、R4 和 R5 中。记号 (SP)+ 表明在将 每个值装入相应的寄存器之后,必须将栈指针增加4。我们将在 2.9.1 节中详细讨论 -(SP) 和 (SP)+ 所表示的寻址方式。

按值和按引用的参数传递

注意在图2-17 和图 2-18 中两个参数 NUM1 和 n 的实际值被传递到了子程序中,子程 序的目的是对一个列表中的数字做加法。调用程序不传递实际的列表项,而传递这个列表中 第一个数的地址,这种技术叫做按引用传递(passing by reference)。第二个参数是按值传递

(passing by value),也就是将实际的表项数 n 传递到子程序中。

60~ 62 [R4]

[R5]

[R2]

[R3]

n NUM1 第三级

第二级

第一级

图2-19 图 2-18 程序中栈的内容

2.7.3 堆栈的结构

现在我们来看在图2-18 和 2-19 的例子中存储空间是如何作为栈来使用的。在子程序执行 期间,栈顶的6 个单元包含了子程序需要的条目。这些单元构成了子程序的一个私有工作空 间,该空间在进入子程序时分配,当子程序将控制交回调用程序时释放。这种空间叫做栈结构

(stack frame)。如果子程序需要更多的空间用于存储局部变量,也可以在栈中为这些变量分配 空间。

图2-20 给出了一个常用的栈结构中的信息布局示例。除了栈指针 SP 外,通常还需要另一 个指针寄存器:结构指针(frame pointer,FP),以便于访问传递到子程序中的参数,以及子程 序中使用的局部变量。在该图中,我们假设有四个参数被传递到子程序中,有三个局部变量要 在子程序内部使用,还有寄存器R2、R3 和 R4 需要进行保存,因为它们也要在子程序中使用。

就像我们会在下面的例子中看到的一样,当使用嵌套子程序的时候,调用程序的栈结构还将包 括返回地址。

如图2-20 所示,FP 寄存 器指向所存储参数正上方的 单元,我们可以使用变址寻 址方式很容易地访问这些参 数和局部变量。参数可以用 地址4(FP)、8(FP)、…访问。

局部变量可以用地址 -4(FP)、

-8(FP)、…访问,FP 中的内 容在子程序执行期间保持不 变, 这 一 点 不 同 于 栈 指 针 SP,SP 必须始终指向栈中当 前的栈顶元素。

现在我们来看看当调用 一个子程序,对它的栈结构 进 行 分 配、 使 用 和 释 放 时,

指 针SP 和 FP 是 如 何 操 作 的。 我 们 首 先 假 定SP 指 向 图2-20 中 原 来 的 栈 顶

(TOS)元素,在子程序被调用之前,调用程序将四个参数压入栈中。然后执行 Call 指令。这

(TOS)元素,在子程序被调用之前,调用程序将四个参数压入栈中。然后执行 Call 指令。这