程序集“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,
]
在其他资源中,我看到它们都仅像调用push
、pop
、call
一样低级或 ret
。它们没有显示这些是如何 实现的 。这是另一个 example:
但是如果我要为整个计算机编写一个模拟器,包括push
、pop
、call
,以及 ret
说明 ,这些内容在幕后是什么样子的?他们如何知道内存中的哪个位置可以自由存储下一个 push
?当您调用 pop
时,内存究竟发生了什么?堆栈上的东西实际上是如何传递给使用 call
调用的函数的?等等。基本上,这些东西是如何在幕后工作的。我知道它们是 implemented in the hardware,但是如果您要在代码中实现这些(仅使用 memory
数组,如上述问题),它们会是什么长什么样?
堆栈指针是一个CPU寄存器(此处%esp
),必须初始化它以引用要用作堆栈的内存区域。这个区域需要是可变的,并且足够大以便程序进行调用传递参数。
此堆栈指针寄存器是 push/pop 操作的参考:
- 推送操作递减堆栈指针并将要推送的值写入该地址,并且
- 弹出操作读取堆栈指针指向的值,然后递增堆栈指针。
堆栈指针(以及其他寄存器)在进程调用 main
之前初始化。这要么由操作系统完成,要么由 __start
在 crt0.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:被调用完成后恢复调用的位置。与 push
和 pop
指令不同,call 指令还会更改模拟程序计数器寄存器,以便下一条要执行的指令是被调用函数的第一个指令。
ret
指令具有 pop 的行为,但弹出到模拟程序计数器中,因此这改变了控制流,使得下一条要执行的指令返回到调用者中 - 一条指令过去的原始调用。
希望您能看到 push
和 pop
是如何对偶的,call
和 ret
也是如此。
最后一件事,如果您想在不指定要压入的值的情况下分配堆栈内存,我们可以从堆栈指针中减去 - 并且要在不弹出的情况下释放堆栈内存,我们可以添加到堆栈指针。 call plus
之后的 addl
指令就是这样做的——它弹出两个最初推送的参数,回收两个推送(值),而不检索它们的值。
我正在尝试详细了解如何实现一个健壮的 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,
]
在其他资源中,我看到它们都仅像调用push
、pop
、call
一样低级或 ret
。它们没有显示这些是如何 实现的 。这是另一个 example:
但是如果我要为整个计算机编写一个模拟器,包括push
、pop
、call
,以及 ret
说明 ,这些内容在幕后是什么样子的?他们如何知道内存中的哪个位置可以自由存储下一个 push
?当您调用 pop
时,内存究竟发生了什么?堆栈上的东西实际上是如何传递给使用 call
调用的函数的?等等。基本上,这些东西是如何在幕后工作的。我知道它们是 implemented in the hardware,但是如果您要在代码中实现这些(仅使用 memory
数组,如上述问题),它们会是什么长什么样?
堆栈指针是一个CPU寄存器(此处%esp
),必须初始化它以引用要用作堆栈的内存区域。这个区域需要是可变的,并且足够大以便程序进行调用传递参数。
此堆栈指针寄存器是 push/pop 操作的参考:
- 推送操作递减堆栈指针并将要推送的值写入该地址,并且
- 弹出操作读取堆栈指针指向的值,然后递增堆栈指针。
堆栈指针(以及其他寄存器)在进程调用 main
之前初始化。这要么由操作系统完成,要么由 __start
在 crt0.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:被调用完成后恢复调用的位置。与 push
和 pop
指令不同,call 指令还会更改模拟程序计数器寄存器,以便下一条要执行的指令是被调用函数的第一个指令。
ret
指令具有 pop 的行为,但弹出到模拟程序计数器中,因此这改变了控制流,使得下一条要执行的指令返回到调用者中 - 一条指令过去的原始调用。
希望您能看到 push
和 pop
是如何对偶的,call
和 ret
也是如此。
最后一件事,如果您想在不指定要压入的值的情况下分配堆栈内存,我们可以从堆栈指针中减去 - 并且要在不弹出的情况下释放堆栈内存,我们可以添加到堆栈指针。 call plus
之后的 addl
指令就是这样做的——它弹出两个最初推送的参数,回收两个推送(值),而不检索它们的值。