32位
默认的调用约定是__cdecl
,这意味着调用者会从右到左把参数推入堆栈中,在函数调用返回后清理堆栈。
因此在你的情况下,调用者:
- 推入b
- 推入a
- 推入返回地址
- 调用该函数。
此时堆栈看起来像这样(以4字节指针为例,记住当你推入东西时,堆指针向后移动):
+-----+ <--- this is where esp is after pushing stuff
| ret | [esp]
+-----+
| a | [esp+4]
+-----+
| b | [esp+8]
+-----+ <--- this is where esp was before we started
| ??? | [esp+12 and beyond]
+-----+
好的,很好。现在问题出现在被调用方。被调用方期望参数在堆栈上的特定位置,因此:
a
被认为在 [esp+4]
b
被认为在 [esp+8]
c
被认为在 [esp+12]
这就是问题所在:我们不知道 [esp+12]
上是什么。因此,被调用方将看到正确的 a
和 b
值,但会将任何未知的垃圾数据解释为 c
。
此时,它几乎是未定义的,并取决于您的函数实际上对 c
执行了什么操作。
在所有这些结束之后,如果您的程序没有崩溃,假设被调用方返回,那么调用方将恢复 esp
,并且堆栈指针将回到应该在的位置。因此,在调用者的视角中,一切都可能很好,堆栈指针最终回到了它应该在的位置,但被调用方看到了 c
的垃圾数据。
64位
64位机器的机制不同,但最终结果大致相同。在64位机器上,Microsoft使用以下调用约定,无论__cdecl
或其他约定(您指定的任何约定都会被忽略,并且所有约定都被视为相同):
- 前四个整数或指针参数按顺序从左到右放置在寄存器
rcx
、rdx
、r8
和r9
中。
- 前四个浮点参数按顺序从左到右放置在寄存器
xmm0
、xmm1
、xmm2
和xmm3
中。
- 其余的内容按从右到左的顺序推送到堆栈中。
- 调用者负责恢复
esp
以及调用后恢复所有易失性寄存器的值。
在你的情况下,调用者:
1.将 `a` 存储到 `rcx` 中。
2.将 `b` 存储到 `rdx` 中。
3.在栈上分配了额外的 32 字节 "影子空间"(请参阅该 MS 文章)。
4.推送返回地址。
5.调用函数。
但被调用者期望:
- 假定 `a` 在 `rcx` 中(检查!)
- 假定 `b` 在 `rdx` 中(检查!)
- 假定 `c` 在 `r8` 中(问题)
因此,与32位情况类似,被调用者将r8
中的任何内容解释为c
,可能会出现问题,最终效果取决于被调用者对c
的处理方式。当返回时,假设程序没有崩溃,调用者恢复所有易失性寄存器(包括rcx
、rdx
以及通常包括r8
和其他相关寄存器),并恢复esp
。