【问题标题】:How can I "start" a DLL like an executable at runtime?如何在运行时像可执行文件一样“启动”DLL?
【发布时间】:2016-12-02 19:07:05
【问题描述】:

我想编写一个非常非常小的程序来解析启动参数并选择几个 DLL 中的一个来“启动”。

我已经编写了一个应用程序,我想将其作为 DLL “运行”,方法是将其编写为应用程序,然后更改 Visual Studio 项目属性以将其构建为 DLL。我知道我需要同时使用 LoadLibrary 和 GetProcAddress 来获得我想要的功能,但是我很难找到关于这方面的清晰和全面的文档,因为很多用例并不是真正的这种性质。另外,我必须根据项目和平台限制走这条路线。

我找到了this page,其中有一些信息,但不够清楚,我无法适应我的目的。

编辑:这就是我现在所处的位置。

我有一个 DLL 项目,其主函数签名如下所示:

__declspec(dllexport) int cdecl main(int argc, char *argv[])

我还有一个应用程序项目,它尝试加载 DLL 并运行上述函数如下所示:

typedef int (CALLBACK* LPFNDLLFUNC1)(int, char *);

...

        HMODULE dllHandle = NULL;
        BOOL freeResult, runTimeLinkSuccess = FALSE;
        LPFNDLLFUNC1 lpfnDllFunc1;    // Function pointer  
        if (args->IsEmpty())
        {
            dllHandle = LoadLibrary(L"TrueApplication.dll");
            if (NULL != dllHandle)
            {
                lpfnDllFunc1 = (LPFNDLLFUNC1)GetProcAddress(dllHandle, "main");
                if (lpfnDllFunc1)
                {
                    int retVal = lpfnDllFunc1(0, "1");
                }

目前,LoadLibrary 调用有效,但 GetProcAddress 无效。

【问题讨论】:

  • 您认为将其设为 DLL 而不是 EXE 的优势是什么?我们必须知道您的推理,因此我们不推荐“取消”您正在寻找的某些 DLL 优势的解决方案。
  • 使用这种方法既不是我的选择,也不是我的想法,但鉴于平台和项目的限制,这是我必须要做的。本质上,如果我使用多个可执行文件而不是 DLL,我将不得不对这个平台进行等效于为每个可执行文件完全拥有不同的应用程序条目。例如,用户必须安装“Launcher”应用以及“App 1”、“App 2”等。

标签: c++ dll loadlibrary getprocaddress


【解决方案1】:

首先,将项目类型从可执行文件更改为 DLL 不足以生成 DLL。您还需要导出一些符号来创建您的 API。至少,你需要用__declspec(dllexport) 装饰你正在导出的函数。但是,我建议您导出 C API,即带有 C 兼容参数的 extern "C" 函数。因此,您导出的函数应该在前面加上extern "C" __declspec(dllexport)

完成此操作后,您可以像这样动态加载 DLL:

   const char* dllname = "myfile.dll";
   h = LoadLibrary(dllname);
   if( h == nullptr )
   {
       /*handle error*/
   }

   using myfunc_type = bool (*)(int x, double y); //example
   auto myfunc = reinterpret_cast<myfunc_type>(GetProcAddress(h, "myfunc"));       
   //......
   myfunc(x,y); //call the imported function

此解决方案比 Jerry Coffin 展示的使用 /delayload 静态加载需要更多的工作,但它有一个优势:如果需要 DLL 但未找到,您可以向用户提供您自己的错误消息,而不是依赖即将到来的消息来自 Windows(这对于非技术人员来说通常是不可接受的)。您还可以在 API 中包含 API 版本验证及其自己的自定义错误消息。

编辑:如果您像这样更改代码示例,它将起作用

extern "C" __declspec(dllexport) int main(int argc, char *argv[]){...}
typedef int (* LPFNDLLFUNC1)(int, char **);

【讨论】:

  • 延迟加载还允许捕获错误并显示自定义消息。
  • @BenVoigt 你能指点我做这件事的指南吗?另外,是否可以为版本不匹配创建自定义错误消息(当 DLL 的 API 随时间变化时)?
  • 那么,对于 DLL 的 main 函数,我想要做的是这样的事情,对吧? using myfunc_type = int(*)(int x, double y); //example auto myfunc = reinterpret_cast&lt;myfunc_type&gt;(GetProcAddress(h, "main"));
  • @Eugene:MSDN 上的这个页面是编写代码以捕获延迟加载错误的起点:msdn.microsoft.com/en-us/library/3aeywt27.aspx 您可以分别捕获“找不到库文件”与“找不到函数”,这应该允许捕获一些版本控制问题,但最好有一个返回版本号的函数。
  • @VGambit 是的,如果您导出了名为 main 的函数。我不确定它是否会起作用,但肯定会令人困惑。我宁愿重命名它。
【解决方案2】:

不需要需要GetProcAddress (...) 来执行此操作,但在您了解编译器如何生成符号名称后,该方法(选项#2)会更简单。


选项#1

DllMain 产生一个主线程

永远不要在 DllMain 中做任何复杂的事情,你可能会死锁你的软件。

DLL 有自己的入口点(以及出口点和线程附加点……这是一个非常繁忙的功能)。只需在您的 DLL 上调用 LoadLibrary (...) 至少会导致调用 DllMain (...) 以进行进程附加。

BOOL
APIENTRY
DllMain ( HMODULE hModule,
          DWORD   ul_reason_for_call,
          LPVOID  lpReserved )

您实际上可以将ul_reason_for_call == DLL_PROCESS_ATTACH 视为执行 DllMain 的命令,就好像它是您程序的主要函数一样。

现在,您绝对不应该在此处实际启动程序循环...DllMain 运行时,它会持有一个非常重要的操作系统锁(DLL 加载程序),您需要通过返回正常程序操作来释放它。

这意味着如果您想使用 DllMain 作为程序的入口点,它需要生成一个线程,并且您原来的 main 方法在该线程完成之前不得返回...


选项 #2

DLL 导出一个main 函数。

非常注意调用约定,编译器会为您重命名符号,并使在GetProcAddress 的 DLL 中定位函数不够直观。

在你的 DLL 中,export main:

__declspec (dllexport)
int
__cdecl main (int argc, char *argv [])
{
  printf ("foobar");
  return 0;
}

在您的程序中,导入 main 从 DLL:

// Need a typedef for the function you are going to get from the DLL
typedef int (__cdecl *main_pfn)(int argc, char *argv[]);

int main (int argc, char *argv[])
{
  HMODULE hModMyDLL = LoadLibraryA ("MyDll.dll");

  if (hModMyDLL != 0) {
    //
    // The preceding underscore deals with automatic decorations
    //   the compiler added to the __cdecl function name.
    //
    //  It is possible to do away with this completely if you use a .def
    //    file to assign export names and ordinals manually, but then you
    //      lose the ability to tell a function's calling convention by its
    //        name alone.
    //
    main_pfn MyMain = (main_pfn)
      GetProcAddress (hModMyDLL, "_main");

    // Call the main function in your DLL and return when it does
    if (MyMain != nullptr)
      return MyMain (argc, argv);
  }

  return -1;
}

这两种方法各有千秋。

DllMain 生成一个线程可以避免完全了解您要加载的 DLL 是如何实现的,但它还需要您设计您的 main 函数永远不会返回 - DLL 将调用 @987654337 @。

导出函数并稍后按名称导入它们可以让您避免在 Windows DLL 加载程序锁周围束手无策。但是,如果您不使用 .def 文件显式命名导出的符号,编译器将添加诸如 _... (__cdecl) 或 ...@n (__stdcall ) 到名称,你必须学习这些约定才能对GetProcAddress 做任何有用的事情。

【讨论】:

  • 我应该指出,你可以在 Win32 API 函数上使用它们未修饰的名称使用 GetProcAddress (...) 的原因(尽管 WINAPI 被定义为 __stdcall)是因为 Microsoft 使用.def 给像user32 这样的DLL 提供它们的导出名称。他们特意去掉了调用约定的装饰,这样GetProcAddress 的使用负担就减轻了。你也可以这样做。
【解决方案3】:

不必必须使用LoadLibraryGetProcAddress 来调用DLL 中的功能。

更多时候,您会创建自己的 DLL,每个 DLL 都有自己的入口点。目前,假设您要解析命令行,选择一个 DLL,并在不带参数的情况下调用其入口点。你最终会得到这样的结果:

void DLL_a();
void DLL_b();
void DLL_c();

int main(int argc, char **argv) { 
    // we'll assume DLL_a is the default:
    if (argc < 2) 
        DLL_a();

    // For now, we'll do a *really* trivial version of parsing the command line
    // to choose the right DLL:
    if (argv[1][0] == 'A')
        DLL_a();
    else if (argv[1]][0] == 'B')
        DLL_b();
    else if (argv[1][0] == 'C')
        DLL_c();
    else {
        std::cerr << "Unrecognized argument\n";
        return 1;
    }
}

当您链接您的主文件时,您将指定与每个 DLL 对应的 .lib,并且您可能希望将 /delayload 标志指定给链接器。这意味着在实际调用 DLL 中的函数之前不会加载 DLL。例如,如果您想分发仅包含 DLL A 的程序的功能缩减版本,只要 DLL B 没有任何功能,它仍然可以运行(用户系统上不存在 DLL B 或 C)或者 C 曾经被调用过。如果您不指定/delayload,加载程序将在程序启动时尝试将所有DLL映射到RAM,运行它们的DllMain来初始化它们以供使用,对它们依赖的所有DLL递归地执行相同的操作,等等

/delayload 有另一个优点:它可以避免将其他 DLL 映射到从不使用的地址。听起来任何给定的调用都只会使用一个 DLL,所以这对你来说可能是一个胜利。

【讨论】:

  • 不仅内存映射,而且 DllMain(和全局对象的构造函数)都不会为未选择的 DLL 运行。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-18
  • 1970-01-01
  • 2013-12-29
  • 1970-01-01
  • 2023-03-31
相关资源
最近更新 更多