如何链接不同 DLL 中的相同函数名

2023年 9月 2日 76.5k 0

对于典型的 DLL 函数调用,您声明函数原型(通过头文件),通知链接编辑器 ( , ldlinkDLL 导出具有该名称的符号(导入库),它将声明的名称与此导出相匹配,并且它成为程序导入表中的导入。当两个不同的 DLL 导出相同的符号时会发生什么?链接编辑器将选择第一个找到的。但是如果您想同时使用这两个 导出怎么办?如果它们具有相同的名称,程序或链接编辑器如何区分它们?在本文中,我将演示一种解决此问题的技术,方法是创建一个程序,该程序同时链接并直接使用两个不同的 C 运行时 (CRT)。

在PE可执行映像中,导入不仅仅是一个符号,而是DLL名称和符号的元组。对于人类显示,元组通常使用感叹号分隔符进行格式化,如 中msvcrt.dll!malloc,但有时没有.dll后缀。您可能已经在堆栈跟踪中看到过这一点。因为它是一个元组而不仅仅是一个符号,所以可以从不同的 DLL 引用和导入相同的符号。与 ELF 相比,ELF 有一个共享对象列表和一个单独的符号列表,动态链接器在加载时将它们配对。这允许像 一样很酷的技巧LD_PRELOAD,但出于同样的原因,加载不太可预测。

Windows 附带了多种 CRT,各种库和应用程序根据它们的构建方式使用其中一种(或不使用)。作为 C 标准库实现,它们导出大部分相同的符号、 mallocprintf等。通过作为元组导入,应用程序一次加载多个 CRT 并不罕见。通常,共存是可传递的。也就是说,一个模块并不直接访问两个 CRT,而是依赖于使用不同 CRT 的模块。一个模块调用,例如,, msvcrt.dll!malloc另一个模块调用ucrtbase.dll!malloc。对于 DLL 限定的符号,只要模块不交叉流,这就是合理的,例如,一个模块中的分配不得在另一个模块中释放。此生态系统中的库必须避免通过其接口暴露其 CRT,例如期望库的调用者访问free()对象:调用者可能无法访问正确的free

再次与 UNIX 生态系统进行对比,其中一个进程只能加载一个 libc,并且每个人都应该共享。库通常期望调用者调用free()它们的对象(例如libreadline、xcb),将它们的接口与 libc 混合。

假设您处于这样的情况,由于面向 UNIX 的库,您的应用程序必须同时使用来自两个不同 CRT 的函数。一种可能是使用 Mingw-w64 编译并与 MSVCRT 链接,另一种可能是使用 MSVC 编译并与 UCRT 链接。我们需要在每个中调用malloc 和free,但它们具有相同的名称。真是个泡菜啊!

有一个明显的、可能也是最常见的解决方案:运行时动态链接。在一个 CRT 上使用加载时链接,并在另一 CRT 上使用 LoadLibrary 和 GetProcAddress 来获取函数指针。但是,完全可以通过加载时链接来完成此操作!

任何其他名称的 malloc 也会分配

想一想,您可能会想:如果名字相同,我如何选择我要称呼的名字?元组表示形式不起作用,因为它!不能出现在标识符中,这就是选择它的原因。诀窍是我们要重命名其中一个!为了进行演示,我将使用我的 Windows 开发工具包w64devkit ,它是一个链接 MSVCRT 的 Mingw-w64 发行版。我将使用 UCRT 作为第二个 CRT 来访问ucrtbase.dll!malloc.

我可以选择任何我想要的有效标识符,所以我将选择 ucrt_malloc. 这将需要一份声明:

__declspec(dllimport) void *ucrt_malloc(size_t);

如果我停在这里并尝试使用它,当然它不会工作:

ld: undefined reference to `__imp_ucrt_malloc'

链接器尚未获悉管理方面的变化。为此,我们需要一个导入库。我将使用.def 文件定义一个,我将其命名为ucrtbase.def

LIBRARY ucrtbase.dll
EXPORTS
ucrt_malloc == malloc

最后一行表示该库具有符号ucrt_malloc,但应将其导入为malloc。这条线是整个方案的关键。注意:双等号很重要,因为单个等号意味着不同的东西。接下来,使用dlltool构建导入库:

$ dlltool -d ucrtbase.def -l ucrtbase.lib

等效的 MSVC 工具是lib,但据我所知它不能完全完成这种重命名。然而,MSVClink将与这个创建的导入库一起正常工作dlltool。这个名字ucrtbase.lib虽然显而易见,但却无关紧要。正是这条LIBRARY线将它与 DLL 联系起来。我的测试源文件如下所示:

#include <stdlib.h>

__declspec(dllimport) void *ucrt_malloc(size_t);
int main(void)
{
void *msvcrt[] = {malloc(1), malloc(1), malloc(1)};
void *ucrt[] = {ucrt_malloc(1), ucrt_malloc(1), ucrt_malloc(1)};
return 0;
}

则编译成功:

$ cc -g3 -o main.exe main.c ucrtbase.lib

我可以看到两个malloc导入objdump

$ objdump -p main.exe
...
DLL Name: msvcrt.dll
...
844a	 1021  malloc
...
DLL Name: ucrtbase.dll
847e	    1  malloc

它也成功加载并运行:

$ gdb main.exe
Reading symbols from main.exe...
(gdb) break 9
Breakpoint 1 at 0x1400013cd: file main.c, line 9.
(gdb) run
Thread 1 hit Breakpoint 1, main () at main.c:9
9           return 0;
(gdb) p msvcrt
$1 = {0xd06a30, 0xd06a70, 0xd06ab0}
(gdb) p ucrt
$2 = {0x6e9490, 0x6eb7c0, 0x6eb800}

指针地址确认这是两个不同的分配器。也许你想知道如果我过河会发生什么?

int main(void)
{
free(ucrt_malloc(1));
}

MSVCRT 分配器有理由对坏指针感到恐慌:

$ cc -g3 -o chaos.exe chaos.c ucrtbase.lib
$ gdb -ex run chaos.exe
Starting program: chaos.exe
warning: HEAP[chaos.exe]:
warning: Invalid address specified to RtlFreeHeap
Thread 1 received signal SIGTRAP, Trace/breakpoint trap.
0x00007ffc42c369af in ntdll!RtlRegisterSecureMemoryCacheCallback ()
(gdb)

虽然您可能不应该像这样干预ucrtbase.dll,但导出重命名的一般原则是合理的。我不希望我需要这样做,但我喜欢我有这个选择。

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论