【问题标题】:x64 DLL export function namesx64 DLL 导出函数名称
【发布时间】:2015-03-19 16:34:42
【问题描述】:

我正在尝试将 32 位 dll(和应用程序)移植到 64 位,并且我成功地构建了它而没有错误。当尝试用我的 64 位应用程序加载它时,我注意到导出的函数名称不同。这就是我导出函数的方式:

#ifdef __cplusplus
extern "C" {
#endif

__declspec(dllexport) long __stdcall Connect(char * name, long size);

#ifdef __cplusplus 
}
#endif

在 Dependency Walker 中,导出的函数具有以下格式:

32 位:_Connect@8

64 位:Connect

在使用 dll 的应用程序中,我显式加载了 dll(LoadLibrary 成功),但 GetProcAddress 在 64 位时失败,因为它找不到具有提供名称的函数。

在我们的应用程序中,我将函数名称保留如下:

#define ConnectName "_Connect@8"
...
GetProcAddress(Dll, ConnectName);

所以我想知道是否可以为 32 位和 64 位 dll 导出相同的函数名称,或者这是一个坏主意?或者我是否需要在我的应用程序中执行以下操作:

#if _WIN64
#define ConnectName "Connect"
#else
#define ConnectName "_Connect@8"
#endif

感谢您的帮助。

【问题讨论】:

    标签: c++ dll 64-bit name-mangling


    【解决方案1】:

    对于 Win32 构建:

    如果你使用__stdcall,你会得到这样的东西(用dumpbin /exports转储):

    __declspec(dllexport) int __stdcall
    
    ->
    
       ordinal hint RVA      name
    
              1    0 00001240 _F1@0 = _F1@0
              2    1 0000124D _F2@0 = _F2@0
    
    

    而你必须使用GetProcAddress("_F1@0")来定位函数指针。

    如果你使用__cdecl,你会得到这样的:

    __declspec(dllexport) int __cdecl
    
    ->
    
       ordinal hint RVA      name
    
              1    0 00001240 F1 = _F1
              2    1 0000124D F2 = _F2
    
    

    你可以使用GetProcAddress("F1")来定位函数指针。

    顺便说一句,如果您将 XXX.def 文件添加到您的 Visual Studio 项目。另一个链接选项将在All Options 窗口中以静默方式添加到您的链接器命令行/DEF:"XXX.def"。如果您稍后出于某种原因更改了 .def 文件名,则此链接选项不会相应更改。您需要在项目属性窗口中手动更改def文件名。

    【讨论】:

      【解决方案2】:

      一个选项,您必须在没有任何修饰的情况下导出函数名称(独立来自您在 x86、__stdcall__cdecl 或其他中使用的特定调用约定)并使用 x86 和 x64 版本中相同的未修饰名称,是使用 DEF files 导出您的 DLL 函数。

      例如您可以将这样的 .DEF 文件添加到您的项目中:

      LIBRARY YOURDLL
      EXPORTS
         Connect          @1
         AnotherFunction  @2
         ... etc. ...   
      

      复制关注

      在 Visual Studio 中创建一个空的解决方案(我使用 VS2013),并在其中创建一个空的 Win32 控制台项目(测试客户端)和一个空的 Win32 DLL 项目(测试 DLL )。

      DLL 项目中添加这个NativeDll.def .DEF 文件

      LIBRARY NATIVEDLL
      EXPORTS
          SayHello @1
      

      DLL项目中添加这个NativeDll.cppC++源代码:

      ///////////////////////////////////////////////////////////////////////////////
      // 
      // NativeDll.cpp    -- DLL Implementation Code
      //
      ///////////////////////////////////////////////////////////////////////////////
      
      
      #include <Windows.h>
      #include <atldef.h>
      #include <atlstr.h>
      
      
      //
      // Test function exported from the DLL
      // 
      extern "C" HRESULT WINAPI SayHello(PCWSTR name)
      {
          //
          // Check for null input string pointer
          //
          if (name == nullptr)
          {
              return E_POINTER;
          }
      
          try
          {
              //
              // Build a greeting message and show it in a message box
              //
              CString message;
              message.Format(L"Hello %s from the native DLL!", name);        
              MessageBox(nullptr, message, L"Native DLL Test", MB_OK);
      
              // All right
              return S_OK;
          }
          //
          // Catch exceptions and convert them to HRESULT codes
          //
          catch (const CAtlException& ex)
          {
              return static_cast<HRESULT>(ex);
          }
          catch (...)
          {
              return E_FAIL;
          }
      }
      

      客户端测试项目中添加这个NativeClient.cppC++源代码:

      ///////////////////////////////////////////////////////////////////////////////
      //
      // NativeClient.cpp     -- EXE Test Client Code
      //
      ///////////////////////////////////////////////////////////////////////////////
      
      
      #include <Windows.h>
      
      
      //
      // Prototype of the function to be loaded from the DLL
      //
      typedef HRESULT (WINAPI *SayHelloFuncPtr)(PCWSTR /* name */);
      
      
      //
      // Simple RAII wrapper on LoadLibrary()/FreeLibrary().
      //
      class ScopedDll
      {
      public:
      
          //
          // Load the DLL
          //
          ScopedDll(PCWSTR dllFilename) throw()
              : m_hDll(LoadLibrary(dllFilename))
          {
          }
      
      
          //
          // Unload the DLL
          //
          ~ScopedDll() throw()
          {
              if (m_hDll)
              {
                  FreeLibrary(m_hDll);
              }
          }
      
      
          //
          // Was the DLL loaded successfully?
          //
          explicit operator bool() const throw()
          {
              return (m_hDll != nullptr);
          }
      
      
          //
          // Get the DLL handle
          //
          HINSTANCE Get() const throw()
          {
              return m_hDll;
          }
      
      
          //
          // *** IMPLEMENTATION ***
          //
      private:
      
          //
          // The wrapped raw DLL handle
          //
          HINSTANCE m_hDll;
      
      
          //
          // Ban copy
          //
      private:
          ScopedDll(const ScopedDll&) = delete;
          ScopedDll& operator=(const ScopedDll&) = delete;
      };
      
      
      //
      // Display an error message box
      //
      inline void ErrorMessage(PCWSTR errorMessage) throw()
      {
          MessageBox(nullptr, errorMessage, L"*** ERROR ***", MB_OK | MB_ICONERROR);
      }
      
      
      //
      // Test code calling the DLL function via LoadLibrary()/GetProcAddress()
      //
      int main()
      {
          //
          // Return codes
          //
          static const int kExitOk = 0;
          static const int kExitError = 1;
      
      
          //
          // Load the DLL with LoadLibrary().
          // 
          // NOTE: FreeLibrary() automatically called thanks to RAII!
          //
          ScopedDll dll(L"NativeDll.dll");
          if (!dll)
          {
              ErrorMessage(L"Can't load the DLL.");
              return kExitError;
          }
      
      
          //
          // Use GetProcAddress() to access the DLL test function.
          // Note the *undecorated* "SayHello" function name!!
          //
          SayHelloFuncPtr pSayHello 
              = reinterpret_cast<SayHelloFuncPtr>(GetProcAddress(dll.Get(), 
                                                                 "SayHello"));
          if (pSayHello == nullptr)
          {
              ErrorMessage(L"GetProcAddress() failed.");
              return kExitError;
          }
      
      
          //
          // Call the DLL test function
          //
          HRESULT hr = pSayHello(L"Connie");
          if (FAILED(hr))
          {
              ErrorMessage(L"DLL function call returned failure HRESULT.");
              return kExitError;
          }
      
      
          //
          // All right
          //
          return kExitOk;
      }
      

      构建整个解决方案(.EXE 和 .DLL)并运行本机 .EXE 客户端。
      这是我在我的电脑上得到的:

      无需修改并且在 x86 和 x64 构建未修饰函数名称(只是 SayHello)即可工作。

      【讨论】:

      • 因此,如果我理解正确,我只需将其添加到 dll 项目中,然后我的应用程序和使用 dll 的 C# PInvokes 将无需更改即可工作?如果是这样,与其他提议的解决方案相比,此解决方案有什么缺点吗?
      • @dbostream:要从原生 C++ DLL 导出具有纯 C 接口的函数,我发现 .DEF 文件可以方便地获取 未修饰 函数名。
      • @Mr.C64:我的立场是正确的。添加序数确实会使link 取消装饰符号名称。正如我所说,我上次必须处理 DEF 文件是在 很久 之前(还记得 FAR PASCAL 声明吗?)。删除之前的评论,但我坚持认为 DEF 文件通常是一个巨大的 PITA(特别是因为我的大部分开发都是跨平台的)。哦,MS 文档显然是完全错误的(这并不奇怪)。
      • 好吧,如果使用 DEF 文件,仍然需要将 PInvoke 更改为 __cdecl 或使用 DllImport 的 EntryPoint 字段指定未修饰的名称?除非我错过了一些东西,否则我觉得只更改为__cdecl 比为我拥有的每个 dll 都创建一个 DEF 文件的工作量更少,特别是因为我以前从未使用过 DEF 文件。
      • @frasnian:好的,没问题,我也删除了我的评论以回复您删除的评论:)
      【解决方案3】:

      如您所知,在 64 位 Windows 中,名称没有被修饰。

      在 32 位 __cdecl__stdcall 符号中,符号名称前加下划线。示例函数的 32 位版本的导出名称中的尾随“@8”是参数列表中的字节数。它在那里是因为您指定了__stdcall。如果您使用__cdecl 调用约定(C/C++ 代码的默认值),您将无法理解。如果您使用__cdecl,则可以更轻松地使用以下内容包装GetProcAddress()

      #if _WIN64
      #define DecorateSymbolName(s)   s
      #else
      #define DecorateSymbolName(s)   "_" ## s
      #endif
      

      然后只需调用

      pfnConnect   = GetProcAddress(hDLL, DecorateSymbolName("Connect"));
      pfnOtherFunc = GetProcAddress(hDLL, DecorateSymbolName("OtherFunc"));
      

      或类似的东西(示例中省略了错误检查)。 为此,请记住将导出的函数声明为:

      __declspec(dllexport) long __cdecl Connect(char * name, long size);
      __declspec(dllexport) long __cdecl OtherFunc(int someValue);
      

      除了更易于维护之外,如果在开发过程中导出函数的签名发生变化,您不必乱用 #define 包装器。

      缺点:如果在开发过程中给定函数的参数列表中的字节数发生了变化,那么导入函数的应用程序将不会捕获它,因为更改签名不会更改名称。就个人而言,我不认为这是一个问题,因为 64 位版本无论如何都会在相同的情况下崩溃,因为名称没有被修饰。您只需确保您的应用程序使用的是正确版本的 DLL。

      如果 DLL 的用户使用 C++,您可以使用 C++ 功能以更好的方式包装事物(例如,将整个显式加载的库包装在包装类中):

      class MyDLLWrapper {
      public:
        MyDLLWrapper(const std::string& moduleName);  // load library here
        ~MyDLLWrapper();                              // free library here
      
        FARPROC WINAPI getProcAddress(const std::string& symbolName) const {
          return ::GetProcAddress(m_hModule, decorateSymbolName(symbolName));
        }
        // etc., etc.
      private:
        HMODULE m_hModule;
        // etc.
        // ...
      };
      

      实际上你可以用这样的包装类做更多的事情,这只是一个例子。

      编辑时:由于 OP 提到在 cmets 中使用 PInvoke - 如果有人决定这样做,不要忘记在使用 PInvoke 时在 [DllImport] 声明中添加 CallingConvention = CallingConvention.Cdecl__cdecl 可能是非托管 C/C++ 的默认值,但不是托管代码的默认值。

      【讨论】:

      • 谢谢,我喜欢这个想法,不过有一个问题。更改为 __cdecl 会对使用 dll 的软件产生任何副作用吗?我们的工具套件中有几个 dll 和应用程序必须更改,因为我们在任何地方都使用 stdcall。此外,我们还有 C# dll,可以 PInvoke 非托管 dll(当前使用 stdcall)是只是将调用约定更改为 cdecl 的问题,还是会出现其他问题,因为使用 32 位和 64 位时导出的名称会有所不同。
      • 如果您更改包含导出函数的新(不同调用约定)声明的头文件并重新构建 DLL,您只需重新构建使用这些导出函数的所有内容,以便它们也使用新调用习俗。如果每个人都在同一个页面上,按照惯例,你应该没问题。
      • 我将 Connect 函数更改为 __cdecl 并使用 Dependency Walker 现在为 32 位和 64 位 dll 显示相同的名称,即 Connect。如果我正确理解link,我不会得到前缀下划线,因为我使用extern "C";因此我不需要DecorateSymbolName。这看起来合理还是我做错了什么?
      • 不,这是意料之中的。 DependencyWalker 理解名称修饰(没有检查,但它可能使用UnDecorateSymbolName() - msdn.microsoft.com/en-us/library/windows/desktop/…
      【解决方案4】:

      __stdcall 在 x64 上不受支持(并且被忽略)。引用MSDN:

      在 ARM 和 x64 处理器上,__stdcall 被编译器接受并忽略;在 ARM 和 x64 架构上,按照惯例,参数尽可能在寄存器中传递,后续参数在堆栈中传递。

      x64 上的调用约定是pretty much __fastcall

      由于 x86 和 x64 上的调用约定和名称修饰规则不同,因此您必须以某种方式对其进行抽象。所以你对#if _WIN64 的想法是朝着正确的方向发展的。

      您可以检查 x86 调用约定和您的需求,也许可以设计一个可以自动执行名称选择过程的宏。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-06-25
        • 1970-01-01
        • 1970-01-01
        • 2011-01-02
        • 2012-12-20
        • 2011-01-31
        • 2010-09-28
        相关资源
        最近更新 更多