如何通过cdecl调用约定来破坏ESP?

时间:2012-02-13 17:36:50

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

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

库(libclang.dll)是使用MinGW编译的,我在VC ++项目中使用它。函数导出为C函数,Dependency Walker告诉我他们有正确的cdecl调用约定。使用dllimport将函数导入到我的项目中,包括Clang的“index.h”文件。看来,并非所有功能都会破坏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(64位)和4.7(32位)中,您可以使用新的 ms_abi函数属性来修复[Update 2]中描述的问题。

3 个答案:

答案 0 :(得分:8)

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

  

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

最后一句是重要的一句:GCC的cdecl版本使被调用者清理隐藏的指针,而Visual C ++的cdecl版本则让调用者清理它。

答案 1 :(得分:5)

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

答案 2 :(得分:4)

您似乎已在更新#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是否会考虑需要在代码中修复的内容?