停止编译器为导入的函数生成包装器

Posted

技术标签:

【中文标题】停止编译器为导入的函数生成包装器【英文标题】:Stop compiler from generating wrappers for imported functions 【发布时间】:2019-11-20 14:19:54 【问题描述】:

我正在尝试获取 KeGetProcessorNumberFromIndex 的地址,但编译器不断生成调用 KeGetProcessorNumberFromIndex 的函数,我得到了这个生成函数的地址。我可以解析 PE 导出,但我想知道是否有办法阻止我正在使用 Visual Studio 的这种行为。我试图禁用所有优化,但这并没有改变任何东西。

uintptr_t addr = (uintptr_t)KeGetProcessorNumberFromIndex; // gets address of KeGetProcessorNumberFromIndex_0 

KeGetProcessorNumberFromIndex_0 proc near
jmp     cs:__imp_KeGetProcessorNumberFromIndex
KeGetProcessorNumberFromIndex_0 endp

【问题讨论】:

您需要禁用 /INCREMENTAL 链接器选项 - 您所看到的 - 这是 jump thunk 或者您可以从 asm 代码中执行此操作 - 声明 extern __imp_KeGetProcessorNumberFromIndex:QWORD 并从__imp_KeGetProcessorNumberFromIndex。您可以在 x64 的情况下执行此操作并直接从 c++ - 声明 extern "C" extern void* __imp_KeGetProcessorNumberFromIndex; 并按原样使用它。但是 x86 名称会出现问题 - __imp__KeGetProcessorNumberFromIndex@8 这是无效的 c++ 名称(@符号) oo。现在查看 wdm.h - 这个函数声明时没有 __declspec(dllimport) 属性。正是这在函数上产生了 jmp thunk。 【参考方案1】:

这就是导入函数在 Windows 中的工作方式。编译器不会生成您所看到的代码,它实际上是在您链接的导入库中,该库之前是由创建导入库的链接器生成的。 (实际上,使用现代导入库和链接器,导入记录以紧凑的形式存储,链接器在链接时生成代码。无论哪种方式,它都不是由编译器本身生成的。)

如果addr 是一个局部变量或者您使用的是C++,那么uintptr_t addr =(uintptr_t)KeGetProcessorNumberFromIndex; 可以在初始化期间在运行时解析,并且它将包含函数的实际地址。要使其工作,尽管 KeGetProcessorNumberFromIndex 必须事先声明为 __declspec(dllimport) 否则编译器不会知道它是一个导入函数,其真实地址不在同一个图像中。显然 DDK 标头缺少这个,所以这就是为什么你没有在 addr 中获得函数的实际地址。

如果 addr 不是局部变量并且您使用的是 C,或者没有使用 __declspec(dllimport),就像这里的情况一样,定义在链接时解析,而链接器不会不知道也无法知道函数在运行时的地址。相反,它使用存根函数的地址,即导入库隐式或显式定义的地址,因为它确实知道存根相对于它创建的图像的开始位置。

简单的解决方案是在你自己的代码中在运行时获取函数的真实地址。由于这是内核代码,您需要使用 MmGetSystemRoutineAddress,正如 Maxim Sagaydachny 所说:

uintptr_t addr = MMGetSystemRoutineAddress("KeGetProcessorNumberFromIndex");

如果你想走硬路线,这样你就可以避免打电话给MMGetSystemRoutineAddress,你可以这样做:

#ifdef _M_IX86

extern "C" NTSTATUS  (__stdcall *_imp__KeGetProcessorNumberFromIndexw)(ULONG, PROCESSOR_NUMBER);
#pragma comment(linker, "/alternatename:__imp__KeGetProcessorNumberFromIndex=__imp__KeGetProcessorNumberFromIndex@8")
uintptr_t addr = (uintptr_t)_imp__KeGetProcessorNumberFromIndex;

#elif defined(_M_X64) 

extern "C" NTSTATUS (__stdcall *__imp_KeGetProcessorNumberFromIndex)(ULONG, PROCESSOR_NUMBER);
uintptr_t addr = (uintptr_t)__imp_KeGetProcessorNumberFromIndex;

#endif

但我不确定是否值得所有这些代码复杂性,并且在 32 位 x86 案例中依赖未记录的链接器开关,只是为了保存函数调用。该代码也特定于 Visual C++,尽管它可以适用于 Clang 或 GCC。

您可以在 this answer by Matteo Italia 中找到有关 __declspec(dllimport) 工作原理的更多详细信息,并在该答案中链接的 Raymond Chen 的博客中找到更多详细信息。

【讨论】:

导入的函数如何在 Windows 中工作 - 不完全是。对于导入函数编译器声明 __imp_fn 变量(对于 x86 或 c++ 名称 fn 将被修饰)。当需要调用 api 时 - 将是 call __imp_fn 指令,当需要获取函数地址时 - 比如说 mov rax,__imp_fn 指令 - 所以这将是 __imp_fn 变量的真实地址。但是使用 /INCREMENTAL 选项 - 代码 可能包含跳转 thunks - fn: jmp __imp_fn - 所以这是跳转思考,用这个而不是我们得到的真实地址 thunk 的地址 - lea rax, fn 其中fnfn:jmp __imp_fn 的标签,而不是mov rax,__imp_fn @RbMm 像往常一样,我真的无法理解您想说什么,但您似乎并没有真正与我写的任何内容相矛盾。相反,当使用__declspec(dllimport) 声明函数时,您提出了编译器在某些情况下可以执行的优化,但这不是其中一种情况。静态初始化静态定义的变量时,编译器不能直接使用__imp__fn 符号,例如uintptr_t addr = (uintptr_t)KeGetProcessorNumberFromIndex; 对不起我纯英语。但你严重错误。这是可能编译器直接使用__imp__fn。我们也可以这样做是代码-(我不这样做一次)-我们可以简单地声明extern "C" extern void* __imp_KeGetProcessorNumberFromIndex; 并按原样使用__imp_KeGetProcessorNumberFromIndex(但对于x86,这里需要技巧)。你所看到的jmp __imp_KeGetProcessorNumberFromIndex - 这是由于 /INCREMENTAL 选项而导致的跳转 thunk。这是更改地址 @Anders 链接器不理解dllimport,编译器无法告诉链接器是否声明了一个符号。编译器生成使用__imp__fn 或仅使用_fn 的代码。由于这段代码非常不同,因为前者是指向函数的指针,而后者是实际函数,因此链接器无法将后者更改为前者。如果编译器使用_fn,链接器将其更改为__imp_fn 为时已晚。 (显然这忽略了链接器实际编译代码的链接时间代码生成。)【参考方案2】:

使用 MmGetSystemRoutineAddress 获取所需函数的地址。 它是用户空间 LoadLibrary/GetProcAddress 的内核等价物

【讨论】:

以上是关于停止编译器为导入的函数生成包装器的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SWIG 生成的 Go 包装器中添加导入语句

为静态库编译 SWIG Python 包装器?

导入pygame时“通用包装器中没有匹配的架构”

DLL 函数调用的间接跳转

为 OpenCV 的 C++ createTrackbar 运行 C 包装器时出现编译分段错误(核心转储)

swig perl 包装器没有为类成员函数生成