【问题标题】:A better way to load DLL functions?加载 DLL 函数的更好方法?
【发布时间】:2013-08-03 03:55:51
【问题描述】:

我觉得有一种比在 typedef 中包含数百个签名然后通过 GetProcAddress 加载指针更好的方法。据我所知,在加载 DLL 函数时,它是最简单的——但也很脏。

有没有更简单的加载 DLL 函数的方法?具体来说,大量的Winapi和Tool Help库函数?我知道你可以只“包含一个 .lib”,但我觉得这会导致不必要的膨胀;我也无法访问源代码(尽管 Jason C 提到可以从 .dll 转到 .lib)。

我正在寻找为此编写一个库。我觉得主要的障碍是处理具有不同签名的函数。或者这正是为什么每个人都使用 typedef 而不是“一些奇特的循环”来加载他们的 DLL 函数的原因?

【问题讨论】:

  • 使用GetProcAddress 的唯一原因是如果您需要在运行时决定要加载哪个库(即,如果您有多个选择、插件,或者如果库不能保证存在) .否则,您应该使用.lib 并让系统负责库加载和链接。
  • @Saustin 是什么让你说导入库会导致不必要的膨胀?它只是一个帮助链接器将导出符号修复为 dll 的存根。如果没有目标文件引用它们,链接器将不会引入不必要的符号。
  • 所以您正在加载 Windows API 函数?为什么不像其他人一样将它们与 .lib 链接?函数签名已经在Windows.h!
  • 这很奇怪。我的意思是,您确实需要明确加载相当多的 Windows API 函数,但是我可以找到的对 CreateTool32Snapshot 的每个引用都表明它是在 kernel32.lib 中定义并在 tlhelp32.h 中声明的,并且从 XP 到支持8. 我从未使用过 CreateTool32Snapshot,所以我不确定,但根据我的经验,微软在描述此类特殊注意事项时通常是准确的。
  • 我应该补充一点,CreateTool32Snapshot 不存在.. 这是一个我严重忽略的错字。 CreateToolhelp32Snapshot 是正确的 API。此外,必须在 Windows.h 之后声明 TlHelp32.h。提醒自己:下次去睡觉。

标签: c++ c windows dll


【解决方案1】:

您的函数必须以某种方式声明,因此您需要函数签名。但是,如果你觉得你在复制函数签名,你可以使用decltype(&func_name) 来消除重复(给定一个具有相同目标签名的函数func_name)。您可以将其包装在一个宏中以使其更可口。

另一种常见的方法是将所有函数指针推入一个大结构(或一堆特定于任务的结构),并加载指向该结构的指针(例如,通过加载一个返回指向该结构的指针的函数) .如下所示:

通用标题

struct func_table {
    void (*f1)(int);
    int (*f2)(void);
};

DLL

static struct func_table table = {
    &f1_Implementation,
    &f2_Implementation,
};

DLLEXPORT struct func_table *getFuncTable(void) {
    return &table;
}

使用 DLL 的程序

static struct func_table *funcs;

void loadFuncTable() {
    struct func_table (*getFuncTable)(void) = GetProcAddress(..., "getFuncTable");
    funcs = getFuncTable();
}

/* call funcs->f1(n) and funcs->f2() */

【讨论】:

  • 要么我明显忽略了某些东西,要么使用函数表需要我访问 DLL 的源代码!对我来说,没有办法以兼容的方式实现它。
  • 我应该说清楚,我已经更新了我的问题。对不起。
  • 很遗憾发帖者没有说明他没有源,因为函数表结构是一个很好的方法。您仍然需要签名somewhere,但它会为您节省 GetProcAddress 调用。
  • 一个值得指出的问题,不过,对于函数表,如果你在 DLL 中添加/删除函数但想要保持二进制兼容性,你必须在 端添加新函数 ,如果你删除函数,你必须用相同大小的表中的占位符替换它们(例如,只保留它们并将它们设置为 NULL)。导入库不受 DLL 导出列表中的更改的影响,但必须通过它仔细维护函数表。 Win API 经常使用的进一步保护措施是为 DLL 指定结构的大小。
  • 在向/从 DLL 传递结构时,这是一种通用的故障保护。微软在 API 调用中经常这样做,尽管我想不出任何例子。使用上面的示例,如果 getFuncTable() 将结构的大小作为参数,或者如果结构有一个成员在调用之前设置为大小,则 DLL 可以确定您的应用程序是否是为不同版本的DLL 通过验证结构大小等于预期大小,否则采取适当措施(例如,如果结构在未来版本中缩小,则仅复制前 X 个字节)。
【解决方案2】:

链接到 .lib 文件的“膨胀”(通过“膨胀”我假设您的意思是可执行文件上的一些额外 kB,你知道......)如果你不是“不必要的”'重新使用它是为了避免处理数百个 GetProcAddress 调用。 :)

不太清楚你所说的“一些奇特的循环”是什么意思; 在此上下文中使用的 typedef 的作用与头文件中的声明类似——它们为编译器和人类读者提供有关被调用函数签名的信息。无论如何,你必须提供这个。

有一些工具可以从 .dll 生成 .lib;但是您仍然必须有一个标头来声明您正在调用的函数,以便编译器知道您在做什么。本质上,为 .dll 生成的 .lib 只是加载 DLL 并为您获取函数地址的“存根”。不同的 API,但本质上是一样的。

解决这个问题的唯一方法是设计不同的 DLL 接口,这样就不需要处理那么多函数。然而,在大多数情况下,我不会说避免函数的 typedefs/declarations 足以激励做这样的事情。例如。您可以只公开一个函数,例如(例如):

void PerformAction (LPCSTR action, DWORD parameter);

并让您的 PerformAction 实现根据“动作”做一些不同的事情。在某些与您的帖子无关的情况下,这当然是合理的,但对于您所描述的“问题”,这并不是真正合适的“解决方法”。

你几乎只需要处理它。使用声明创建标头并生成存根 .lib,或使用 typedef 创建标头并使用 LoadLibrary/GetProcAddress。前者将为您节省一些打字时间。后者可让您处理不存在 DLL 的情况,或加载在设计时未知的 DLL(例如另一个答案中提到的插件)。

【讨论】:

  • 有趣,我知道 .libs 包含代码,但我不知道有工具可以从 DLL 派生它们——我也不知道它们是廉价的存根。至于您对 PerformAction 的解释,我确实觉得这会是加载细节的更好方法,而不是一次删除所有 GetProcAddress 调用。
  • 使用 MSVC 有一个创建 .def 文件的中间步骤,但您可以直接使用 dumpbin.exe 列出符号来完成。然后您可以使用 lib.exe 从 .def 文件生成导入库(您实际上根本不需要 DLL 来执行此操作,只需要 .def)。在 MinGW 下,您可以使用 dlltool 代替 lib.exe。
  • 我不知道有像 lib.exe 这样的 MSVC 工具,我马上试试。
  • 传统上它正是用于此目的 - 为 DLL 创建导入库。如果您正在编写一个 DLL,并且您想在编译它时生成存根 LIB,您将创​​建一个 .def 文件列出它导出的函数,并告诉链接器为您创建导入库。您可以在该文件中添加其他内容:msdn.microsoft.com/en-us/library/28d6s79h%28v=vs.80%29.aspx。作为替代方案,您可以在您的函数上使用 __declspec(dllexport) (一个 MSVC 扩展):msdn.microsoft.com/en-us/library/a90k134d%28v=vs.80%29.aspx
  • 感谢您的帮助,您已经消除了我对这个主题的大量(常见?)误解。我的可执行文件上的“几个额外的 kB”听起来不再那么糟糕了。但与 TlHelp32.h 一样,有时您可能不得不“处理它”。
【解决方案3】:

Visual C++ 有一个delay load a DLL 选项。您在 DLL 的 lib 文件中链接的代码,包括其标头,并正常调用函数。 Visual C++ 链接器和运行时负责在第一次调用函数时调用 LoadLibrary() 和 GetProcAddress()。

但是,如果由于任何原因无法加载 DLL,这将导致 runtime exception,而不是像正常链接的 DLL 那样导致您的应用程序无法加载。

如果您的应用程序始终加载并需要此 DLL 来运行,那么您应该正常使用 DLL(使用 .lib 文件)。在这种情况下,延迟加载 DLL 将一无所获。

【讨论】:

  • 有趣.. 那么这是否意味着必须让 DLL 加载的唯一方法是在路径内?在我看来,这是在调用 WinMain/main 时完成的——这意味着无法说“嘿,从这个文件加载这个 dll——不是那个!”也就是说,除非您编写了一些为您放置 DLL 的包装可执行文件。
  • @Saustin 如果 DLL 不在 DLL 搜索路径中,那么您仍然可以通过在 dliStartProcessing 通知中自己调用 LoadLibary() 并返回 HMODULE 来处理它。 msdn.microsoft.com/en-us/library/z9h1h6ty.aspx
  • 这感觉是正确的路径,但我感到很困惑——我是在使用通知挂钩来更新进程的地址表,还是在加载时为我完成?我可能需要稍微修改一下。
  • @Saustin - 您只需使用正确的路径调用LoadLibary 来手动加载DLL。之后,运行时将为您处理加载 DLL 的函数。
  • 我明白了——所以这种方法的主要优点是能够控制何时加载哪个 DLL?我知道这仍然需要链接 .lib,但我也在重新考虑使用静态库链接。
【解决方案4】:

加载 Windows 库有几种不同的方式,但每种方式都有不同的用途。

手动使用LoadLibrary()GetProcAddress() 旨在允许关于几种不同动态链接情况的运行时决策。这些调用只是简单的系统调用,不会影响生成的 PE 文件的任何方面,GetProcAddress() 返回的地址在代码生成方面没有任何特殊之处。这意味着调用者有责任在语法层面正确使用这些符号(例如,为函数调用使用正确的调用约定以及参数的正确数量、大小和对齐方式(如果有))。

链接到与.dll 关联的.lib 文件是不同的。客户端代码使用外部标识符引用运行时内容,就好像它们是静态链接的符号一样。在构建过程中,链接器将使用.lib 文件中的符号解析这些标识符。虽然原则上这些符号可以指向任何东西(就像任何其他符号一样),自动生成的.lib 文件将简单地提供充当微型代理的符号 到将由 PE 加载器在加载时填充的内存内容。

这些代理的实现方式取决于预期的内存访问类型。例如,引用.dll 中的函数的符号将被实现为单个间接jmp 指令,这样来自客户端代码的原始call 指令将命中来自客户端代码的jmp 指令.lib 内容并将被转发到 PE 加载器在加载时解析的地址。动态加载的函数代码位于该地址。

除了更多细节之外,所有这些都用于指示 PE 加载器执行与客户端代码自行完成相同的工作:调用 LoadLibrary() 将 PE 映射到内存中,然后使用GetProcAddress() 挖掘导出表,以找到并计算每个所需符号的正确地址。 PE 加载器在加载时根据客户端可执行文件的导入表中的信息自动执行此操作。

换句话说,load-time 动态链接比 run-time 动态链接更慢和更胖,如果有的话,但它的目的是提供对普通开发人员来说更简单的编程接口,特别是因为许多库总是需要并且总是可用的。在这些情况下,尝试通过手动加载提供任何额外的逻辑是没有用的。

总结一下,不要仅仅因为它们似乎提供了更高的性能或鲁棒性而费心使用LoadLibrary()GetProcAddress()。尽可能链接.lib 文件。

进一步讨论这个话题,甚至可以创建根本不包含 任何 导入表的 PE 映像(并且仍然可以访问系统调用和其他导出例程)。恶意软件使用这种方法通过剥离任何加载时间信息来隐藏可疑的 API 使用(例如 Winsock 调用)。

【讨论】:

    【解决方案5】:

    创建一个类作为 dll 的包装器,其中类的公共成员函数只不过是指向通过 get proc address 获得的 dll 中的函数的指针,这是从 dll 加载函数的一种好方法。

    最好使用 c++ 的 RAII 能力来释放通过释放类的析构函数中的库加载的库。

    这个:

    typedef int(WINAPI *ShellAboutProc)(HWND, LPCSTR, LPCSTR, HICON);
    
    int main() {
      HMODULE hModule = LoadLibrary(TEXT("Shell32.dll"));
    
      ShellAboutProc shellAbout =
          (ShellAboutProc)GetProcAddress(hModule, "ShellAboutA");
    
      shellAbout(NULL, "hello", "world", NULL);
    
      FreeLibrary(hModule);
    }
    

    可以这样实现:

    class ShellApi {
      DllHelper _dll{"Shell32.dll"};
    
    public:
      decltype(ShellAboutA) *shellAbout = _dll["ShellAboutA"];
    };
    
    int main() {
      ShellApi shellApi;
      shellApi.shellAbout(NULL, "hello", "world", NULL);
    }
    

    DllHelper 类在哪里:

    class ProcPtr {
    public:
      explicit ProcPtr(FARPROC ptr) : _ptr(ptr) {}
    
      template <typename T, typename = std::enable_if_t<std::is_function_v<T>>>
      operator T *() const {
        return reinterpret_cast<T *>(_ptr);
      }
    
    private:
      FARPROC _ptr;
    };
    
    class DllHelper {
    public:
      explicit DllHelper(LPCTSTR filename) : _module(LoadLibrary(filename)) {}
    
      ~DllHelper() { FreeLibrary(_module); }
    
      ProcPtr operator[](LPCSTR proc_name) const {
        return ProcPtr(GetProcAddress(_module, proc_name));
      }
    
      static HMODULE _parent_module;
    
    private:
      HMODULE _module;
    };
    

    所有致谢Benoit

    我找不到比这更优雅的方式了。

    【讨论】:

      【解决方案6】:

      应该提供一个函数来手动加载给定文件名的 dll。在 Windows API 中,它是 LoadLibrary()。除非您正在开发“插件”样式的应用程序或者您需要手动控制加载和卸载,否则通常不会使用它。

      您应该考虑如何使用您的代码来确定静态库还是动态加载的 dll 是满足您要求的最佳解决方案。

      【讨论】:

      • LoadLibrary 将 DLL 加载到内存中;仍然需要 GetProcAddress 才能找到相关函数的地址。
      猜你喜欢
      • 1970-01-01
      • 2012-06-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多