【问题标题】:How can a cdecl calling convention corrupt the ESP?cdecl 调用约定如何破坏 ESP?
【发布时间】:2023-03-21 01:55:01
【问题描述】:

我的应用程序崩溃了,因为我调用的库函数更改了 ESP,尽管它被声明为 cdecl。

库 (libclang.dll) 是使用 MinGW 编译的,我在 VC++ 项目中使用它。这些函数被导出为 C 函数,Dependency Walker 告诉我它们具有正确的 cdecl 调用约定。这些函数通过包含 Clang 的“index.h”文件使用 dllimport 导入我的项目中。似乎并非所有函数都在破坏 ESP,因此某些函数执行成功,而其他函数则导致崩溃。

这是一个工作函数的组装:

// call to clang_getNumDiagnostics(TU); - works!
5AF3EFAB  mov         esi,esp  
5AF3EFAD  mov         eax,dword ptr [ebp-30h]  
5AF3EFB0  push        eax  
5AF3EFB1  call        dword ptr [__imp__clang_getNumDiagnostics (5AF977E0h)]  
5AF3EFB7  add         esp,4  
5AF3EFBA  cmp         esi,esp  
5AF3EFBC  call        @ILT+7135(__RTC_CheckEsp) (5AF16BE4h)

下面的函数调用会改变 esp(加 4),从而由于 __RTC_CheckEsp 中的运行时检查而导致崩溃。

// call to clang_getTranslationUnitCursor(TU); - fails!
5AF3EFC1  mov         esi,esp  
5AF3EFC3  mov         eax,dword ptr [ebp-30h]  
5AF3EFC6  push        eax  
5AF3EFC7  lea         ecx,[ebp-234h]  
5AF3EFCD  push        ecx  
5AF3EFCE  call        dword ptr [__imp__clang_getTranslationUnitCursor (5AF9780Ch)]  
5AF3EFD4  add         esp,8  
5AF3EFD7  cmp         esi,esp  
5AF3EFD9  call        @ILT+7135(__RTC_CheckEsp) (5AF16BE4h)  

我已经针对这个问题发布了question,但我认为我专门询问了调用约定 cdecl 以检索有关 esp 损坏可能性的更具体信息,因为我认为这可能是问题的根源......所以请原谅这个“双重职位”。

源代码也可能在于调用了错误的函数(可能是由于我使用 dlltool 创建的 def 文件中存在问题,后来又从中创建了导入库 - 序数与 Dependency Walker 显示的不同 - 我试过了有更正的序数,但没有变化)。我觉得这不太可能是问题的根源,因为其他函数调用工作正常并返回正确的值......

谢谢!

[更新]

根据 __imp__clang_getTranslationUnitCursor 的要求组装

6660A4A0  push        ebp  
6660A4A1  mov         ebp,esp  
6660A4A3  push        edi  
6660A4A4  push        ebx  
6660A4A5  mov         eax,dword ptr [ebp+8]  
6660A4A8  mov         ebx,eax  
6660A4AA  mov         al,0  
6660A4AC  mov         edx,14h  
6660A4B1  mov         edi,ebx  
6660A4B3  mov         ecx,edx  
6660A4B5  rep stos    byte ptr es:[edi]  
6660A4B7  mov         eax,dword ptr [ebp+8]  
6660A4BA  mov         dword ptr [eax],12Ch  
6660A4C0  mov         eax,dword ptr [ebp+8]  
6660A4C3  mov         edx,dword ptr [ebp+0Ch]  
6660A4C6  mov         dword ptr [eax+10h],edx  
6660A4C9  mov         eax,dword ptr [ebp+8]  
6660A4CC  pop         ebx  
6660A4CD  pop         edi  
6660A4CE  pop         ebp  
6660A4CF  ret         4  

[更新 2] 由于 VC++ 和 GCC 都使用 cdecl 作为默认值,并且没有在函数声明中明确说明的情况下无法在 GCC 中强制使用另一个默认调用约定(对于有问题的函数没有这样做),我实际上确信 cdecl 无处不在.

我发现了这个 link,它说明了一些差异,可以解释为什么某些功能有效而其他功能无效:

Visual C++ / Win32

  • 大于 8 字节的对象在内存中返回。

  • 当在内存中返回时,调用者将指向内存位置的指针作为第一个参数(隐藏)传递。被调用者填充内存,并返回指针。 调用者 将隐藏指针与其余参数一起弹出。

MinGW g++ / Win32

  • 大于 8 字节的对象在内存中返回。

  • 当在内存中返回时,调用者将指向内存位置的指针作为第一个参数(隐藏)传递。被调用者填充内存,并返回指针。 被调用者在返回时从堆栈中弹出隐藏指针。

这可能是问题吗?有什么办法可以解决这个问题吗?还是我必须更改 Clang 的 Index.h 并切换到 stdCall?

[更新 3]

这里是对应的GCC-Bug。似乎在 4.6 (64bit) 和 4.7 (32bit) 中您可以使用新的 ms_abi 函数属性 来修复 [Update 2] 中描述的问题。

【问题讨论】:

  • 假设您对导入函数的 cdecl 假设是正确的,则此代码看起来不错。当您进入 __imp__clang_getTranslationUnitCursor 调用时,代码是什么样的?
  • 这是你的问题:ret 4。这不是 cdecl。
  • 好的,我在帖子中添加了调用的程序集。我实际上不知道我可以使用 MinGW dll 来实现它,但似乎我可以......
  • 我实际上认为 cdecl 是 gcc 的默认调用约定,当声明没有特别设置为另一个时。我用我发现的关于 cdecl 的新信息更新了我的帖子......
  • 我不熟悉 MinGW g++,但你必须找到两个平台之间 100% 兼容的调用约定。

标签: c++ visual-c++ x86 mingw calling-convention


【解决方案1】:

GCC 和 Visual C++ 不实现相同的 cdecl 调用约定。 Wikipedia explains:

对 cdecl 的解释存在一些变化,尤其是在如何返回值方面。因此,为不同操作系统平台和/或由不同编译器编译的 x86 程序可能不兼容,即使它们都使用“cdecl”约定并且不调用底层环境。 [...]为了传递“内存中”,调用者分配内存并将指向它的指针作为隐藏的第一个参数传递;被调用者填充内存并返回指针,返回时弹出隐藏指针。

最后一句话很重要:GCC 版本的 cdecl 使被调用者清除隐藏指针,而 Visual C++ 版本的 cdecl 将其留给调用者清除。

【讨论】:

    【解决方案2】:

    根据 clang 网站,在开始下,您可以使用 msvc 构建它,所以为什么不为自己省点麻烦并构建一个 msvc libclang,这将确保使用正确的调用约定和 ABI。或者,您可以使用 makefile 通过 msvc 使用 gcc 进行构建。

    【讨论】:

    • 过去几个月我使用 MSVC 构建,但现在我需要使用 C++ 标准库,这就是我构建 MinGW clang 的原因,所以它使用 MinGW 标头。不过,我会用 MSVC 以及 nostdinc 和 nostdinc++ cmdline 参数来试试我的运气,看看会发生什么......
    • @CodeSalad:如果您担心 STL,请使用 boost,它完全可移植,并且会给您一些有用的附加功能
    • @CodeSalad:MSVC 中的 C++ 标准库有什么问题?
    • @Necrolis:我正在使用 Clang 编写一个静态分析工具,所以我需要该工具来解析使用 STD 的代码
    • @Michael Burr:目前,Clang 无法解析 MSVC 附带的 STD,因为 MS STD 内部使用非标准 c++ 扩展
    【解决方案3】:

    您似乎在更新 #2 中发现了问题。如果您想在不修改 clang 库源代码的情况下解决此问题,您可以编写自己的包装函数,该函数使用两个编译器实际上同意通过内存缓冲区返回值的调用约定在 GCC 中编译:

    // compile this wrapper in MinGW - it will be able to call the 
    //  original clang_getTranslationUnitCursor() correctly, since
    //  it'll have the same idea of how __cdecl shoudl handle values
    //  returned in a memory buffer
    //
    // Since both MSVC and GCC __stdcall functions seem to handle return 
    //  values via memory in the same way, this wrapper should be callable 
    //  by MSVC
    
    CXCursor __stdcall wrapper_getTranslationUnitCursor(CXTranslationUnit tu)
    {
        return clang_getTranslationUnitCursor(tu);
    }
    

    希望您不必为太多功能执行此操作。

    另一种方法是在 MSVC 中编译一个包装器,该包装器使用汇编语言模块(或内联汇编)来处理调用约定的差异。

    我想知道 clang 是否会认为这是需要在他们的代码中修复的东西?

    【讨论】:

    • __stdcall 是跨编译器调用函数的答案。它适用于虚拟方法,甚至可以简化跨语言调用。
    猜你喜欢
    • 2012-03-17
    • 2023-03-28
    • 1970-01-01
    • 2015-04-16
    • 2014-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-19
    相关资源
    最近更新 更多