停止编译器为导入的函数生成包装器
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
其中fn
是fn: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 的内核等价物
【讨论】:
以上是关于停止编译器为导入的函数生成包装器的主要内容,如果未能解决你的问题,请参考以下文章