有时,编写代码来完成特定任务可能会变得冗余。在汇编语言中,尤其如此,因为指令的数量有限,大多数任务都涉及用它们做非常相似的事情。子程序是一组旨在执行程序中频繁使用的操作的指令。从某种意义上说,这就像在C或Java这样的语言中编写函数,但层次稍微低一些—特别是我们必须自己做更多的切换工作。
假设你想要将所有寄存器设置为零。有八个寄存器,所以写出来需要八条指令。如果你想在多个地方这样做,每次都写出八条指令就会变得很痛苦。相反,我们可以使用子程序,只写一次。为此,我们使用CALL(实际上是ACALL或LCALL)和RET指令。例如,
JMP START
ALL_THE_ZEROS:
MOV R0, #0
MOV R1, #1
...
MOV R7, #7
RET ; 从子程序调用返回。
START:
MOV R3, #7
CALL ALL_THE_ZEROS ; 调用子程序。
MOV R7, #4
CALL ALL_THE_ZEROS ; 再次调用它。
CALL指令看起来类似于JMP指令,因为你使用标签来指定代码中的去哪里。CALL和JMP的区别在于CALL在执行跳转之前,会将CALL之后的下一条指令的PC地址推入堆栈。调用子程序跳转到标记的子程序,然后从那里继续。当子程序完成后,使用RET指令。RET将从堆栈中弹出最后一条CALL指令原本存储在那里的PC地址,然后跳转到该地址并从那里继续执行。
练习9:
在寄存器R0和R1中存储数字。编写一个子程序,将R0中的数字与R1中的数字的四倍相加。你可以使用乘法或通过四次添加R1来完成这个操作。结果应该在R1中。调用子程序。
MOV R0, #1
MOV R1, #2
CALC: MOV A, R1 ; Move R1 to accumulator
MOV B, #04H ; Load B with 4
MUL AB ; Multiply A by 4
ADD A, R0 ; Add R0 to the result
MOV R1, A ; Store final result in R1
RET ; Return from subroutine
ACALL CALC
还有一件事。调用子程序可能是危险的。当你调用子程序时,它可以改变你正在使用的寄存器中的值。如果你不小心,可能会得到意外的行为。为了解决这个问题,在调用之前保存寄存器中的值,然后在之后恢复它们。这通常是通过将寄存器的值推入堆栈,然后在之后弹出它们来完成的。
练习10:
制作一个子程序,以你希望的方式改变R0和R1的值。当程序开始时,将R0和R1初始化为某个值。将R0和R1的值推入堆栈。调用子程序以正确的顺序从堆栈中弹出R0和R1的值。
MOV R0, #1
MOV R1, #2
CALC: MOV A, R1 ; Move R1 to accumulator
MOV B, #04H ; Load B with 4
MUL AB ; Multiply A by 4
ADD A, R0 ; Add R0 to the result
MOV R1, A ; Store final result in R1
RET ; Return from subroutine
ACALL CALC
还有一件事。调用子程序可能是危险的。当你调用子程序时,它可以改变你正在使用的寄存器中的值。如果你不小心,可能会得到意外的行为。为了解决这个问题,在调用之前保存寄存器中的值,然后在之后恢复它们。这通常是通过将寄存器的值推入堆栈,然后在之后弹出它们来完成的。
练习10:
制作一个子程序,以你希望的方式改变R0和R1的值。当程序开始时,将R0和R1初始化为某个值。将R0和R1的值推入堆栈。调用子程序以正确的顺序从堆栈中弹出R0和R1的值。
; Initialize R0 and R1 with some values
MOV R0, #25H ; Initialize R0 with 25H
MOV R1, #37H ; Initialize R1 with 37H
; Push R0 and R1 onto stack
; Note: Push order is important for correct popping
MOV A, R0 ; Move R0 to accumulator
PUSH ACC ; Push R0's value onto stack
MOV A, R1 ; Move R1 to accumulator
PUSH ACC ; Push R1's value onto stack
; Call subroutine that changes R0 and R1
ACALL CHANGE_VALS
; Pop values back into R0 and R1
; Note: Pop in reverse order of push
POP ACC ; Pop top value from stack
MOV R1, A ; Restore R1
POP ACC ; Pop next value from stack
MOV R0, A ; Restore R0
SJMP $ ; Infinite loop after completion
; Subroutine to change register values
CHANGE_VALS:
MOV R0, #0FFH ; Change R0 to FFH
MOV R1, #0AAH ; Change R1 to AAH
RET ; Return from subroutine
让我解释一下这个程序是如何工作的:
1.初始化:
- R0 用 25H 初始化
- R1 用 37H 初始化
2.推送到堆栈:
- 由于我们不能直接推送寄存器,我们首先将每个寄存器移动到累加器
- 首先推送 R0 的值,然后推送 R1 的值
- 堆栈在内存中向下增长,因此最后推送的项目位于顶部
3.子程序:
- CHANGE_VALS 将 R0 更改为 FFH,将 R1 更改为 AAH
- 这表明原始值安全地存储在堆栈中
4.从堆栈弹出:
- 以相反的顺序弹出值(LIFO – 后进先出)
- 首先弹出 R1 的值,然后弹出 R0 的值
- 值从累加器移回各自的寄存器
5.执行后:
- 最初:R0 = 25H,R1 = 37H
- CHANGE_VALS 之后:R0 = FFH,R1 = AAH
- 弹出后:R0 = 25H,R1 = 37H(恢复为原始值)