【问题标题】:If I do not pass enough parameters when calling a function in a DLL, what will happen?如果我在调用 DLL 中的函数时没有传递足够的参数,会发生什么?
【发布时间】:2017-08-25 08:45: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 尝试使用它们,则您的应用程序很可能会崩溃。
  • 调用和返回将保持堆栈不变,因为在 cdecl 中调用者是从堆栈中删除参数。当然,如果缺少的参数被调用函数访问,它们将具有未定义的值或导致未定义的行为。
  • @RichardCritten 不是随机值,而是不确定值
  • @tofro 我认为 MichaelWalz 的观点是,例如,这些值不是随机生成的。它们实际上在实践中可能是相当可预测的(例如,堆栈可能对齐,使得该值始终是调用者中某个局部变量的值)。但这不是指定的行为。当询问“如果……会发生什么”时,这可能会产生更大的不同,因为仅测试它可能在一种环境中可靠地工作,但在另一种环境中则不行。对于真正随机的值,您会期望在不同环境中具有相似的值分布。
  • @tofro 不,但这意味着随机生成的一系列内存访问应该与一系列顺序地址一样执行。我并不是想(太)迂腐,而是想澄清我所期望的是 MichaelWalz 的意图。 "Indeterminate Value" 是标准中的技术术语,具有精确的含义,而“随机值”可能不是。

标签: c++ c dll


【解决方案1】:

32 位

默认调用约定是__cdecl,这意味着调用者将参数从右到左压入堆栈,然后在调用返回后清理堆栈。

所以在你的情况下,调用者:

  1. 推动 b
  2. 推一个
  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 的正确值,但会将[esp+12] 中发生的任何未知垃圾解释为c

此时它几乎是未定义的,并且取决于您的函数对c 的实际作用。

在这一切结束并且被调用者返回之后,假设你的程序没有崩溃,调用者将恢复esp,并且堆栈指针将回到它应该在的位置。所以从调用者的 POV 来看,一切可能都很好,堆栈指针最终回到了它应该在的位置,但是被调用者看到 c 的垃圾。


64 位

64 位机器上的机制不同,但最终结果大致相同。 Microsoft 在 64 位计算机上使用 the following calling convention不管 __cdecl 或其他任何东西(您指定的任何约定都将被忽略,所有约定都被同等对待):

  • 前四个整数或指针参数按从左到右的顺序放置在寄存器 rcxrdxr8r9 中。
  • 前四个浮点参数按从左到右的顺序放置在寄存器 xmm0xmm1xmm2xmm3 中。
  • 剩余的所有内容都被推入堆栈,从右到左。
  • 调用者负责恢复esp,并在调用后恢复all volatile registers的值。

所以在你的情况下,调用者:

  1. a 放入rcx
  2. b 放入rdx
  3. 在堆栈上分配额外的 32 字节“影子空间”(请参阅​​那篇 MS 文章)。
  4. 推送返回地址。
  5. 调用函数。

但被调用者期待:

  • a 假定在 rcx 中(检查!)
  • b 假定在 rdx 中(检查!)
  • c 假定在 r8 中(问题)

因此,与 32 位情况一样,被调用者将 r8 中发生的任何内容解释为 c,随后出现潜在的问题,最终效果取决于被调用者对 c 所做的事情。返回时,假设程序没有崩溃,调用者restores all volatile registersrcxrdx,一般还包括r8和朋友)并恢复esp

【讨论】:

  • 请注意for 64 bit binaries,参数将改为通过寄存器传递。除此之外,效果与此处描述的几乎相同 - 未知垃圾只是从寄存器而不是堆栈内存中读取。
  • 也许你应该补充一点,被调用者清理的调用约定(如 x86 上的 stdcall)会导致堆栈变得不平衡,这几乎可以保证至少在下一次返回时崩溃。
  • 另外值得注意的是,如果被调用的代码从不读取或写入c(假设__cdecl 等)此特定代码将“运行良好”。
  • 我没有在纯 C++ 中尝试过这个,但是根据我从 C# 调用 C++ DLL 的经验,Visual Studio 非常适合给你一个消息说堆栈已经变得不平衡,并且可以自我纠正.但是,它确实会导致主要的效率问题。
猜你喜欢
  • 2017-05-24
  • 2011-06-19
  • 2011-10-03
  • 1970-01-01
  • 2022-11-26
  • 2022-10-18
  • 1970-01-01
  • 1970-01-01
  • 2013-12-16
相关资源
最近更新 更多