__imp_ 符号的语义
Posted
技术标签:
【中文标题】__imp_ 符号的语义【英文标题】:Semantics of __imp_ symbols 【发布时间】:2014-05-11 02:02:42 【问题描述】:调用 DLL 函数涉及链接器生成存根,除非函数声明为 __declspec(dllimport)
,在这种情况下,可以绕过存根,直接调用导入表更高效,例如
__declspec(dllimport) void ExitProcess(int);
ExitProcess(0);
生成
call qword ptr [__imp_ExitProcess]
其中__imp_ExitProcess
解析为可执行文件中导入表中的某个位置。
我试图弄清楚__imp_ExitProcess
是如何解决的。可以肯定的是,它在 kernel32.lib 中作为一个符号出现,但该符号具有存储类 IMAGE_SYM_CLASS_EXTERNAL
,节号为零且值为零,这相当于只是说“这将在其他地方定义”而实际上没有定义它。
__imp_
前缀是否有特殊含义,即链接器是否注意到该前缀并将其作为将符号解析为名称已删除该前缀的 DLL 函数的导入表条目的指令?还是发生了其他事情?
【问题讨论】:
你在看哪个kernel32.lib
?
@IgorSkochinsky Microsoft C++ 2013 附带的 64 位版本。286522 字节。
它是外部的,在 kernel32.dll 中的其他地方定义。这由加载程序在运行时解决。前缀没有意义。
@HansPassant 对,加载器在运行时从导入表工作,并不关心前缀。但是在链接时,链接器如何知道__imp_ExitProcess
应该指向导入表而不是作为未定义符号被拒绝?
【参考方案1】:
kernel32.lib
的成员不是普通的目标文件,而是特殊的占位符。来自PE/COFF spec:
传统的导入库,即描述 从一个图像导出以供另一个图像使用,通常遵循布局 第 7 节“存档(库)文件格式”中描述。首要的 不同之处在于导入库成员包含伪目标文件 而不是真实的,其中每个成员都包含该部分 构建导入表所需的贡献 在第 6.4 节“.idata 节”中进行了描述。链接器生成 在构建导出应用程序时此存档。
可以从一小组信息中推断出导入的部分贡献。链接器可以生成完整的, 每个成员的详细信息到导入库中 图书馆的创建时间或只写规范信息 到库并让以后使用它的应用程序生成 动态的必要数据。
[...] 一个简短的导入库编写如下:
Archive member header Import header Null-terminated import name string Null-terminated DLL name string
这些信息足以准确地重建会员在使用时的全部内容。
在我机器上的kernel32.lib
中,__imp_ExitProcess
在第一个和第二个链接器成员(符号列表)中被提及,并指向描述导入的特定伪对象:
Archive member name at 8: /
50107C36 time/date Thu Jul 26 01:07:34 2012
uid
gid
0 mode
106CA size
correct header end
2515 public symbols
[...]
3C874 ExitProcess
3C874 __imp_ExitProcess
[...]
Archive member name at 3C874: KERNEL32.dll/
50107639 time/date Thu Jul 26 00:42:01 2012
uid
gid
0 mode
2D size
correct header end
Version : 0
Machine : 8664 (x64)
TimeDateStamp: 50107639 Thu Jul 26 00:42:01 2012
SizeOfData : 00000019
DLL name : KERNEL32.dll
Symbol name : ExitProcess
Type : code
Name type : name
Hint : 371
Name : ExitProcess
因此,如您所见,.lib 中的数据明确表示它引用了来自 DLL KERNEL32.dll
的导入名称 ExitProcess
。链接器可以使用它在导入部分构建必要的元数据。
现在,上面只讨论了__imp_ExitProcess
符号是如何解析的。我不是 100% 确定,但我认为如果一个符号(例如 ExitProcess
)已被解析为这样的导入存根,并且它不以 __imp_
开头,那么链接器必须生成对 IAT 槽的跳转存根(用于代码符号)或间接访问(用于数据访问)。
【讨论】:
你在最后一段中所说的是真的,我对存根生成的理解也与你在最后一段中的理解一致。但是我仍然不清楚__imp_ExitProcess
是如何解决的。可以肯定的是,符号列表中数字的重合暗示了它在某种程度上与ExitProcess
相关,但是这如何转化为解析符号的规范或算法呢?我的第一个猜想是它通过去除前面的 __imp_
...
人们也可以通过查看符号列表来推测它可能会按照“当外部符号具有与显式导入相同的符号列表编号并且它们是仅有的两个符号那个数字,把它们当作以这种方式连接,'但我不知道是这种情况还是其他原因 - 如果是这样,如果有几个符号具有相同的数字会发生什么?它是第一个确定的链接器成员吗?即使规范说一个已经过时了?等等。
@rwallace:阅读 PE/COFF 规范,尤其是关于档案的第一个和第二个成员(符号索引)的部分。您使用索引来查找定义符号的存档成员。在这种情况下,归档成员不是完整的目标文件而是导入存根,因此您不需要进行任何额外的符号查找 - 只需使用其中的信息来生成导入目录元数据。
是的,我已经阅读了该部分十到十五遍,尽管我可能仍然遗漏了一些含义。但是您认为该算法是我上面第二个建议的某个版本?也许你是对的,虽然我现在也有一个数据点,我会在答案中发布。
我不知道您为什么担心是否有其他符号指向同一个成员。担心你真正需要的那个。 1)取您需要解析的符号(__imp_ExitProcess
)。 2)在索引中查找(第一或第二,应该无关紧要)。 3) 提取目标存档成员。 4a) 如果是完整的 COFF,请执行完整的链接步骤。 4b)如果是导入存根(伪对象),获取目标DLL名称和导入名称/序号并生成导入目录数据。 5b) 将__imp_
符号的引用替换为新生成的IAT条目的地址。【参考方案2】:
刚刚想出了一个提供数据点的测试。以下程序:
__declspec(dllimport) void ExitProcess(int);
void __imp_ExitProcess(int x)
int main()
ExitProcess(0);
使用 Microsoft 编译器编译并运行时崩溃(但如果将空函数重命名为其他任何内容,则运行正常);因此,似乎链接器的行为好像__imp_
名称很特殊,至少在某种意义上,以这种方式运行的链接器将在 Microsoft 链接器这样做的所有情况下生成正确的可执行文件,除非我遗漏了什么.
【讨论】:
【参考方案3】:链接器向您的程序添加如下函数:
void (*__imp_ExitProcess)(int) = ...;
void ExitProcess(int n)
return (*__imp_ExitProcess)(n);
其中 __imp_ExitProcess 指向 KERNEL32.DLL 中的“真实”ExitProcess。
在你的代码中声明:
__declspec(dllimport) void ExitProcess(int);
相当于:
extern void (*__imp_ExitProcess)(int);
#define ExitProcess (*__imp_ExitProcess)
除了 __declspec(dllimport) 由编译器处理,而不是由预处理器处理。
【讨论】:
以上是关于__imp_ 符号的语义的主要内容,如果未能解决你的问题,请参考以下文章
error LNK2019: 无法解析的外部符号 __imp___CrtDbgReportW
error LNK2019: 无法解析的外部符号 __imp__InternetCrackUrlW@16,该符号在函数 "int __cdecl ParseURLW
错误 1 error LNK2019: 无法解析的外部符号 __imp__pthread_create,该符号在函数 _main 中被引用 解决方法
引用了未解析的外部符号 __imp__JNI_CreateJavaVM@12
glCubicRayCast.obj : error LNK2019: 无法解析的外部符号 __imp__glewInit,该符号在函数 _main中被引用