比方说,如果DLL有一个功能:
int TestFunc01(int v)
{
WCHAR buff[256];
::StringCchPrintf(buff, _countof(buff), L"You passed a value of %d", v);
return ::MessageBox(NULL, buff, L"Test Dll Message", MB_OK | MB_ICONINFORMATION);
}
仅按其序数值导出(以下是.def
文件):
LIBRARY DllName
EXPORTS
TestFunc01 @1 NONAME
所以现在当我想从另一个模块静态链接到该函数时,如果函数是通过其名称导出的,我会执行以下操作:
extern "C" __declspec(dllimport) int TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
但是,我如何仅通过其序数值静态链接到它?
PS。我正在使用Visual Studio C ++编译器和链接器。
但是,我如何仅通过其序数值静态链接到它?
绝对相同的方式,当按名称输出功能时 - 没有任何区别。在这两种情况下你需要2件事 - 正确的功能声明:
extern "C" __declspec(dllimport) int /*calling convention*/ TestFunc01(int v);
和lib文件,包含在链接器输入中。
当您将somename.def文件包含到visual studio项目时,它会自动将/def:"somename.def"
选项添加到链接器(否则您需要手动添加此选项)。生成的lib文件将包含__imp_*TestFunc01*
符号 - 就位置*将是基于c或c ++符号和调用约定的不同装饰,以防x86。
从另一方面来说,当你调用函数时,使用__declspec(dllimport)
属性。编译器(CL)将生成call [__imp_*TestFunc01*]
- 所以引用__imp_*TestFunc01*
符号(再次*就地实际装饰)。链接器将搜索__imp_*TestFunc01*
符号并在lib文件中找到它。
NONAME
选项对于此过程无关紧要 - 这只会影响它在PE中如何形成此函数的IAT / INT条目(将按名称或序号导入)
请注意,如果我们仅通过link.exe /lib /def:somename.def
将生成的lib文件与def文件分开 - 链接器将没有正确的导出函数声明(def文件只包含名称而不调用约定和c或c ++名称) - 因此它始终被视为符号作为extern "C"
和__cdecl
在具体情况下可见,在dll函数中实现为int TestFunc01(int v)
- 所以没有extern "C"
- 因为lib文件中的符号将像__imp_?TestFunc01@@YAHH@Z
(我假设__cdecl和x86),但在另一个模块函数中使用extern "C"
- 所以链接器将搜索__imp__TestFunc01
当然没有找到它,因为它不存在于lib文件中。因为这样,当我们导出/导入一些符号时 - 两个模块必须相等。最好用明确的调用约定在单独的.h文件中声明它
您可以使用lib.exe工具从您编写的.DEF文件创建.lib文件,您不必实际提供任何目标文件。
您可以编写与您要导出的序号匹配的.def,但也提供名称,然后使用lib.exe创建.lib。
在代码中,您将其声明为:extern“C”__ declspec(dllimport)ret_type funcName(arg_type);
重要的是调用约定匹配函数实际使用的任何内容,但名称必须与您创建的.lib使用的名称相匹配,即使该名称不遵循该调用类型的修饰法线。
这不是为了与actual answer竞争。 David Heffernan在那里的评论中提出了一个很好的观点,关于使用dllimport
。所以我想做几次测试,看看当我使用__declspec(dllimport)
和不使用时它会有什么不同:
DLL中的函数声明:
extern "C" int __cdecl TestFunc01(int v)
{
WCHAR buff[256];
::StringCchPrintf(buff, _countof(buff), L"You passed a value of %d", v);
return ::MessageBox(NULL, buff, L"Test Dll Message", MB_OK | MB_ICONINFORMATION);
}
然后从另一个模块导入并调用它:
__declspec(dllimport)
extern "C" __declspec(dllimport) int __cdecl TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译机器代码:
call
指令从导入地址表(IAT)读取函数地址:
这给了它导入函数的位置:
__declspec(dllimport)
extern "C" int __cdecl TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译机器代码:
在这种情况下,它是单个call
指令的相对jmp
:
然后从IAT读取函数地址:
然后跳到它:
对于64位,我们必须将调用约定更改为__fastcall
。其余的保持不变:
__declspec(dllimport)
extern "C" __declspec(dllimport) int __fastcall TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译机器代码:
call
指令再次从IAT读取函数地址(在x64的情况下,它使用相对地址):
给它的功能地址:
__declspec(dllimport)
extern "C" int __fastcall TestFunc01(int v);
int _tmain(int argc, _TCHAR* argv[])
{
TestFunc01(123);
}
编译机器代码:
再一个相对call
到一个jmp
:
然后从IAT读取函数地址:
然后跳到它:
所以如你所见,通过不使用dllimport
,你在技术上会产生额外的jmp
。我不确定跳转的目的是什么,但肯定不会让你的代码运行得更快。也许这是一个代码维护的事情,也许它是一种对更新进行热补丁的方法,也许是一种调试功能。因此,如果有人可以了解跳跃的目的,我会很高兴听到它。