我正在处理Deitel的书中的Simple-Compiler项目 C如何编程。它的主要目标是为名为SIMPLE的高级语言生成编译器,相关的机器语言称为SIMPLETRON。
我已经完成了这个编译器的一些基本功能,但现在我已经遇到了增强的需求 - 为SIMPLE实现 gosub 和返回(子程序功能)语言。
这里的主要障碍是SIMPLETRON不支持间接寻址,这意味着使用堆栈返回子程序地址的策略是行不通的。在这种情况下,是否可以以某种方式使子程序工作?
PS:我搜索了这个问题,发现了一个相关的问题here。似乎自修改代码可能是答案,但我没有找到具体的解决方案,因此我仍然提出了这个问题。 此外,在我看来,SIMPLETRON的机器说明必须扩展到使自修改代码在这里工作,对吗?
SIMPLETRON机器语言的背景信息:
- 它只包含一个累加器作为寄存器。
- 所有支持的机器说明如下:
醇>
- 输入/输出操作
#define READ 10
:从终端读取一个字到内存中,并以操作数作为内存地址。#define WRITE 11
:从内存中写入一个字到终端,并以操作数作为内存地址。- 加载/存储操作
#define LOAD 20
:将一个字从内存加载到累加器中,并以操作数作为内存地址。#define STORE 21
:将累加器中的一个字存储到内存中,并以操作数作为内存地址。- 算术运算
#define ADD 30
:从存储器中向累加器中的字添加一个字(将结果保留在累加器中)并使用操作数作为 记忆地址。#define SUBTRACT 31
:减去一个字......#define DIVIDE 32
:分词......#define MULTIPLY 33
:乘以一个词......- 转移控制操作
#define BRANCH 40
:分支,并以操作数作为代码位置。#define BRANCHNEG 41
:如果累加器为负且操作数为代码位置,则为分支。#define BRANCHZERO 42
:如果累加器为零并且操作数为代码位置,则为分支。#define HALT 43
:结束该计划。没有操作数。
答案 0 :(得分:3)
我不熟悉SIMPLE或SIMPLETRON,但总的来说我至少可以想到3种方法。
自我修改代码
在每个子例程的末尾都有一个BRANCH 0
指令,在此之前,代码将返回地址加载到累加器中,并STORE
将其加载到代码本身中,从而有效地形成{{1}指令。
潜在来电者的静态列表
如果SIMPLE没有间接调用(即每个BRANCH <dynamic>
定位一个静态已知的子例程),则编译器知道每个子例程的可能调用者列表。然后它可以让每个调用传递一个唯一的参数(例如在累加器中),子例程可以测试(伪代码):
gosub
内联
如果SIMPLE不允许递归子程序,则根本不需要在机器代码级别实现调用。只需将每个子程序完全内联到其调用者中即可。
答案 1 :(得分:2)
是的,你可以做到这一点,甚至是合理的,没有自我修改的代码。
您将返回地址变成一个巨大的案例陈述。 秘诀是理解“返回地址”只是一种方式 回到呼叫点,那个记忆只是一个巨人 命名位置数组。
想象一下,我有一个带有许多逻辑调用位置的程序,带有指令 在电话标记为:
之后 CALL S
$1: ...
...
CALL T
$2: ...
...
CALL U
$3: ...
我们需要用我们的机器可以实现的东西来替换CALL。 我们暂时假设在任何时刻只有一个子程序调用处于活动状态。
然后重要的是,在子程序完成后,该控制 返回到通话后的点。
您可以通过编写以下SIMPLETRON代码(我正在编写语法)来解决此问题。按照惯例,我假设我有一堆内存位置K1,K2,...包含常量1,2,...等我需要的常数。
K1: 1
K2: 2
K3: 3
...
LOAD K1
JMP S
$1: ...
...
LOAD K2
JMP T
$2: ...
...
LOAD K3
JMP U
$3:....
S: STORE RETURNID
...
JMP RETURN
T: STORE RETURNID
...
JMP RETURN
U: STORE RETURNID
...
JMP RETURN
RETURN: LOAD RETURNID
SUB K1
JE $1
LOAD RETURNID
SUB K2
JE $2
LOAD RETURNID
SUB K3
JE $3
JMP * ; bad return address, just hang
实质上,每个呼叫站点都记录该呼叫站点唯一的常量(RETURNID),“RETURN”逻辑使用该唯一ID来确定返回点。如果你有很多子程序,返回逻辑代码可能会很长,但是,嘿,这是一个玩具机器,我们对效率不感兴趣。 您总是可以将返回逻辑转换为二元决策树;然后 代码可能很长但只需要log2(callcount)来决定如何回来,而不是真的那么糟糕。
让我们放松一下我们假设只有一个子程序在任何时刻都有效。 您可以为每个子例程定义一个RETURNID,但仍然使用相同的RETURN代码。有了这个想法,任何子程序都可以调用任何其他子程序。显然,这些例程不是可重入的,因此在任何调用链中都不能多次调用它们。
我们可以使用同样的想法来实现返回堆栈。诀窍是认识到堆栈只是一组内存位置,其中一个地址解码器可以选择堆栈成员。所以,让我们实现 PUSH和POP指令作为子程序。我们改变了我们的召唤惯例 使调用者记录RETURNID,使累加器空闲 传递一个值:
LOAD K1
STORE PUSHRETURNID
LOAD valuetopush
JMP PUSH
$1:
LOAD K2
STORE POPRETURNID
JMP POP
$2:...
TEMP:
STACKINDEX: 0 ; incremented to 1 on first use
STACK1: 0 ; 1st stack location
...
STACKN: 0
PUSH: STORE TEMP ; save value to push
LOAD PUSHRETURNID ; do this here once instead of in every exit
STORE RETURNID
LOAD STACKINDEX ; add 1 to SP here, once, instead of in every exit
ADD K1
STORE STACKINDEX
SUB K1
JE STORETEMPSTACK1
LOAD STACKINDEX
SUB K2
JE STORETEMPSTACK2
...
LOAD STACKINDEX
SUB Kn
JE STORETEMPSTACKn
JMP * ; stack overflow
STORETEMPSTACK1:
LOAD TEMP
STORE STACK1
JMP RETURN
STORETEMPSTACK2:
LOAD TEMP
STORE STACK2
JMP RETURN
...
POP: LOAD STACKINDEX
SUB K1 ; decrement SP here once, rather than in every exit
STORE STACKINDEX
LOAD STACKINDEX
SUB K0
JE LOADSTACK1
LOAD STACKINDEX
SUB K1
JE LOADSTACK2
...
LOADSTACKn:
LOAD STACKn
JMP POPRETURN
LOADSTACK1:
LOAD STACK1
JMP RETURNFROMPOP
LOADSTACK2:
LOAD STACK2
JMP RETURNFROMPOP
RETURNFROMPOP: STORE TEMP
LOAD POPRETURNID
SUB K1
JE RETURNFROMPOP1
LOAD POPRETURNID
SUB K2
JE RETURNFROMPOP2
...
RETURNFROMPOP1: LOAD TEMP
JMP $1
RETURNFROMPOP2: LOAD TEMP
JMP $2
请注意,我们需要RETURN,以处理没有值的返回,以及RETURNFROMPOP,它处理来自POP子例程的返回值值。
所以这些看起来很笨拙,但我们现在可以实现一个下推堆栈 固定但任意大的深度。如果我们再次从堆栈位置和returnID检查中创建二进制决策树,那么运行时成本只是堆栈/调用计数大小的对数,实际上非常好。
好的,现在我们有普通的PUSH和POP子程序。现在我们可以进行调用,将返回地址存储在堆栈中:
LOAD K1 ; indicate return point
STORE PUSHRETURNID
LOAD K2 ; call stack return point
JMP PUSH
$1: LOAD argument ; a value to pass to the subroutine
JMP RECURSIVESUBROUTINEX
; returns here with subroutine result in accumulator
$2:
RECURSIVESUBROUTINEX:
...compute on accumulator...
LOAD K3 ; indicate return point
STORE PUSHRETURNID
LOAD K4 ; call stack return point
JMP PUSH
$3: LOAD ... ; some revised argument
JMP RECURSIVESUBROUTINEX
$4: ; return here with accumulator containing result
STORE RECURSIVESUBROUTINERESULT
LOAD K5
STORE POPRETURNID
JMP POP
$5: ; accumulator contains return ID
STORE POPRETURNID
LOAD RECURSIVESUBROUTINERESULT
JMP RETURNFROMPOP
就是这样。现在你有一个带有堆栈的完全递归的子程序调用,没有(好的,伪造的)间接。
我不想手动编程这台机器,因为构建RETURN例程对于编码并保持正确是一件令人头疼的问题。但是编译器非常乐意制造所有这些东西。
答案 2 :(得分:0)
虽然无法从SIMPLE指令集中获取当前指令的位置,但汇编程序可以跟踪指令位置,以生成等效的返回指令。
汇编程序将生成一个分支来解决程序映像中的指令以用作返回指令,然后实现一个调用,它将生成代码以加载“返回指令”并将其存储在子程序结束之前分支到该子程序。 “调用”的每个实例都需要程序映像中的“返回指令”的实例。您可能希望保留一系列可变内存来存储这些“返回说明”。
示例“代码”使用包含返回指令标签的调用作为参数:
call sub1, sub1r
; ...
sub1: ; ...
sub1r: b 0 ;this is the return instruction
另一种选择类似于MASM PROC和ENDP,其中ENDP将保存返回指令。 call指令假定endp方向包含要修改的分支,标签与相应的proc指令相同。
call sub1
; ...
sub1 proc ;subroutine entry point
; ...
sub1 endp ;subroutine end point, "return" stored here
这里的问题是累加器会被“调用”破坏(但不受“返回”的影响)。如果需要,子程序参数可以存储为变量,可能使用汇编程序指令进行标记:
sub1 parm1 ;parameter 1 for sub1
;....
load sub1.parm1 ;load sub1 parameter 1