为什么DLL函数调用未编译为相对CALL指令

时间:2017-11-27 07:03:51

标签: visual-studio dll x86 dllimport

当我调用函数 DLLFunction(int)时,它在DLL中定义。我的Intel X86 PC上的Visual Studio 2013将其编译为以下指令

CALL [__imp__DLLFunction@4] // Call absolute indirect address FF 15 00 90 40 00 CALL [00409000h] // original absolute CALL instruction FF 15 00 90 39 01 CALL [01399000h] // After address fixup by OS loader // __imp__DLLFunction@4 is the IAT entry address for DLLFunction, where there stores the address for DLLFunction().

调用者图像中的IAT的RVA(相对视觉地址)是0x9000,其中存储导入功能'地址。 RVA Import Function Address 0x9000 0x60fd1014 // DLLFunction 0x9004 0x60fdxxxx // someOtherDLLFunction0 0x9008 0x60fdxxxx // someOtherDLLFunction1 ...

  

为什么编译器不生成相对CALL指令?

如果使用相对CALL指令,加载器不需要像这样修复所有这些CALL指令的地址。

3 个答案:

答案 0 :(得分:1)

CALL [__imp__DLLFunction@4]没有调用通过间接跳转将控制引导到导入函数的通常存根,它直接通过IAT中的指针调用导入的函数。

当外部函数使用__declspec(dllimport)注释时(可能以任何方式使编译器意识到程序员的意图),就会发生这种情况。

没有它,编译器会生成相对(近)调用,链接器会添加存根。

:401005 E806000000     call 401010h              ;Relative near call to the stub
... The stub ...
:401010 FF25F4B04000   jmp DWORD PTR [0040b0f4]  ;Indirect abs jump

意图明确,上面的代码转换为

:401005 FF15F4B04000   call DWORD PTR [0040b0f4]

使用绝对间接呼叫 这样可以避免跳转,但需要在加载时进行额外的修复,相对间接的调用会更好,但不幸的是,它不存在。

x86-64代码可以使用RIP相对寻址来缓解修复问题。

答案 1 :(得分:0)

更新:我没有仔细阅读问题:根据OP,间接call目标仍在可执行文件中,并且本身是间接jmp

下面的答案是讨论将call rel32直接直接放入DLL中。

这需要在每个call指令处修改机器代码,以便在动态加载DLL时放入正确的偏移量。 (您不知道将加载DLL的地址,因此您不知道可执行文件链接时可执行文件与DLL之间的rel32距离。)

使用函数指针表将所有重定位项放在一个可以在动态链接时有效写入的地方。

如果DLL从需要调用它的代码加载超过2GB,它在64位代码中也无法达到足够远。

IIRC,Windows确实支持您为正常链接的DLL描述的方案(在编译时使用链接器,而不是运行时dll导入)。  每个DLL都有一个“首选”加载地址,并且乐观地调用它的代码使用call rel32指令,因此如果该地址不可用,每个调用站点都需要修复。这些修复程序在加载进程时发生。启用ASLR后,DLL每次都不会加载到同一地址。

一旦进程已经运行,其代码页将是只读的,因此如果需要这些修正,则会出现问题。这可能是动态DLL导入不使用此机制的原因。 (该实现可以使用VirtualProtect使代码页可以写入这些修正,但是同时导入两个不同的线程导致DLL不是线程安全的。。一个线程可能在完成后将页面设为只读,但是另一个线程仍在向该页面写入修正,导致出现故障。)

此外,交叉修改代码通常不安全。其他线程可以在您应用修正的同一函数中运行指令。您可以使用rel32或其他内容以原子方式存储新的xchg。这可能是安全的。

顺便说一下,在Linux上,甚至像libc这样的“普通”库也是通过这样的间接层来调用的(来自全局偏移表的函数指针)。请参阅Sorry state of dynamic libraries on Linux

部分是在运行时动态链接(加载时间)的开销与加载后的性能之间的权衡。

答案 2 :(得分:0)

使用相对地址要么在加载后修补对DLL函数的所有相对调用,要么链接器需要有关被调用函数的大小要求的有根据的猜测,并用加载的函数替换加载器。 p>

这两种方法都有更糟糕的最坏情况:要么修补可能数百个相对调用,要么在相对跳转之后需要发出间接调用,如果动态加载的函数不适合预先分配的空间。

还可以推测修补可执行文件会给病毒扫描程序带来很多误报,这需要非常熟悉dll加载的标准过程。