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

Visual studio 为什么DLL函数调用未编译为相对调用指令,visual-studio,dll,x86,dllimport,Visual Studio,Dll,X86,Dllimport,当我调用在DLL中定义的函数DLLFunction(int)时。英特尔X86 PC上的Visual Studio 2013将其编译为以下指令 CALL[\u\u imp__DLLFunction@4]//调用绝对间接地址 FF 15 00 90 40 00 CALL[00409000h]//原始绝对调用指令 FF 15 00 90 39 01调用[01399000h]//操作系统加载程序修复地址后 //_uuuimp__DLLFunction@4是DLLFunction的IAT条目地址,其中存储

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


CALL[\u\u imp__DLLFunction@4]//调用绝对间接地址
FF 15 00 90 40 00 CALL[00409000h]//原始绝对调用指令
FF 15 00 90 39 01调用[01399000h]//操作系统加载程序修复地址后
//_uuuimp__DLLFunction@4是DLLFunction的IAT条目地址,其中存储了DLLFunction()的地址。

调用方映像中IAT的RVA(相对可视地址)为0x9000,其中存储导入函数的地址。

RVA导入函数地址
0x9000 0x60fd1014//dll函数
0x9004 0x60fdxxxx//someOtherDLLFunction0
0x9008 0x60fdxxxx//someotherdllffunction 1
...

为什么编译器不生成相对调用指令


如果使用相对调用指令,加载程序不需要像这样为所有这些调用指令设置地址。

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

下面的答案是讨论如何使调用rel32直接进入DLL


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

使用函数指针表可以将所有重新定位的内容放在一个地方,在动态链接时可以高效地写入这些内容

如果一个DLL的加载距离需要调用它的代码超过2GB,那么它在64位代码中也无法达到足够的深度


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

一旦进程已经运行,它的代码页将是只读的,因此如果需要这些修正,这是一个问题。这可能就是动态DLL导入不使用此机制的原因。(实现可以使用
VirtualProtect
使这些修复程序的代码页可写,但是让两个不同的线程同时导入DLL是不安全的。。一个线程可能会在完成后使页面只读,但另一个线程仍在将修复程序写入该页面,从而导致(有过错。)

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

顺便说一句,在Linux上,即使是像
libc
这样的“普通”库也会通过这样的间接层(全局偏移表中的函数指针)进行调用。看


这在一定程度上是运行时动态链接开销(加载时间)与加载后性能之间的折衷。

使用相对地址可能需要在加载后修补对DLL函数的所有相对调用,或者,链接器需要对被调用函数的大小要求进行有根据的猜测,并用加载的函数替换加载程序

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


人们还可以推测,修补可执行文件会导致病毒扫描程序出现大量误报,病毒扫描程序需要非常熟悉dll加载的标准过程。

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

当外部函数用
\uu 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相对寻址来缓解修复问题。

嗨,彼得,你的答案是相反的。是这样吗?如果使用相对调用,则在动态加载DLL时,不需要在每个调用指令处修改机器代码,以将其放入正确的偏移量。@ZhiboShen:不,您不知道相对于主可执行文件将在哪里加载DLL,但所有相对偏移量都会使硬代码偏离该距离<代码>调用rel32在独立于位置的库或可执行文件中工作,但需要在单独加载的库之间进行修复。嗨,Peter,X86上的Visual Studio使用IAT。来自另一个DLL的函数的绝对地址存储在调用线程的IAT中,必须由OS loader修复。CALL rel32是从IAT呼叫rel32。尽管我们不知道DLL相对于主线程的加载位置,但IAT和.text都在调用线程地址r中