1、第九章 自陷程序和子程序9.1 LC-3 自陷程序9.1.1 介绍回忆先前一章的图 8.5 的程序,为了成功地获得从键盘输入,程序员需要知道以下几件事情(第 8 章):1、键盘与显示器的硬件数据寄存器:显示器的数据寄存器是为了能够显示一个提示符,而键盘数据寄存器则是让程序知道到哪儿去寻找输入的字符。2、键盘与显示器的硬件状态寄存器:显示器的状态寄存器是为了让程序知道什么时候可以显示提示符中的下一个字符,而键盘状态寄存器则让程序知道什么时候有人键入了一个字符。3、键盘输入和执行程序之间的异步关系。这是大多数应用程序员不知道的知识。实际上,在现实中,如果应用程序员(或有时称为“用户程序员” )必须
2、在这个层面上理解输入与输出,那么在商业上将会很少运用输入/输出,程序员也会大大减少。另外,如果允许用户程序员直接访问 KBDR 和 KBSR 等来实现输入/输出的行为,将会造成另外一个问题出现。输入/输出行为包含了被许多程序所共享的设备寄存器的使用,这就意味着,如果一个用户程序员被允许访问硬件寄存器,他/她没有谨慎处理,这会给其他用户程序制造混乱。这样,让用户程序员访问这些寄存器是不明智的。我们说硬件寄存器是有特权的,那些不拥有适当特权级别的程序是不能访问它们的。特权的概念带来了一大堆麻烦。不幸的是,我们在这里不能做更多的涉及,把它留给以后做更认真的处理。现在,我们只是注意到这里有用户程序不能
3、访问的资源,只有那些被赋予足够特权的程序才可以控制它们,而没有特权的程序则不可以。说完这些,我们继续手头上的问题,如何“更好”的解决需要输入和/或输出的用户程序。一个更简单同时也是更安全的解决需要 I/O 的用户程序问题的方案包括自陷(TRAP)指令和操作系统。操作系统拥有适当的特权级别。我们已经在第 5 章介绍了 TRAP 指令。我们看到,在某些任务中,用户程序通过调用TRAP 指令使操作系统做这个工作。这样,用户程序不必要知道前面提到的复杂的细节,并且其他用户程序也会被保护起来,避免用户程序员的不恰当行为的后果。图 9.1 显示了一个用户程序在到达地址 x4000 时,需要执行一个输入输出
4、任务。用户程序请求操作系统代表它完成这个任务。操作系统控制机器,处理 TRAP 指令指定的请求,然后把控制权返还给用户程序。我们经常把这个用户程序的请求称为服务调用或系统调用。9.1.2 TRAP 机制TRAP 机制包括一些要素,如下:1、 一组由操作系统代表用户程序去执行的服务程序。它们是操作系统的一部分,在存储器中的起始地址是任意的。LC-3 被设计为总共可以识别 256 个服务程序。附录 A 中的表 A.2 包含了 LC-3 现有的操作系统服务程序的完整列表。2、 这 256 个服务程序的起始地址的一张表。这张表被存储在存储单元的 x0000 到x00FF 中。不同的公司对这张表有不同的
5、命名,一家公司叫它系统控制块,另一家公司叫它 Trap 向量表。图 9.2 提供了一个 LC-3 的 Trap 向量表的瞬态图。在这些起始地址中,字符输出服务程序(单元 x0430)包含在单元 x0021 中;键盘输入服务程序(单元 x04A0)包含在单元 x0023 中,还有,停机服务程序(单元 xFD70)包含在单元 x0025 中。3、TRAP 指令。当用户程序希望让操作系统代表用户程序执行某一个服务程序,然后把控制权交还给用户程序时,用户程序使用 TRAP 指令。4、返回用户程序的一个链接。服务程序必须有一种可以把控制权交还给用户程序的机制。9.1.3 TRAP 指令TRAP 指令通过
6、做两件事实现服务程序的执行: 它根据它的 trap 向量,改变 PC 的值为相应的服务程序的首地址。 它提供了一个返回调用 TRAP 指令的程序的路径。这个返回路径指的是链接。TRAP 指令说明如下。TRAP 指令由两部分组成:TRAP 的操作码 1111 和 trap 向量(7:0位) 。11:8 位必须为 0。trap 向量标识了用户程序希望操作系统执行的程序。在下面的例子中,trap 向量是 x23。15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 01 1 1 1 0 0 0 0 0 0 1 0 0 0 1 1TRAP TRAP 向量在 TRAP 指令的指令周期的
7、执行阶段,做 4 件事:1、 8 位的 trap 向量通过零扩展到 16 位而形成一个地址,该地址被加载到 MAR。对于 trap 向量 x23,地址就是 x0023,它是 TRAP 向量表中的一条记录的地址;2、 TRAP 向量表位于存储单元 x0000 到 x00FF 中。位于 x0023 中的纪录被读取,它的内容是 x04A0(如图 9.2) ,被加载到 MDR 中;3、 通用寄存器 R7 被加载为 PC 中的当前内容。这会给用户程序提供一个返回路径,这一点马上就会变得更清楚;4、 MDR 的内容被加载到 PC 中,完成这个指令周期。由于 PC 现在包含了 x04A0,所以处理从存储单元
8、 x04A0 继续下去。地址 x04A0 是从键盘输入一个字符的操作系统服务程序的起始地址。我们说 trap 向量指向 TRAP 程序的起始地址。因此,TRAP x23 使操作系统开始执行键盘输入服务程序。为了返回到位于用户程序中的 TRAP 指令之后的指令(在执行完服务程序之后) ,必须有一个机制保存用户程序的下一条指令的地址。在上面列出的执行阶段的第 3 步提供了这个链接。在把服务程序的起始地址加载到 PC 之前,通过在 R7 中保存 PC 的值,TRAP 指令为服务程序提供了将控制返回到用户程序的适当单元的全部信息。你知道,PC 已经被更新(在 TRAP 指令的取指令阶段) ,指向下一条
9、指令。这样,TRAP 服务程序开始执行,R7包含了紧跟在 TRAP 指令之后的用户程序指令的地址。9.1.4 完整的机制我们已经详细的显示了 TRAP 指令如何调用服务程序来实现程序的命令,我们也显示了 TRAP 指令如何提供服务程序需要的将控制返回到用户程序的正确位置的信息。剩下的唯一的事就是显示将控制返回到用户程序的正确位置的服务程序中的真正的指令。回忆第5 章的 JMP 指令。假设在 TRAP 服务程序执行的过程中, R7 的内容未被改变。如果是那样的话,通过在 TRAP 服务程序的最后执行一条 JMP R7 指令,控制就可以返回到用户程序的正确位置。图 9.3 显示了 LC-3 使用
10、TRAP 和 JMP 指令来完成图 9.1 的例子。控制流从一个需要从键盘输入字符的用户程序开始(A ) ,到代表用户程序执行此任务的操作系统服务程序( B) ,再返回到可能会使用在输入字符中包含的信息的用户程序(C) 。回忆计算机继续执行指令周期(取指令,译码等) ,正如你所知道的,改变控制流的方式就是在当前指令的执行阶段,改变 PC 中的内容。这样,下一次取指令阶段读取的将是改变了方向的地址。因此,要调用字符输入服务程序,我们调用使用了 trap 向量 x23 的 TRAP 指令。该指令的执行使存储单元 x0023 的内容(在这个例子里包含了 x04A0)被加载进 PC,同时紧跟在 TRA
11、P 指令之后的指令地址被加载到 R7 中。图 9.3 中的虚线显示了使用 trap 向量从 trap向量表中获得 TRAP 服务程序的起始地址。下一个指令周期从取得 x04A0 的内容开始,也就是请求(接受)键盘输入的操作系统服务程序的第一条指令。我们马上就看到的那段服务程序,跟我们在 8.4 节中学过的键盘输入程序是一样的。回忆那一段完整的输入程序(图 8.5) ,R0 包含了那个键入的键的ASCII 码。TRAP 服务程序以 JMP R7 指令结束。JMP R7 指令的执行,将 PC 加载为 R7 的内容。如果 R7 在执行服务程序期间没有改变,它将会包含原始的用户程序中紧跟着 TRAP
12、指令之后的那条指令的地址。因此,用户程序继续执行,且 R0 包含了从键盘输入的字符的ASCII 码。JMP R7 指令对于返回用户程序提供了如此的便利,以至 LC-3 汇编程序为这条指令提供了一个助记符 RET,如下所示:15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 01 1 0 0 0 0 0 1 1 1 0 0 0 0 0 0RET下面的程序被用来说明 TRAP 指令的使用。它也可以被用来逗一个 4 岁的孩子玩!例 9.1写一个做如下事情的游戏程序:一个人坐在键盘前,每次这个人键入一个大写字符,程序输出该字符的小写。如果此人键入了一个 7,程序结束。下面的 LC-
13、3 汇编程序作了该工作。01 .ORIG x3000 02 LD R2, TERM ; 加载 -703 LD R3,ASC ; 加载 ASCII 码的差值04 AGAIN TRAP x23 ; 请求键盘输入05 ADD R1, R2, R0 ; 测试是否是终止字符06 BRz EXIT 07 ADD R0, R0, R3 ; 改变为小写字母08 TRAP x21 ; 输出到显示器09 BRnzp AGAIN ; 再做一次0A TERM .FILL xFFC9 ; xFFC9 是 7 的 ASCII 码的负数0B ASC .FILL x0020 0C EXIT TRAP x25 ; 停机0D .
14、END程序执行如下。首先它分别将 xFFC9 和 x0020 读入 R2 和 R3。常数 xFFC9 是 7 的ASCII 码的负数,它是用来测试键盘上输入的字符,看这个四岁的孩子是否想继续玩。常数 x0020 是一个大写字母与其小写表示的 ASCII 码之间的差的零扩展。例如,A 的 ASCII码是 x41 而 a 的 ASCII 码是 x61。Z 和 z 的 ASCII 码分别是 x5A 和 x7A。然后执行 TRAP x23,它调用键盘输入服务程序。当服务程序结束,控制返回应用程序(在 05 行)时,R0 包含了键入字符的 ASCII 码。ADD 和 BRz 指令来测试是否是终止字符 7
15、。如果键入的字符不是 7,大小写的 ASCII 码的差(x0020)将被加到键入字符的ASCII 码上,并将结果保存到 R0。然后调用显示器输出服务程序,这使得同一字母的小写被显示到屏幕上。当控制返回应用程序(这次在 09 行)时,将执行无条件分支转移到AGAIN,请求另一个键盘输入。在这个例子中,程序的正确运行是假设坐在键盘前的人只输入大写字母和 7。但如果他输入“$”呢?例子 9.1 的一个更好的解决方案是能够测试输入的字符,确定它是否是字母表中 26 个字母的大写形式,如果不是,则采取适当的操作。问题:将测试错误数据增加到上面的程序中。也就是说,写一段可以将键入的任意大写字母以小写表示,
16、并且如果输入字符不是大写字母,则终止执行的程序(习题 9.6) 。9.1.5 处理 I/O 的 TRAP 程序利用上面提供的概念,图 8.5 描述的输入程序可以做稍微修改,成为图 9.4 的输入程序。需要两处改变:(1)我们添加了.ORIG 和.END 伪操作。.ORIG 说明了输入服务程序的起始地址在 trap 向量表的地址 x0023 中发现的地址, (2)我们使用 JMP R7(助记符RET)结束这个输入服务程序,而不是使用图 8.5 的 20 行的 BR NEXT_TASK。因为这个服务程序通过 trap x23 调用,所以使用 JMP R7。这不是用户程序的一部分,与图 8.5 的情
17、况相同。8.3.2 节的输出程序可被做类似的修改,如图 9.5 所示。结果就是使用恰当的 TRAP 向量的 TRAP 指令,可以简单、安全的调用输入(图 9.4)和输出(图 9.5)服务程序。在输入的情况下,在完成 TRAP x23 后, R0 中包含了从键盘输入的字符的 ASCII 码。在输出的情况下,初始程序必须将要想在显示器上显示的字符的 ASCII 码加载入 R0,然后再调用TRAP x21。9.1.6 停机的 TRAP 程序回想一下,在 4.5 节中,运行锁通过与石英振荡器做“与”操作,产生用来控制计算机运行的时种。我们注意到如果那个 1 位的锁被清空,那么与门的输出就是 0,这样就
18、停止了时钟。多年前,大多数的 ISA 都有一条 HALT 指令来停止时钟。已知此命令被执行的频率非常低,看起来为它提供一个操作码很浪费。在很多现代计算机中,运行锁的清空是用TRAP 程序来实现的。在 LC-3 中,运行锁是机器控制寄存器的15位,机器控制寄存器被存储映射到单元 xFFFE。图 9.6 显示了停止处理器的 TRAP 服务程序,也就是停止时钟的程序。首先(02,03 和 04 行) ,寄存器 R7、R1 和 R0 都被保存。R0 和 R1 被保存是因为它们在服务程序中要被用到,R7 被保存是因为它的内容会在执行 TRAP x21(09 行)后被覆盖。然后(08 行到 0D 行) ,
19、 “Halting the machine”(停止机器)这个标识被显示在显示器上。最后(11 行到 14 行) ,运行锁(MCR15)通过对 MCR 与 0111111111111111 做“与”操作而被清空。也就是说 MCR14:0保持不变,但是 MCR15被清空。问题:什么指令(或是 TRAP 服务程序)可以使时钟开始?01 .ORIG xFD70 ; 程序的位置02 ST R7, SaveR7 03 ST R1, SaveR1 ; R1:临时存储 MCR 的值04 ST R0, SaveR0 ; R0 用作工作空间05 ;06 ; 输出停机的消息07 ;08 LD R0,ASCIINew
20、Line09 TRAP x210A LEA R0, Message0B TRAP x220C LD R0,ASCIINewLline0D TRAP x210E ;0F ; 清空 xFFFE 的 15 位,停机10 ;11 LDI R1, MCR ; 加载 MCR 的值到 R1 中12 LD R0,MASK ; R0=x7FFF13 AND R0, R1, R0 ; 用屏蔽清空最高位14 STI R0, MCR ; 将 R0 的值存储到 MCR 中15 ;16 ;从 HALT 程序返回17 ;18 ;19 LD R1,SaveR1 ;恢复寄存器1A LD R0, SaveR01B LD R7,
21、SaveR7 1C RET1D ;1E ; 一些常数1F ;20 ASCIINewLine .FILL x000A21 SaveR0 .BLKW 122 SaveR1 .BLKW 123 SaveR7 .BLKW 124 Message .STRINGZ “Halting the machine. “25 MCR .FILL xFFFE ; MCR 的地址26 MASK .FILL x7FFF ; 清空最高位的屏蔽27 .END图 9.6 LC-3 的停机服务程序9.1.7 寄存器的保存和恢复我们应该更加明确强调的我们曾经在过去提到的一点,就是需要保存寄存器中的值,1、 如果该值将被某个接下来
22、的行为破坏,并且2、 如果当完成那个接下来的行为后,我们还需要用到那个值。假设我们想从键盘输入十个十进制数字,将它们的 ASCII 码转化成二进制表示,并且将这十个二进制数存进以地址“Binary”开头的十个连续的存储单元。下面的程序片段做个这个工作。01 LEA R3,Binary ;初始化为第一个单元02 LD R6,ASCII ;05 行用到的模板03 LD R7,COUNT ;初始化为 1004 AGAIN TRAP x23 ;获取键盘输入05 ADD R0,R0,R6 ;去掉 ASCII 码模板06 STR R0,R3,#0 ;存储二进制数07 ADD R3,R3,#1 ;指针加 1
23、08 ADD R7,R7,#-1 ;计数器减 109 BRp AGAIN ;还有更多字符?0A BRnzp NEXT_TASK ;0B ASCII .FILL xFFD0 ;x0030 的负数0C COUNT .FILL #100D Binary .BLKW #10这个程序片段的第一步是初始化。我们用为存储这 10 个十进制数而分配的存储器空间的起始地址来加载 R3。我们将 ASCII 码模板的负数加载 R6,这是为了从每一个 ASCII 码减去 x0030。我们把计数器的初始值 10 存入 R7。然后执行十次循环,每次从键盘上取一个字符,去除 ASCII 码模板,储存二进制结果,并检验我们是
24、否做完。但是这段程序不能运行!为什么?答案:04 行的 TRAP 指令将 03 行里加载到 R7 的值“10”替换为 ADD R0,R0,R6 指令的地址。所以 08 和 09 行的指令没有执行被安排做的循环控制功能。给我们的信息就是:如果一个寄存器内的值将在寄存器被存储了其他值之后再次用到,我们必须在其他事情发生之前将其保存,在我们接下来用它之前将其恢复。我们通过将寄存器存进存储器,而保存它的值;通过把它加载回寄存器而恢复它的值。在图 9.6 中,03行包含了用来保存 R1 的 ST 命令,11 行包含了将实施 TRAP 服务程序工作的值加载到 R1的 LDI 指令, 19 行包含了将 R1
25、 恢复为在服务程序被调用之前的初始值的 LD 指令,22 行则为保存 R1,而在存储器中留出一个单元。保存/恢复问题可以通过:在 TRAP 执行之前由调用程序处理,或者在 TRAP 指令执行之后由被调用的程序(例如,服务程序)处理。我们将在 9.2 节中看到在另外一类调用/被调用程序中存在同样的问题,子程序机制。如果由调用程序处理这个问题,我们用术语 caller-save(调用者保存) 。如果由被调用的程序处理这个问题,我们用术语 callee-save(被调用者保存) 。哪个程序知道哪些寄存器将被接下来的操作所破坏,适合处理这个问题的就是哪一个。被调用程序知道需要哪些寄存器来完成它的工作。
26、因而在它开始执行之前,它会用一系列存储指令保存这些寄存器中的值。在它完成以后,它又会用一系列的加载指令恢复那些寄存器的值。而且它还预留了一些存储单元来保存这些寄存器中的数据。在图 9.6 中,HALT 程序需要使用 R0 和 R1。因此它在 03 行和 04 行用 ST 指令将它们的值都保留下来,而在 19 行和 1A 行又用 LD 指令将它们的值恢复,它在 21 行和 22 行为这些值预留了存储单元。调用程序知道它控制下的指令将造成什么样的破坏。让我们再来看图 9.6,调用程序知道每一种 TRAP 指令都会破坏 R7 中的数据。因此在 HALT 服务程序中的第一条 TRAP 指令被执行以前,
27、它将保存 R7,当 HALT 服务程序中的最后一条 TRAP 指令执行完毕后,R7 被恢复。9.2 子程序我们刚才已经看到了,如果程序员不需要去学习 I/O 硬件的细节,而能够依赖于操作系统所提供的程序片段来完成这些任务,他们的效率将能够大大的得到提高。而且我们在过去也提到,使操作系统来访问那些设备寄存器,而使我们不必受其他程序员的支配是一件非常美妙的事情。我们看到在用户的程序中通过调用 TRAP 指令来请求服务程序,从而使操作系统来处理。通过 JMP R7 指令重新回到原来的程序。类似的,能够在同一程序内多次调用一个程序段,而每次需要时不必说明其源程序的全部细节,通常是很有用的。另外,有时一
28、个人写一个需要程序片断的程序,而另一个人写那些程序片断。一个人也可能需要一段由厂商或某个独立的软件供应商提供的片断。通常那些提供的程序片断的集合让用户程序员不用自己去写。这个集合被称作库。例如数学库,包括执行诸如平方根、正弦、余切等函数的片断。因为这些原因,提供一个使用程序片断的方法是有益的。那些程序片断被称为子程序,或者换句话说是程序,或用 C 的术语是函数。使用它们的机制被称为调用/ 返回机制。9.2.1 调用/返回机制图 9.4 提供了一个在同一程序内多次执行一个程序片断的简单图解。注意起始于符号地址 L1 的 3 条指令,还要注意起始于地址 L2、L3 和 L4 的 3 条指令,这 4
29、 个 3 条指令的序列,每一个都做如下工作:LABEL LDI R3, DSRBRzp LABELSTI Reg, DDR这 4 个程序片断中,有两个存储 R0 的内容,另外两个存储 R2 的内容,但是这点很容易解决,我们稍后就会看到。关键是,除了被用作 STI 指令的源寄存器是哪个有点小麻烦外,这 4 个程序片断做的是相同的工作。调用/返回机制允许我们把这个 3 条指令的序列作为一个子程序,子程序在我们的程序中只需包括一次,就多次执行这个指令序列。调用机制计算子程序的起始地址,加载到 PC,保存返回地址以便返回调用程序的下一条指令。返回机制使用返回地址加载 PC。图 9.7 显示了在程序中使
30、用子程序和不使用子程序的指令执行流程。调用/返回机制与 TRAP 指令在将控制重定向到一个程序片断,同时保存返回到调用程序的链接这一点上,表现十分相似。二者都是,将 PC 加载为程序片断的起始地址,同时R7 被加载为需要返回到调用程序的地址。程序片断的最后一条指令,无论是 TRAP 服务程序还是子程序程序片断,都是 JMP R7 指令,它将 PC 加载为 R7 的内容,因此将控制返回到跟在调用程序后面的一条指令。在子程序和被 TRAP 指令调用的服务程序之间有一点重要的不同之处。尽管这有点超出本课程的范围,我们将做个简单的介绍。这点与程序片断被要求做的工作的本质有关。在 TRAP 指令的情况下
31、(我们曾经看到的) ,服务程序包括操作系统资源,它们通常需要访问计算机的底层硬件的特权;它们是由管理计算机资源的系统程序员写的。在子程序的情况下,它们要么是由写包含了调用指令的程序的相同程序员所写,要么是由某个同事所写,或者是被某个库的一部分所提供。而这两种情况所包括的资源都是不可以干扰其他人的程序的,所以它们可以成为某个用户程序的一部分。9.2.2 JSR(R) 指令LC-3 规定了一个调用子程序的操作码,0100。该指令使用两种寻址模式,PC 相对寻址,或基址寻址之一,来计算子程序的起始地址。取决于使用哪个寻址模式,LC-3 汇编语言为此操作码提供了两个不同的助记符,JSR 和 JSRR。
32、该指令做两件事:在 R7 中保存返回地址,计算子程序的起始地址并加载到 PC。返回地址是增加了 1 的 PC,它指向跟在调用程序中的 JSR 或 JSRR 指令后的一条指令。JSR(R)指令由三部分组成。15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0操作码 A 计算地址位15:12位包含操作码,0100。11位表示寻址模式,如果寻址模式是 PC 相对寻址,其值为 1;如果寻址模式是基址寻址,其值为 0。10:0位包含被用作计算子程序的起始地址的信息。JSR 和 JSRR 的唯一区别就是用作计算子程序的起始地址的寻址模式。JSRJSR 指令通过将指令的 11 位的偏移
33、量(10:0位)做符号扩展,并与增加了 1 的 PC 相加,从而计算出子程序的目标地址。这种寻址模式与 LD 和 ST 指令的寻址模式几乎相同,除了 PC 偏移量使用的是 11 位,而不是像 LD 和 ST 那样使用 9 位。如果下面这条 JSR 指令被存储在地址 x4200,那它的执行会使 PC 加载为 x3E05,R7中则加载 Xx4201。15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 00 1 0 0 1 1 0 0 0 0 0 0 0 1 0 0JSR A PCoffset11JSRRJSRR 指令和 JSR 除了寻址模式不同外,其他完全相同。JSRR 获得子
34、程序的起始地址的方式与 JMP 指令完全相同,也就是说,它使用由指令的8:6位说明的寄存器中的内容。如果下面这条 JSRR 指令被存储在地址 x420A,并且如果 R5 包含 x3002,那么 JSRR指令的执行会使 R7 加载为 x420B,PC 中则加载 x3002。15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 00 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0JSRR A BaseR问题:什么是 JMPR 指令能够提供的而 JSR 指令无法提供的重要特点?9.2.3 重访字符输入的 TRAP 程序让我们再看一下图 9.4 的键盘输入服务程序。特别的
35、,让我们看一下在符号地址L1,L2,L3 和 L4 中都出现的三行的指令序列:LABEL LDI R3, DSRBRzp LABELSTI Reg, DDRJSR/RET 机制能够让我们用一段简单的子程序来代替这四次出现的同一组指令序列吗?答案:是的,差不多可以。在图 9.8 中,我们改进了键盘输入服务程序,在 05,0B,11 和 14 行包含:JSR WriteChar在 1D 到 20 行是一段四条指令的子程序:WriteChar LDI R3,DSRBRzp WriteCharSTI R2, DDRRET注意需要使用 RET 指令(实际上是 JMP R7)结束子程序。注意这个词:差不多
36、。在原来的以 L2 和 L3 开头的原始指令序列中,STI 指令将R0(并非 R2)的内容传送给 DDR。我们可以很容易的修改如下:在图 9.8 的 09 行,我们使用LDR R2, R1, #0代替LDR R0, R1, #0这就使得提示符中的每个字符都被加载到 R2。子程序 WriteChar 将每个字符从 R2 中传送给 DDR。在图 9.8 的 10 行,我们插入一条指令ADD R2,R0,#0是为了将键盘的输入(在 R0 中)传送给 R2。子程序 WriteChar 将它从 R2 传给DDR。注意,R0 仍然包含着键盘的输入。此外,因为服务程序中后面的指令没有加载R0,所以当控制返回
37、用户程序后,R0 仍然包含键盘的输入。在图 9.8 的 13 行,我们插入指令LD R2,Newline是为了将字符“换新行”传送给 R2。子程序 WriteChar 将它从 R2 传给 DDR。最后,我们注意到,与图 9.4 不同,这个 TRAP 服务程序包含了几条 JSR 指令。这样,包含在 R7 中的返回到调用程序的链接,在服务程序开始执行时,就早已被覆盖了(实际上在 03 行,被第一条 JSR 指令覆盖) 。因此,我们在执行第一条 JSR 指令之前,在 02 行保存 R7,并且在执行最后一条 JSR 指令之后,在 16 行恢复 R7。图 9.8 是真正的用于键盘输入的 LC-3 的 T
38、RAP 服务程序。01 .ORIG x04A002 START ST R7,SaveR7 03 JSR SaveReg04 LD R2,Newline05 JSR WriteChar06 LEA R1,PROMPT07 ;08 ; 09 LOOP LDR R2,R1,#0 ; 得到下一个提示字符 0A BRz Input0B JSR WriteChar0C ADD R1,R1,#10D BR LOOP0E ;0F Input JSR ReadChar10 ADD R2,R0,#0 ; 把字符送给 R2 准备回显到显示器11 JSR WriteChar 12 ;13 LD R2,Newline1
39、4 JSR WriteChar15 JSR RestoreReg16 LD R7,SaveR717 RET ;JMP R7 结束 TRAP 程序18 ;19 SaveR7 .FILL x00001A Newline .FILL x000A1B Prompt .STRINGZ “Input a character“1C ;1D WriteChar LDI R3,DSR1E BRzp WriteChar1F STI R2,DDR20 RET ;JMP R7 结束子程序21 DSR .FILL xFE0422 DDR .FILL xFE0623 ;24 ReadChar LDI R3,KBSR25 BRzp ReadChar26 LDI R0,KBDR27 RET28 KBSR .FILL xFE0029 KBDR .FILL xFE022A ;2B SaveReg ST R1,SaveR12C ST R2,SaveR22D ST R3,SaveR32E ST R4,SaveR42F ST R5,SaveR530 ST R6,SaveR631 RET32 ;