程序集“push”、“pop”、“call”和“ret”操作的幕后究竟发生了什么?

What happens exactly under the hood to Assembly `push`, `pop`, `call`, and `ret` operations?

我正在尝试详细了解如何实现一个健壮的 stack/register 机器(我猜是一种混合机器):。那里的答案显示:

const memory = [
  8, // initial program counter
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  "push",
  1,
  "push",
  2,
  "add",
  "push",
  3,
  "add",
  "print",
  "exit",
  0,
  0,
]

在其他资源中,我看到它们都像调用pushpopcall一样低级或 ret。它们没有显示这些是如何 实现的 。这是另一个 example:

但是如果我要为整个计算机编写一个模拟器包括pushpopcall,以及 ret 说明 ,这些内容在幕后是什么样子的?他们如何知道内存中的哪个位置可以自由存储下一个 push?当您调用 pop 时,内存究竟发生了什么?堆栈上的东西实际上是如何传递给使用 call 调用的函数的?等等。基本上,这些东西是如何在幕后工作的。我知道它们是 implemented in the hardware,但是如果您要在代码中实现这些(仅使用 memory 数组,如上述问题),它们会是什么长什么样?

堆栈指针是一个CPU寄存器(此处%esp),必须初始化它以引用要用作堆栈的内存区域。这个区域需要是可变的,并且足够大以便程序进行调用传递参数。

此堆栈指针寄存器是 push/pop 操作的参考:

  • 推送操作递减堆栈指针并将要推送的值写入该地址,并且
  • 弹出操作读取堆栈指针指向的值,然后递增堆栈指针。

堆栈指针(以及其他寄存器)在进程调用 main 之前初始化。这要么由操作系统完成,要么由 __startcrt0.o 中调用 main.

完成

一个合适的模拟器必须模拟一些操作系统行为 and/or __start 行为,例如在调用 main.

之前将有效的初始值放入堆栈指针寄存器

How do they know which place in memory is free to store the next push?

堆栈指针是引用:它的值表示堆栈中第一项的地址。

What happens exactly to the memory when you call pop?

内存没有任何变化,只是更新了堆栈指针(寄存器的值发生变化,因此它指向的位置被移动),因此内存现在被视为可用(例如,用于另一次推送)。堆栈指针以下的内存被认为是空闲堆栈区,堆栈指针及以上的内存被认为是使用堆栈区。

How do the things on the stack actually get passed to the function being called with call?

调用函数(调用者)和被调用函数(被调用者)都可以访问 CPU 堆栈寄存器。

被调用者知道栈上有一个return地址和参数。 return 地址在堆栈的顶部——它是堆栈指针直接指向的地方。因此堆栈指针的地址 + 4 然后指向参数 #1,+8 指向参数 #2,等等。


模拟 CPU 的模拟器将同时模拟内存和 CPU 寄存器,例如堆栈指针和程序计数器(也称为指令指针)。指令指针保存下一条要执行的指令的地址,这是处理器管理控制流的方式。

为了模拟入栈指令,模拟器将递减模拟堆栈指针寄存器,然后将要入栈的值写入该堆栈指针中保存的地址,同时增加程序计数器寄存器以准备执行push

之后的下一条顺序指令

模拟器对 pop 执行相反的操作:它从模拟内存中模拟堆栈指针寄存器引用的位置读取,然后递增该模拟寄存器。它还将递增程序计数器以准备执行 pop.

之后的下一条顺序指令

call 指令具有推送的行为,其中推送的值是模拟程序计数器 — 修改后它指的是紧跟在 call 指令之后的指令 — 这是returnaddress:被调用完成后恢复调用的位置。与 pushpop 指令不同,call 指令还会更改模拟程序计数器寄存器,以便下一条要执行的指令是被调用函数的第一个指令。

ret 指令具有 pop 的行为,但弹出到模拟程序计数器中,因此这改变了控制流,使得下一条要执行的指令返回到调用者中 - 一条指令过去的原始调用。

希望您能看到 pushpop 是如何对偶的,callret 也是如此。

最后一件事,如果您想在不指定要压入的值的情况下分配堆栈内存,我们可以从堆栈指针中减去 - 并且要在不弹出的情况下释放堆栈内存,我们可以添加到堆栈指针。 call plus 之后的 addl 指令就是这样做的——它弹出两个最初推送的参数,回收两个推送(值),而不检索它们的值。