是否有可能在没有间接寻址的情况下实现子程序?

时间:2017-01-08 17:04:39

标签: c compiler-construction subroutine

我正在处理Deitel的书中的Simple-Compiler项目 C如何编程。它的主要目标是为名为SIMPLE的高级语言生成编译器,相关的机器语言称为SIMPLETRON。

我已经完成了这个编译器的一些基本功能,但现在我已经遇到了增强的需求 - 为SIMPLE实现 gosub 返回(子程序功能)语言。

这里的主要障碍是SIMPLETRON不支持间接寻址,这意味着使用堆栈返回子程序地址的策略是行不通的。在这种情况下,是否可以以某种方式使子程序工作?

PS:我搜索了这个问题,发现了一个相关的问题here。似乎自修改代码可能是答案,但我没有找到具体的解决方案,因此我仍然提出了这个问题。 此外,在我看来,SIMPLETRON的机器说明必须扩展到使自修改代码在这里工作,对吗?

  

SIMPLETRON机器语言的背景信息:

     
      
  1. 它只包含一个累加器作为寄存器。
  2.   
  3. 所有支持的机器说明如下:      
        
    • 输入/输出操作      
          
      • #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:结束该计划。没有操作数。
      •   
    •   
  4.   

3 个答案:

答案 0 :(得分:3)

我不熟悉SIMPLE或SIMPLETRON,但总的来说我至少可以想到3种方法。

  1. 自我修改代码

    在每个子例程的末尾都有一个BRANCH 0指令,在此之前,代码将返回地址加载到累加器中,并STORE将其加载到代码本身中,从而有效地形成{{1}指令。

  2. 潜在来电者的静态列表

    如果SIMPLE没有间接调用(即每个BRANCH <dynamic>定位一个静态已知的子例程),则编译器知道每个子例程的可能调用者列表。然后它可以让每个调用传递一个唯一的参数(例如在累加器中),子例程可以测试(伪代码):

    gosub
  3. 内联

    如果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