如果我在调用DLL中的函数时没有传递足够的参数,会发生什么?

15

在一个dll项目中,该函数如下:

extern "C" __declspec(dllexport) void foo(const wchar_t* a, const wchar_t* b, const wchar_t* c)

在另一个项目中,我会使用foo函数,但我在头文件中声明了foo函数。

extern "C" __declspec(dllimport) void foo(const wchar_t* a, const wchar_t* b)

我只用了两个参数调用它。

结果是成功的,我认为这涉及到__cdecl 调用,但我想知道这是如何和为什么有效的。


这将取决于 foo 对缺失参数的处理方式,它们将具有随机值。如果像问题中一样它们是指针,如果 foo 尝试使用它们,你的应用程序很可能会崩溃。 - Richard Critten
调用和返回将保持堆栈不变,因为在 cdecl 中调用者应该从堆栈中移除参数。当然,如果被调用的函数访问缺失的参数,它们将具有未定义的值或导致未定义的行为。 - Paul Ogilvie
2
@RichardCritten 不是“随机值”,而是“不确定值”。 - Jabberwocky
3
我认为MichaelWalz的观点是这些值不是随机生成的,例如,在实践中它们可能是非常可预测的(例如,可能堆栈对齐使得该值始终是调用者的某个局部变量的值)。但这不是一种被规定的行为。当问“如果……会发生什么”时,这可能会产生更大的差异,因为仅仅测试在一个环境中可靠地工作,但在另一个环境中却不起作用。对于真正的随机值,在不同的环境中你期望看到类似分布的值。 - Joshua Taylor
1
@tofro 不是,但这意味着随机生成的一系列内存访问应该与一系列顺序地址的表现大致相同。我不想太过学究,只是想澄清MichaelWalz的意图。 "Indeterminate Value" 是标准中的技术术语,具有精确的含义,而“随机值”可能没有。 - Joshua Taylor
显示剩余4条评论
1个回答

32

32位

默认的调用约定是__cdecl,这意味着调用者会从右到左把参数推入堆栈中,在函数调用返回后清理堆栈。

因此在你的情况下,调用者:

  1. 推入b
  2. 推入a
  3. 推入返回地址
  4. 调用该函数。

此时堆栈看起来像这样(以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] 上是什么。因此,被调用方将看到正确的 ab 值,但会将任何未知的垃圾数据解释为 c

此时,它几乎是未定义的,并取决于您的函数实际上对 c 执行了什么操作。

在所有这些结束之后,如果您的程序没有崩溃,假设被调用方返回,那么调用方将恢复 esp,并且堆栈指针将回到应该在的位置。因此,在调用者的视角中,一切都可能很好,堆栈指针最终回到了它应该在的位置,但被调用方看到了 c 的垃圾数据。


64位

64位机器的机制不同,但最终结果大致相同。在64位机器上,Microsoft使用以下调用约定,无论__cdecl或其他约定(您指定的任何约定都会被忽略,并且所有约定都被视为相同):

  • 前四个整数或指针参数按顺序从左到右放置在寄存器rcxrdxr8r9中。
  • 前四个浮点参数按顺序从左到右放置在寄存器xmm0xmm1xmm2xmm3中。
  • 其余的内容按从右到左的顺序推送到堆栈中。
  • 调用者负责恢复esp以及调用后恢复所有易失性寄存器的值。
在你的情况下,调用者:
1.将 `a` 存储到 `rcx` 中。 2.将 `b` 存储到 `rdx` 中。 3.在栈上分配了额外的 32 字节 "影子空间"(请参阅该 MS 文章)。 4.推送返回地址。 5.调用函数。
但被调用者期望:
- 假定 `a` 在 `rcx` 中(检查!) - 假定 `b` 在 `rdx` 中(检查!) - 假定 `c` 在 `r8` 中(问题)

因此,与32位情况类似,被调用者将r8中的任何内容解释为c,可能会出现问题,最终效果取决于被调用者对c的处理方式。当返回时,假设程序没有崩溃,调用者恢复所有易失性寄存器(包括rcxrdx以及通常包括r8和其他相关寄存器),并恢复esp


6
请注意,对于 64 位二进制文件,参数将通过寄存器传递。除此之外,其效果与本文所描述的基本相同 - 未知的垃圾数据只是从寄存器而不是栈内存中读取。 - ComicSansMS
1
也许你应该提到被调用者清理调用约定(如x86上的stdcall)会导致堆栈失衡,这几乎保证了下一个返回时会崩溃。 - poizan42
值得注意的是,如果被调用的代码 *从不读取或写入 c*(并且假设使用了 __cdecl 等),这个特定的代码将“正常运行”。 - Yakk - Adam Nevraumont
1
我没有在纯C++中尝试过这个,但是根据我的经验,在C#中调用C++ DLL时,Visual Studio会非常好地向您发出消息,指出堆栈已变得不平衡,并进行自我纠正。然而,这确实会导致重大的效率问题。 - Cody

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接