对DLL函数调用的地址修正是一个多阶段过程:链接器将调用指令指向间接跳转指令,并将间接跳转指令指向.data节所在的Windows程序的导入表中的内存字。加载程序将在运行时加载DLL时放置函数的地址。
间接跳转指令必须由链接器生成,因为编译器不知道该函数将在DLL中生成。通过为每个函数仅生成一个间接跳转指令来最小化程序文件大小,无论它调用多少个位置。
考虑到这一点,显而易见的方法是在所有目标文件中的所有编译器生成的代码之后收集文本部分末尾的所有间接跳转指令,这看起来确实发生了什么。我尝试使用Microsoft linker / nodefaultlib开关的简单测试用例(它生成一个足够小的可执行文件,我可以理解完整的反汇编)。
当我以正常方式将一个小程序与C标准库链接时,生成的可执行文件足够大,我无法完成所有的反汇编,但据我所知,间接跳转指令似乎分散在整个代码中,每次只有三个小组。
这是否有理由让我失踪?
答案 0 :(得分:3)
间接跳转指令必须由链接器生成,因为 编译器不知道函数会变成DLL。
实际上,情况并非总是如此。如果使用__declspec(dllimport)
标记函数,则编译器 知道它将是DLL导入,在这种情况下,它可以生成间接调用:
; HMODULE = LoadLibrary("mylib");
push offset $SG66630
call [__imp__LoadLibraryA@4]
(__imp__LoadLibraryA@4
是指向IAT中导入的指针)
如果不使用dllimport
,则编译器会生成相对函数调用:
push offset $SG66630
call _LoadLibraryA@4
在这种情况下,链接器必须生成跳转存根:
LoadLibraryA proc near
jmp [__imp__LoadLibraryA@4]
LoadLibraryA endp
事实上,它确实将这些跳转存根组合在一起(尽管可能是通过编译单元和/或导入的DLL,而不是100%确定)。
注意:在过去,链接器没有显式生成跳转存根,而是从导入库中获取它们。它们包含完整的目标文件,包括生成PE导入目录所需的存根和结构。请参阅此文章了解它的工作原理:https://www.microsoft.com/msj/0498/hood0498.aspx
目前,导入库只有API和DLL名称,链接器知道如何生成导入它们所需的代码和元数据。