我正在开发一个Motorola 68000汇编程序,该程序使用子例程连接两个字符串。挑战在于通过堆栈实现输入和输出的参数传递,因此我专注于正确设置和恢复堆栈。
我在Sep Roland和Erik Eidt的帮助下开发了程序逻辑。之后,我研究了如何使用堆栈传递参数,这就是为什么我的代码有大量注释的原因。
任务要求:
- 用68000 汇编语言实现一个使用堆栈传递参数的子程序。
- 该子程序接受两个输入字符串:
- A =
"Hello"
- B =
"World"
- A =
- 它将它们连接成输出字符串C,结果为:
- C =
"HelloWorld"
- C =
- 主程序应该:
- 通过推送参数来准备堆栈。
- 调用子程序。
- 函数返回后正确恢复堆栈。
我的实现:
ORG $8000
;DATA
StringA DC.B 'Hello',0 ; First string with a null terminator
StringB DC.B 'World',0 ; Second string with a null terminator
StringC DS.B 256 ; Buffer for the concatenated string
START:
; The stack pointer (A7) starts at address $8000.
; In the 68000 architecture, A7 always points to the memory address where
; the next value will be saved (push operation).
pea.l StringC ; Equivalent to [move.l #StringC, -(a7)]
; The stack pointer (A7) is decremented by 4 (pushing a longword = 4 bytes)
; Initial A7 = $8000, now A7 = $7FFC
pea.l StringB ; A7 = $7FF8
pea.l StringA ; A7 = $7FF4
; Therefore, the stack (from lowest to highest address) contains:
; A7 = $7FF4 |StringA address|
; A7 = $7FF8 |StringB address|
; A7 = $7FFC |StringC address|
; A7 = $8000 (original SP value before the push operations)
bsr.s CopyStrings ; Call the first subroutine, saving the PC (Program Counter)
; onto the stack
; When executing bsr.s, the processor:
; - Saves the return address (PC) on the stack (another 4 bytes subtracted from A7).
; - Then branches to CopyStrings.
; Upon returning from the subroutine (rts), the stack pointer A7 will remain
; where the subroutine left it. However, we need to clean up the three parameters
; (StringA, StringB, StringC) that we previously pushed.
addq.l #8,a7 ; Restore 8 bytes of the stack
addq.l #4,a7 ; Restore the remaining 4 bytes (total 12 bytes)
SIMHALT
CopyStrings:
; At the entry of the subroutine, the stack looks like this:
; A7 |Return Address |
; A7+4 |StringA Address|
; A7+8 |StringB Address|
; A7+12 |StringC Address|
move.l 4(a7),a0 ; Retrieve the address of StringA
move.l 8(a7),a1 ; Retrieve the address of StringB
move.l 12(a7),a2 ; Retrieve the address of StringC
CopyA:
move.b (a0)+,(a2)+ ; Load a character from StringA into StringC
bne.s CopyA ; If the character is not null, continue copying
subq.l #1,a2 ; Move back 1 byte to overwrite the null terminator
CopyB:
move.b (a1)+,(a2)+ ; Load a character from StringB into StringC
bne.s CopyB ; If the character is not null, continue copying
rts ; Return from subroutine
END START
问题:
- 我通过堆栈传递参数的方法正确吗?
- 我应该考虑哪些优化或最佳实践?
任何反馈都将不胜感激!
对于在堆栈上传递参数的 C 风格调用,您的方法是正确的。C 风格(尤其是较旧的 C)以反向传递参数,以便它们在堆栈上按正向顺序出现。这对于可变函数(例如
printf
)特别有用。此外,调用者会清理推送的参数,这对可变函数也特别有用。较旧的 C 编译器将所有函数视为潜在的可变函数,因为早期函数原型并不是真正需要的。这意味着您可以省略参数(例如可选参数),或传递额外的参数,并且由于调用者知道它推送了什么,因此它负责弹出。另一方面,Pascal 不支持可变参数函数,因此会按正向顺序传递参数,并在返回时由被调用方从堆栈中删除参数。由于返回地址实际上妨碍了传递的参数,因此芯片设计人员制作了一个特殊的返回和释放指令,
rtd
该指令支持函数返回到堆栈顶部的地址,但也支持在通过弹出获得返回地址后弹出参数。(如果没有该指令,被调用方清理结尾必须将返回地址弹出到寄存器中,从堆栈中弹出参数,然后使用寄存器中的返回地址进行间接跳转)。我认为较新的 C 能够在声明(即,在生成返回/结尾的代码时)和使用(即,在调用时、在调用站点)时清楚地区分可变参数函数和采用固定参数的函数,因此,虽然可能还会向后传递参数,但能够将其用于
rtd
非可变参数函数。此外,68k 的现代调用约定可能会在寄存器中传递至少 6 个项,在 d0-d2 中传递 3 个项,在 a0-a2 中传递 3 个项,具体取决于类型(无论是指针还是整数)。溢出参数将进入堆栈(而可变函数可能会在堆栈上传递所有参数)。
您的函数没有输出/返回值。如果有,并且您希望通过堆栈传递,则调用者可以在传递参数之前将零或未初始化的字/长推入堆栈,以便被调用者仍可以使用
rtd
返回并释放除返回值之外的所有内容。还有一个问题是,使用两个 16 位
addq
指令弹出是否比使用单个较长的(32 位)addi
指令更好。我会选择较长的指令,以减少指令数量,虽然我不知道 68k 系列各个型号的确切时间,但我怀疑这可能是相同或更快的。没问题,但您可以将这两条
addq.l #.., a7
指令合并为一条adda.l #12, a7
。在 68000 上,这将在 14 个时钟内运行,比您编写的少 2 个时钟。现在数组的地址已在堆栈中,您有绝佳的机会在CopyStrings子例程中少破坏一个地址寄存器。这在较大的程序中非常有用。请注意,您不能使用简单的 加载地址寄存器。指令集有一条专门用于此的指令。
move
movea