如何在运行时“启动”像可执行文件的DLL?

时间:2016-12-02 19:07:06

标签: c++ dll loadlibrary getprocaddress

我想编写一个非常非常小的程序来解析启动参数并选择几个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。

3 个答案:

答案 0 :(得分:2)

必须使用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;
    }
}

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

/delayload还有另外一个优点:它可以避免将其他DLL映射到地址,如果它们从未使用过的话。听起来任何给定的调用只会使用一个DLL,所以这可能是你的胜利。

答案 1 :(得分:1)

首先,将项目类型从可执行文件更改为DLL不足以构建DLL。您还需要导出一些符号来创建API。至少,您需要使用__declspec(dllexport)修饰要导出的函数。但是,我建议您导出C API,这意味着extern "C"函数与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 **);

答案 2 :(得分:1)

需要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 Loader),你需要通过返回正常的程序操作来释放它

这意味着如果你想使用DllMain作为你的程序的入口点,它需要生成一个线程,你的原始main方法必须不会返回,直到该线程完成...

  

选项#2

     
    

DLL导出main函数。

         
      

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

  

在您的DLL中,导出 main

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

在你的程序中,从DLL中导入 main

// 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将致电ExitProcess (...)

导出函数并稍后通过名称导入它们可以避免围绕Windows DLL加载程序进行pussyfooting。但是,如果您不使用.def文件显式命名导出的符号,编译器将添加_... __ cdecl )或{{1}等装饰。 ( __ stdcall )到名称,您必须学习这些约定,以便对...@n执行任何有用的操作。