我想编写一个函数(在hlasm中),它调用自身和其他函数。
在x86或z80(可能还有其他版本)上,您只需调用函数,并在其结尾处进行返回。然后处理器将存储并检索返回地址。
指令集具有以下指令: BAL reg,func ...在reg中存储返回地址,然后在结束时你可以将BR reg跳转到该返回地址。另一个问题是显然没有推/弹指令。
所以我现在有点兴奋。
问候
答案 0 :(得分:5)
在System / 360和后续操作系统中,执行此操作的基础架构是所谓的可重入编程的一部分。基本概念是R13指向的保存区域的存储是从操作系统获得的(想想C中的malloc)。在程序的最开始使用系统宏调用来获取存储。同样,系统宏调用将存储返回到操作系统,并在程序的出口处进行编码
您还没有提到您正在使用的操作系统。我将对此示例代码做出以下假设:
这个骨架例程是可重入的,因此它也可以递归使用,正如你所提到的那样(有警告说重新调用自身的坏代码最终会导致存储异常终止)。代码需要一个基址寄存器,但我建议使用名为" baseless"的现代编码方法,其中代码本身不使用基址寄存器,因为它使用相对&分支机构的即时指示。 (您始终需要一个基本寄存器来存储数据。)
WORKAREA DSECT , Reentrant work area (like C stack)
DS 18F Save area
FIELD1 DS F Some variable
FIELD2 DS F Another variable
WORKLEN EQU *-WORKAREA Length of reentrant work area
SUBRTN1 RSECT , HLASM will perform reentrant checking
STM R14,R12,12(R13) Save registers at entry
LR R12,R15 Set code base register
USING SUBRTN1,R12 Establish code addressability
LGHI R0,WORKLEN Get length of reentrant work area
STORAGE OBTAIN, Obtain reentrant work area X
LENGTH=(0) ..Length is in R0
ST R1,8(,R13) Forward chain in prev save area
ST R13,4(,R1) Backward chain in next save area
L R14,20(,R13) Get R1 at entry (parameters)
LR R13,R1 Set up new save area/reentrant workarea
USING WORKAREA,R13 Establish work area addressability
LM R2,R3,0(R14) Get addresses of parameters
STM R2,R3,FIELD1 Save parameter addresses for later
…
*** Logic goes here
…
LR R1,R13 Address to be released
L R13,4(,R13) Address of prior save area
LGHI R0,WORKLEN Length of storage to release
STORAGE RELEASE, Release reentrant work area X
ADDRESS=(1), ..Address in R1 X
LENGTH=(0) ..Length in R0
LM R14,R12,12(R13) Restore registers
OI 15(R13),X'01' This bit on means this save area is inactive
BR R14 Return to caller
这是一个非常基本的示例,并介绍了一些高级汇编程序概念,如DSECT(虚拟部分),它描述了一个区域,但没有在程序中实际分配存储。 RSECT是汇编程序通过在程序试图修改自身时发出警告来强化重入的一种方式。 (还有一个汇编程序选项,RENT,但它适用于整个源代码; RSECT仅用于该部分。)
要记住这个例子的关键是你有两个基址寄存器,一个寻址码,一个寻址数据。这与早期的x86架构相似,包括代码段和数据段。在这种情况下,数据段也用作堆栈段。
可重入程序是系统级z / Architecture编程的重要组成部分,因为它对于多任务环境几乎是必需的。它是一种重要的技术,可以理解是否会编写超出典型批处理程序的应用程序(在一般意义上)。
如果您处于LE环境(COBOL,PL / I,C / C ++或汇编程序主程序)中,则应使用LE提供的CEEENTRY和CEETERM宏。 LE编译器生成的程序通常是可重入的(即使是带有NORENT选项的COBOL)。确保在CEEENTRY宏上编码MAIN = NO,否则会出现各种性能问题和可能的逻辑错误。 LE堆栈机制是我在上面演示的技术的高级版本,最显着的是分配专用于堆栈使用的大区域,以便后续调用不会有调用操作系统来获取存储的开销。
如果您在z / VSE或z / VM环境或非IBM BS2000 / OSD环境中工作,我可以修改上面的示例。
请对问题发表评论,我会更新此示例以进一步明确。
答案 1 :(得分:3)
除了已经发布的内容之外,还有一些想法。
首先,任何复杂的汇编程序员都会考虑子程序链接,并且多年来会建立起偏好。除了"工作"之外,你还需要一些高效,简单和易于调试的东西。解决方案分为三个部分:
保存状态是一个复杂的问题。上面的例子来自" zarchasmpgmr"展示了带链接保存区域的经典方法,如果你要调用你无法控制的东西,你应该熟悉它是如何工作的。然而,有时候,这不是最好的方法,因为"存储获取"您在示例中看到的是一个不是" free"在执行效率方面。换句话说,如果你调用很多子程序并且它们每个都获得/释放工作区域,那么你会发现你的汇编语言例程比用C语言写的那样慢得多。
我个人的偏好是使用保存区" stack"一个接一个的多个条目。当您调用子程序时,子程序将其寄存器保存在堆栈中,并将堆栈指针(R13)递增到下一帧 - 快速,简单且无需系统服务。它甚至易于调试,因为整个调用堆栈通常是连续的。
那么堆栈中的内容"框架"?至少,你需要非参数寄存器(R2-R12),虽然为了调试我经常喜欢保存R0-R15。另外,对于调试,我喜欢保存类似时间戳的东西,甚至可能保存"拥有的子程序的名称。堆栈框架。
如果你只是存储一个时间戳和所有十六个寄存器,那么你需要的是大约80个字节 - 我可能会这样声明:
WORKAREA DSECT ,
STACK DS 10CL80 A 10-entry subroutine save stack
DS 80X'FF' End of stack marker
您当然可以在堆叠框架中包含各种额外的东西。例如,您可能需要子例程的名称或类似的东西,而不是时间戳。
在程序开始时,这一切都设置一次,然后R13用于指向堆栈......当调用例程时,R13在堆栈中上下浮动,以便它始终指向到第一个可用的框架。
作为旁注,由于R13实际上指向任何被调用程序可用于保存其寄存器的区域,因此在调用期望标准OS样式链接约定的例程时也可以使用这种类型的堆栈帧。 。在这种情况下,您在堆栈框架中看到的内容就像上面的标准链接一样,但在大多数情况下,您不必担心调用某些未知的服务喜欢你的私人堆栈框架的想法。
这种方法的唯一缺点是它对于深递归并不是很好,因为你通常会分配固定数量的堆栈帧。如果你有深度递归,你需要检查堆栈的末尾,并且如果初始量不够,你需要一些策略来增加堆栈。
我注意到您可以以相同的方式使用系统链接堆栈...您将发布" BRANCH AND STACK" (BAKR)指令,并且在硬件下,硬件维护的内容与我上面列出的内容非常相似。唯一的问题是BAKR会保存更多的状态信息,所以它比我描述的要慢一些。
几乎无论你如何设置状态保存,诀窍是找到并分支到子程序。作为其中的一部分,您想要考虑您传递给被调用例程的任何参数 - 通常R1将包含(或指向)您想要的任何参数,尽管有时R0也很有用,特别是如果例程是你控制的东西。
有几种方法可以对实际的调用序列进行编码,但传统的方法是使用包含目标子例程地址的R14和包含返回地址的R15来调用子例程。
小型休闲程序与您在大型应用程序中可能需要做的事情之间存在差异。你使用了BAL Rx的一个例子,子程序 - 这是有效的,但你调用的子程序需要在任何基本寄存器上可寻址,并且它也必须是同一个编译单元的一部分(即,标签"子程序"在BAL Rx中,子程序需要在同一个源文件中)。
如果你在一个更大的应用程序中调用子程序,或者你想单独组装你的子程序,创建一个包含每个子程序成员的目标模块库,你就不能使用BAL。同样,这里有很多解决方案,但我更喜欢使用A或V型文字来将子程序的地址加载到R15中,然后使用BALR而不是BAL:
*
* Call the subroutine
*
LA R1,some_parameter R1 -> subroutine parameter
L R15,=A(Subrtn) R15 -> subroutine address
BALR R14,R15 Call the subroutine
使用这种方法,子程序可以在任何地方......所有需要在基本寄存器内的是文字池(LTORG允许你放在任何你喜欢的地方)。如果您想在完全不同的模块中使用子程序,您需要做的就是将= A()更改为= V()。
如果要使用系统提供的链接堆栈,它没有太大区别 - 使用BAKR 0,R15代替BALR指令。
当子程序完成时,它将控制返回到BALR之后的指令。通常,子程序的结果在R15中返回,但有时也使用R0 / R1。例如,如果我有一个获得存储的子程序,我可能会使用R15来表示它是否成功(有点像ERRNO),如果R15 = 0,R0将设置为长度,R1设置为对象的地址。
重点是,您可以以创造性的方式使用参数寄存器,并且通常在几个寄存器中返回结果更有效,而不是将结果存储在某处的内存中并返回结果区域的地址。
好的,我们假设您使用BALR R14,R15来进入子程序。这给了我们:
您要做的是将寄存器保存在堆栈中,以便在返回调用方时能够正确恢复它们。如果您使用基本寄存器问题编写大型应用程序等等,您还需要处理子程序内部本地可寻址性的一些情况。把它放在一起,你会得到类似这样的代码:
*
* Called subroutine
*
PUSH USING Guarantees no "stale" USINGs here
DROP ,
Subrtn CSECT , Allows subroutines in a different object
STCK 0(R13) Saves the timestamp in the stack
STM R0,R15,8(R13) Save the caller's registers
LA R13,80(R13) R13 -> next stack frame
LR Rx,R15 Setup a local base register
USING Subrtn,Rx
. . .
. . . (Subroutine does it's work here)
. . .
SubExit EQU *
*
* Return to the caller here R0/R1 have our result, R15 = return code
*
SLR R15,R15 (we'll assume a good return code)
SHI R13,80 Back up to prior stack frame
LM R2,R14,16(R13) Re-load caller's R2/R14
BR R14 Return to the caller
LTORG , Force subroutine literal pool here
如果你需要处理大规模的递归,你需要添加一些指令来检查堆栈帧的结尾(以X' FF'开头的堆栈帧)然后有什么你喜欢扩展堆栈的策略。
如果您使用BRANCH AND STACK,则返回稍微简单一点 - 只需要一个PROGRAM RETURN指令弹出堆栈帧并返回到创建堆栈帧的任何位置。
无论你做什么,程序进入/退出和子程序链接都是很好的东西,可以作为汇编程序宏编写,并在所有应用程序中反复使用。在我个人的技巧包中,你会发现很多这些东西,所以当我需要写一些新的东西时,我已经解决了所有这些问题。
一张海报指出了LE汇编程序宏(CEESTART等),它们绝对做了很多我所描述的内容,但代价是额外的复杂性,但附加值可以自由地调用LE运行时就像它们是你自己的子程序一样。重点是花费尽可能少的时间来担心琐事,并尽可能多地担心手头的真正任务。
答案 2 :(得分:0)
如果它是您自己程序中的子程序,则BAS(分支和保存)/ BR是标准。如果子程序然后调用另一个子程序,您可以保存调用者的地址,然后再恢复它:
<some code>
BAS R10,SUBRTN1 .Branch to subroutine
<some mode code>
.
.
.
*
SUBRTN1 DS 0H
ST R10,R10SAVE .Save Return address
BAS R10,SUBRTN2 .Call subroutine 2
L R10,R10SAVE .Restore return address
BR R10 .and return
*
SUBRTN2 DS 0H
<some code>
BR R10 .Return to caller
但是,这是针对“内部”子程序的,必须注意子程序不会覆盖主程序使用的寄存器。
如果存在风险,那么您可能希望使用标准链接约定,其中一个程序调用另一个程序,并且调用程序为被调用程序提供保存区域以保存调用程序寄存器等。
答案 3 :(得分:0)
显然,S / 360(至少在Z / OS下)的常规约定很复杂,并且不使用标准的调用堆栈。
但是只是为了好玩,我将对RISC回答一个天真的问题(大概在S / 360硬件上可能会发生,无论它是否在Z / OS或Linux S / 390下都可行)。
在x86上,push reg
等效于sub rsp,8
/ mov [rsp], reg
(但没有设置标志)。在像MIPS之类的ISA或大多数其他RISC(包括S / 360)上,您可以简单地使用2条指令模拟推入和弹出。
调用栈不是神奇的。 push和pop只是窥视孔优化,可以有效地 执行存储或加载以及递减或递增指针的常见操作。
理论上,如果要发明新的调用约定/ ABI,则可以选择任何寄存器用作堆栈指针,除非硬件对某个寄存器有任何隐式使用(例如,用于中断处理程序)。
分支链接/间接分支再次类似于RISC,并且是所有RISC的工作方式(MIPS,PowerPC,ARM等)。在非叶子功能中,只需像对待其他调用一样简单地对待链接寄存器保留的寄存器,并将其保存/存储在堆栈中。
例如在函数输入时,为本地人保留足够的空间,并用一个sub
或st
或stm
保存要保存的寄存器以保存插槽。返回之前,请重新加载所有必要的寄存器,包括链接寄存器,然后您可以正常返回。
查看ISA的GCC或clang输出,例如MIPS,它也没有特定的堆栈指令(甚至对于中断处理程序也没有异步使用堆栈),因此实现调用堆栈是100%的软件约定,ISA不需要