如何对汇编程序执行 CALL & RET?

How to implement CALL & RET to an assembler?

根据这个编造的微处理器架构的指令集: https://github.com/mertyildiran/DASM

在我们用 C 编写的玩具汇编程序中,我们实现了 PUSH 和 POP 指令,如下所示:

推送:

这基本上是4个DEC和1个ST指令的组合。

        else if (strcmp(token,"push")==0) // PUSH instruction: combination of 4 DEC and 1 ST instruction on Stack Pointer (SP)
        {
            op1 = strtok(NULL,"\n\t\r ");
            op2[0] = sp; // Let's say address of SP is 9
            printf("\n\t%s\t%s\n",strupr(token),op1);
            ch = (op2[0]-48) | ((op2[0]-48)<<3); // Prepare bitwise instruction format for DEC instructions
            program[counter]=0x7800+((ch)&0x00ff); // Decrease Stack Pointer 4 times
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;
            program[counter]=0x7800+((ch)&0x00ff); // Decrease Stack Pointer 4 times
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;
            program[counter]=0x7800+((ch)&0x00ff); // Decrease Stack Pointer 4 times
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;
            program[counter]=0x7800+((ch)&0x00ff); // Decrease Stack Pointer 4 times
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;

            ch = ((op1[0]-48) << 2) | ((op2[0]-48) << 6); // Prepare bitwise instruction format for ST instruction
            program[counter]=0x3000+((ch)&0x00ff); // Store the value in Stack
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;
        }

POP:

这基本上是1个LD和4个INC指令的组合。

        else if (strcmp(token,"pop")==0) // POP instruction: combination of 1 LD and 4 INC instructions on Stack Pointer (SP)
        {
            op1 = strtok(NULL,"\n\t\r ");
            op2[0] = sp; // Let's say address of SP is 9
            printf("\n\t%s\t%s\n",strupr(token),op1);

            ch = (op1[0]-48) | ((op2[0]-48) << 3); // Prepare bitwise instruction format for LD instruction
            program[counter]=0x2000+((ch)&0x00ff); // Store the value in Stack
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;

            ch = (op2[0]-48) | ((op2[0]-48)<<3); // Prepare bitwise instruction format for INC instructions
            program[counter]=0x7700+((ch)&0x00ff); // Decrease Stack Pointer 4 times
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;
            program[counter]=0x7700+((ch)&0x00ff); // Decrease Stack Pointer 4 times
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;
            program[counter]=0x7700+((ch)&0x00ff); // Decrease Stack Pointer 4 times
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;
            program[counter]=0x7700+((ch)&0x00ff); // Decrease Stack Pointer 4 times
            printf("> %d\t%04x\n",counter,program[counter]);
            counter++;
        }

所以我的问题是我们如何使用堆栈实现 CALL 和 RET 指令?

我知道 CALL 指令会将 PC 的当前状态存储在堆栈中,因此程序将能够 return 它离开 RET 指令的位置。但这让我想到了两个子问题:

  1. CALL执行后,如果在子过程中,一条指令将一些东西压入堆栈或覆盖CALL的return地址,程序如何获取先前存储在堆栈中的地址。
  2. 我们如何将相关标签的地址传递给机器代码级别的CALL?在我们的汇编程序中,JMP 和 jZ 指令也没有完成,原因相同。

如果想看全图:https://github.com/mertyildiran/DASM/blob/master/assembler.c

对于 2.subquestion(JMP/CALL) 你可以用 jmp lpp 行来解释这个例子:

.data
     count: 60
     array: .space 10
     char: 0xfe
.code
        ldi 0 count
        ld  0 0
        ldi 1 array
        ldi 2 char
        ld  2 2
lpp     st 1 2
        inc 1
        dec 0
        jz loop
        jmp lpp
loop    sub 1 2 3
lp1     jmp lp1  

"how an assembler pass label address to JMP instruction?"

从 machine/CPU 架构中可以明显看出它是如何解码指令的。我对你的问题有点困惑,虚构的 CPU 是否已经是最终的(然后它应该描述,你如何加载 pc register = 这基本上就是 jump 所做的,加上它可能是有条件的),或者您将两个项目合二为一,同时创建硬件机器规格和它的汇编器。而且我也懒得再看一遍,所以我就把现实中常见的方法给大家看一下。

ARM - 类 RISC 指令集,固定操作码大小:

无论是16b(thumb)还是32b模式,操作码的前几位都指定了指令(B, BL, BLX,B是纯跳转,BL类似于CALL,但不使用栈,a"link" 寄存器代替存储 return 地址),其余位指定保存目标地址的寄存器(因此调用函数 foo 你可以做 load r0,foo bl r0 ), 或相对于当前 PC.

的立即数

这意味着,在 16b 模式下,您可以无条件地跳转 +-2kiB,或者有条件地从 -252 跳转到 +258(条件变体以更多位编码,从立即数中拿走一些) .

这有时会导致这样的情况,高级编译器要么使用寄存器变体,要么跳到足够近的另一个跳转指令,该指令跳得更远。

在 32b 模式下,为立即数保留的剩余位可为您提供更好的范围,所有变体约为 +-32MiB。 (还有一种模式,32b "thumb",它有另一种不同的编码,但在这个例子中无关紧要)。

有趣的是,address 中的 bit-0 确实指定了指令是在 thumb 模式还是 full 32b 模式,因为所有地址都必须在 ARM 上对齐,所以跳转到地址 0x00000001 就是用 switch 跳转到 0x00000000 CPU 到拇指模式。

x86 - 类 CISC 在历史上超越了品味

这个有几乎所有可能的变体(除了你在过去 3 个月里一直在写的那么长的 .asm 中真正需要的那个)。指令编码具有可变长度,因此指令确实使用尽可能多的字节,由英特尔决定需要。

  • 相对跳转(单字节短 -128 ... +127)- 检查
  • 绝对跳转到地址(编码为jmp操作码字节,然后编码地址所需的字节数)
  • 寄存器跳转(到寄存器中存储的地址)
  • 条件相对跳转(不只是 "extended" jmp ARM 情况下的编码,而是特定的操作码)
  • 诸如 loopjcxz/jecxz 之类的额外内容,也许还有一些我什至忘记了。

所以它归结为要么将值加载到某个寄存器中,要么将立即数编码到指令操作码中,然后将其视为绝对地址(例如在 32b 平台上,你设法编码 25 位对于地址,将允许您寻址 32MiB 的 RAM;而不是 4GiB),或作为相对地址(25 位 => pc+-16Mi)。


附录,这可能是您问题的一部分? 比如如何 "know" 将来某个标签的地址是什么?

大部分汇编器都是两次pass的,所以先生成指令操作码(可以计算源码各部分的长度),收集符号table中的所有符号,然后收集所有地址符号根据操作码长度和 org 指令定义。然后在第二遍中,你用特定的符号值填充操作码中的所有立即值。

这也表明,为什么两次通过的汇编程序不能自动处理 jmp rel8/rel16(由 8b 或 16b 立即数定义的相对跳转),但程序员必须指定他想使用哪一个。 (或使用多通道汇编程序,它将首先尝试 rel8,当失败时,它将使用 rel16 重新编码 jmp 并移动+重新编译超出该点的所有内容)。

查看您的 push 编码示例源代码,我觉得您需要做一些工作来改变您的汇编程序的工作方式...(无论如何它都很丑陋,就像创建操作码和打印输出一样屏幕 - 不要犹豫,再写一遍,我敢打赌它会变得更干净、更简单)。


编辑:最后,我可能更好地理解了那段汇编程序。

所以 programuint16_t[],对吧? (应该是问题的一部分)。

而且指令机器码也是固定大小的,16b也是。

但是你的 counter 以 16b 字计...目标体系结构的寻址模式也是如此吗?

在地址0上存储了16b字,在地址1上存储了下一个16b字? (并且它没有重叠,如在 x86 中,地址以 8b 字节计算 => 在这种情况下,第一个指令字将位于地址 01,第二个字将位于地址 23。确保您的 counter 遵循正确的寻址方案(否则在定义标签符号的值时必须转换它)。

并且您的 asm 正在生成类似 0x0000 0x7802 的输出:地址 0,字 7802,使用参数 2 执行一些指令 78(不明确的字节顺序!要么你很幸运在编译主机上有相同的,要么你将以错误的机器代码结束,每个单词中都有交换字节),下一个操作码 0x0001 0x7803, ...等等...

所以看起来你已经为指令编码固定了 16b 大小,不确定 jmp 是否会像 store/load 指令一样吃掉整个 8b,或者那个是特殊的,保留更多位用于立即编码.如果只有其他字节可用,唯一有意义的使用方式是 signed 8b -128..+127 相对跳转。

如果寻址适用于 16b 字,那么您可以有效地跳转 -128..+127 条指令back/forward。如果寻址适用于 8b 字节,则您的范围仅限于 -64..+63 条指令。很难说,因为我没有弄清楚有关目标平台的任何细节(你应该添加一些 link 或其他东西,至少示例 jmp 是如何编码的以及内存是如何映射的)。