如果调用约定不匹配,会发生什么?

如果调用约定不匹配,会发生什么?

作者:BlogUpdater |  时间:2020-10-17 |  浏览:39 |  评论已关闭 条评论

蝎子
信不信由你,调用约定不匹配是程序经常出问题的原因之一。当你的程序代码中出现不相匹配的调动约定的时候,编译器会”大吼大叫”,但是懒惰的程序员只会在其中进行强制转换,以使编译器”尽快闭嘴”。
结果是:Windows不得不永远支持你编写的错误代码。

Windows窗口过程
有很多人会错误地声明了Windows窗口过程(通常是将它声明为__cdecl,而不是__stdcall),因为这个原因,我们分发消息给窗口过程的函数添加了额外的保护,这样它就可以检测到错误的函数声明,并执行适当的修复。
这是你为什么在堆栈上经常会看到神秘的0xdcbaabcd的原因。向窗口过程分发消息的函数会检查这个值是否在堆栈中的正确位置。如果不是,那么它会检查窗口过程是否从堆栈中弹出了一个多余的双字(如果是这样的话,它将尝试修复堆栈),或者窗口过程是否错误地声明为__cdecl而不是__stdcall(如果是这样的话,它将参数从窗口过程的堆栈中弹出)。

DirectX回调函数
在DirectX库中大量地使用了回调函数,并且人们再次将其回调函数声明为__cdecl,而不是__stdcall。因此DirectX枚举器必须对这些错误声明的函数进行特殊的堆栈清理。(真难啊…)

IShellFolder::CreateViewObject
我记得,曾经这么一个程序,它错误地声明了一个名为CreateViewWindow的函数,并且开发者设法以某种方式欺骗了编译器以接受它。如下图所示:

他们不仅错误地声明了函数签名,而且在函数的内部,即使函数没有成功完成,它也返回了S_OK。调用此函数后,我不得不添加额外的代码来清理堆栈,并确认它的返回值是不是正确的。

Rundll32.exe入口点函数
在Microsoft的知识库文章中,介绍了rundll32.exe调用的函数所需要的函数签名。但这并没有阻止人们使用rundll32调用那些并非由rundll32设计的随机函数,例如user32模块里的LockWorkStation或ExitWindowsEx。
下面,就让我们来看看,如果使用rundll32.exe调用ExitWindowsEx之类的函数时,会发生什么情况。
rundll32.exe会解析其命令行,并假定函数的编写方式如下:
void CALLBACK ExitWindowsEx(HWND hwnd, HINSTANCE hinst,LPSTR pszCmdLine, int nCmdShow);

但是,实际的ExitWindowsEx的函数原型是这样的:
BOOL WINAPI ExitWindowsEx(UINT uFlags, DWORD dwReserved);

接下来会发生什么呢?
当进入ExitWindowsEx函数时,堆栈看起来像这样:

但是,函数希望看到堆栈是这样的:

会发生什么?
rundll32.exe传递的hwnd窗口句柄,会被误认为是uFlags,而hinst会误认为为dwReserved。由于窗口句柄是随机的,因此最终会将这个随机标志传递给ExitWindowsEx。也许今天是EWX_LOGOFF,明天是EWX_FORCE,下次是EWX_POWEROFF。

现在假设该函数设法返回。(例如,函数执行失败)。ExitWindowsEx函数清除堆栈中的两个参数,而不知道它已传递给四个参数。生成的堆栈是这样的:

现在堆栈的数据已经被破坏了,真正有趣的事情即将发生。
例如,假设上图中的”.. rest of the stack ..”里保存的是一个返回地址。好了,原始代码将执行”返回”指令以通过该返回地址返回,但是在堆栈被破坏的情况下,”返回”指令将返回命令行并尝试像执行代码一样尝试执行它。

Random custom functions
开发者可能会将一个函数导出为__cdecl,但将其视为__stdcall。这似乎可行,但是返回时,堆栈将被破坏(因为调用者期望使用__stdcall函数来清理堆栈,但是得到的却是无法使用的__cdecl函数)。
好的,我们已经举了这么多的例子了。我想你应该可以明白我想表达的要点了,下面是一些你肯定会问的问题。

为什么编译器不能捕获这些错误呢?
编译器确实这样做了。(不是上面的那个rundll32例子啊)
但是人们似乎已经习惯了只使用强制转换来关闭编译器的警告。

我们看看下面的函数原型:
LRESULT CALLBACK DlgProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

上面的函数原型是错误的,正确的版本请看下面:
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg,WPARAM wParam, LPARAM lParam);

如果你这样调用:
DialogBox(hInst, MAKEINTRESOURCE(IDD_CONTROLS_DLG), hWnd, DlgProc);

编译器会马上发出如下的编译器错误信息:
error C2664: ‘DialogBoxParamA’ : cannot convert parameter 4 from ‘LRESULT (HWND,UINT,WPARAM,LPARAM)’ to ‘DLGPROC’.

然后,你想懒惰一下,使用下面的强制转换来关闭错误信息:
DialogBox(hInst, MAKEINTRESOURCE(IDD_CONTROLS_DLG), hWnd, reinterpret_cast(DlgProc));

“噢,拜托,谁会这么愚蠢,会在没有实际修复代码错误的情况下,仅仅是加入强制类型转换来以使编译错误消失?”
看来,大部分人都会这样做。

在网络上,我还看到了很多开发者犯了这样的错误,这里就不一一列举了。

带有这些错误的程序代码是如何工作的?
当然,这些程序在某种程度上可以正常工作,要不然使用者肯定会注意到并要求开发者修复这个错误。
但是,程序如何在被破坏的堆栈中生存呢?(这个问题我后面会回答)

总结
强制类型转换,不应该出现在任何一个完美代码主义者的字典里。
但,没它,还真不行。

最后
Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。
本文来自:《What can go wrong when you mismatch the calling convention?》

评论已关闭。