环境:vs2005 + xpsp3
作者:magictong
时间:2010-09-08
注:例子演示里面都是以debug模式下的汇编来讲,在release下因为经过一些优化,过程会有一些区别,但是最终的结论是一样的。
__declspec本身就是microsoft对c++的扩展,因此后面的讨论都是指在VS2005下编译的结果,与__declspec(dllimport)相对的一个组合用法是__declspec(dllexport),__declspec(dllexport)是作用于PE文件导出函数、类、变量等等(如果你不用def文件导出函数,就必须使用__declspec(dllexport)来进行导出)。__declspec(dllimport)的具体作用在msdn上面讲得比较模糊,如下 Importing into an Application Using __declspec(dllimport) :
英文:
Using __declspec(dllimport) is optional on function declarations, but the compiler produces more efficient code if you use this keyword. However, you must use __declspec(dllimport) for the importing executable to access the DLL's public data symbols and objects. Note that the users of your DLL still need to link with an import library.
中文:
在函数声明上使用 __declspec(dllimport) 是可选操作,但如果使用此关键字,编译器将生成更有效的代码。但是,为使导入的可执行文件能够访问 DLL 的公共数据符号和对象,必须使用 __declspec(dllimport)。请注意,DLL 的用户仍然需要与导入库链接。
所谓可选操作,就是可要可不要,但是如果使用了,编译器会生成更高效的代码。嗯,编译器就是这么一个意思。后来我测试了四种情况,来看看到底是什么情况。
情况一:EXE内部调用自己的swap函数,swap函数没有使用__declspec(dllimport)修饰
对swap的调用生成的汇编代码为:
swap(m, n);
00411423 lea eax,[n]
00411426 push eax
00411427 lea ecx,[m]
0041142A push ecx
0041142B call swap (411113h)
00411430 add esp,8
看一下411113h 这个地址是一条jmp指令,跳转到swap的真正地址:
0x00411113 e9 98 04 00 00
00411113 jmp swap (4115B0h)
int swap(int& a, int& b)
{
004115B0 push ebp
004115B1 mov ebp,esp
004115B3 sub esp,0C0h
因此,这种情况仅仅是正常的的函数调用。
情况二:EXE内部调用自己的swap函数,swap函数使用__declspec(dllimport)修饰
这种情况跟情况一生成的代码基本一样,唯一有点点区别是在编译的时候在swap的实现处会出现下面的警告(大概意思就是声明有点不一致,与编译器的预期有点不一样,因为编译器预期会是一个dll的导出函数,嗯,大概是这么个情况,不用深究):
warning C4273: 'swap' : inconsistent dll linkage
情况三:EXE内部调用dll的add函数,add函数不使用__declspec(dllimport)修饰
对add的调用生成的汇编代码为:
add(m, n);
004113FC mov eax,dword ptr [n]
004113FF push eax
00411400 mov ecx,dword ptr [m]
00411403 push ecx
00411404 call add (411127h)
看下411127h这个地址也是一条jmp指令,跳转到411620h
0x00411127 e9 f4 04 00 00
00411127 jmp add (411620h)
跳转到411620h之后发现居然还不是add的地址,而是
00411620 jmp dword ptr [__imp_add (4181E0h)]
依然是一个jmp指令,跳转到4181E0h指向的一个位置,看下内存,应该是跳转到0x100110f0去执行: 0x004181E0 f0 10 01 10
跟过去,居然还是一条跳转指令,这次是跳转到0x10011340:
100110F0 jmp add (10011340h)
再跟过去,这次终于对了:
int __stdcall add(int a, int b)
{
10011340 push ebp
10011341 mov ebp,esp
10011343 sub esp,0C0h
10011349 push ebx
这次很纠结,中间多了几个jmp指令,这个我们先不讲,我们先看看情况四之后再讨论。
情况四:EXE内部调用dll的add函数,add函数使用__declspec(dllimport)修饰
对add的调用生成的汇编代码为:
add(m, n);
004113FC mov esi,esp
004113FE mov eax,dword ptr [n]
00411401 push eax
00411402 mov ecx,dword ptr [m]
00411405 push ecx
00411406 call dword ptr [__imp_add (4181E0h)]
这次情况好像有点不一样,直接call了4181E0h指向的一个位置,看下内存:
0x004181E0 f0 10 01 10
也就是说跳转到0x100110f0去执行,跟过去:
100110F0 jmp add (10011340h)
直接一个jmp指令到10011340h,再跟过去就是add的真正地址了:
int __stdcall add(int a, int b)
{
10011340 push ebp
10011341 mov ebp,esp
10011343 sub esp,0C0h
这种情况下,对比情况三,情况四少了2条jmp指令……
结论:
看来msdn说的没错o(∩_∩)o ,确实使用了__declspec(dllimport)之后,生成的代码更加高效。在对release代码的查看中发现,最终会少一条jmp指令。
其实整个来讲还是比较好理解的,对于编译器来讲,你没有__declspec(dllimport)这个来修饰函数声明,它开始就会把函数当成一个本地函数来生成调用代码(如:call add (411127h)),但是最后链接的时候发现这个函数是个动态链接库里面的函数,真正地址会存在输入表里面,因此中间就多了至少一条jmp指令来中转。如果使用了__declspec(dllimport)来修饰,那么编译器知道,哦,是个外部的函数,函数的地址在输入表里面,虽然现在不知道函数真正地址,但是装载之后该函数地址在输入表中的位置是固定的,因此编译器就直接生成调用输入表中某个地方的函数地址的代码了(如:call dword ptr [__imp_add (4181E0h)])。
【END】
作者:magictong
时间:2010-09-08
注:例子演示里面都是以debug模式下的汇编来讲,在release下因为经过一些优化,过程会有一些区别,但是最终的结论是一样的。
__declspec本身就是microsoft对c++的扩展,因此后面的讨论都是指在VS2005下编译的结果,与__declspec(dllimport)相对的一个组合用法是__declspec(dllexport),__declspec(dllexport)是作用于PE文件导出函数、类、变量等等(如果你不用def文件导出函数,就必须使用__declspec(dllexport)来进行导出)。__declspec(dllimport)的具体作用在msdn上面讲得比较模糊,如下 Importing into an Application Using __declspec(dllimport) :
英文:
Using __declspec(dllimport) is optional on function declarations, but the compiler produces more efficient code if you use this keyword. However, you must use __declspec(dllimport) for the importing executable to access the DLL's public data symbols and objects. Note that the users of your DLL still need to link with an import library.
中文:
在函数声明上使用 __declspec(dllimport) 是可选操作,但如果使用此关键字,编译器将生成更有效的代码。但是,为使导入的可执行文件能够访问 DLL 的公共数据符号和对象,必须使用 __declspec(dllimport)。请注意,DLL 的用户仍然需要与导入库链接。
所谓可选操作,就是可要可不要,但是如果使用了,编译器会生成更高效的代码。嗯,编译器就是这么一个意思。后来我测试了四种情况,来看看到底是什么情况。
情况一:EXE内部调用自己的swap函数,swap函数没有使用__declspec(dllimport)修饰
- int swap(int& a, int& b);
- int swap(int& a, int& b)
- {
- a = a ^ b;
- b = a ^ b;
- a = a ^ b;
- return 0;
- }
int swap(int& a, int& b);
int swap(int& a, int& b)
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
return 0;
}
对swap的调用生成的汇编代码为:
swap(m, n);
00411423 lea eax,[n]
00411426 push eax
00411427 lea ecx,[m]
0041142A push ecx
0041142B call swap (411113h)
00411430 add esp,8
看一下411113h 这个地址是一条jmp指令,跳转到swap的真正地址:
0x00411113 e9 98 04 00 00
00411113 jmp swap (4115B0h)
int swap(int& a, int& b)
{
004115B0 push ebp
004115B1 mov ebp,esp
004115B3 sub esp,0C0h
因此,这种情况仅仅是正常的的函数调用。
情况二:EXE内部调用自己的swap函数,swap函数使用__declspec(dllimport)修饰
这种情况跟情况一生成的代码基本一样,唯一有点点区别是在编译的时候在swap的实现处会出现下面的警告(大概意思就是声明有点不一致,与编译器的预期有点不一样,因为编译器预期会是一个dll的导出函数,嗯,大概是这么个情况,不用深究):
warning C4273: 'swap' : inconsistent dll linkage
情况三:EXE内部调用dll的add函数,add函数不使用__declspec(dllimport)修饰
- #if IMPORTDLLDLL_EXPORTS
- #define API_DECLSPEC __declspec(dllexport)
- #else
- #define API_DECLSPEC
- #endif
- // -------------------------------------------------------------------------
- API_DECLSPEC int __stdcall add(int a, int b);
- int __stdcall add(int a, int b)
- {
- return a + b;
- }
#if IMPORTDLLDLL_EXPORTS
#define API_DECLSPEC __declspec(dllexport)
#else
#define API_DECLSPEC
#endif
// -------------------------------------------------------------------------
API_DECLSPEC int __stdcall add(int a, int b);
int __stdcall add(int a, int b)
{
return a + b;
}
对add的调用生成的汇编代码为:
add(m, n);
004113FC mov eax,dword ptr [n]
004113FF push eax
00411400 mov ecx,dword ptr [m]
00411403 push ecx
00411404 call add (411127h)
看下411127h这个地址也是一条jmp指令,跳转到411620h
0x00411127 e9 f4 04 00 00
00411127 jmp add (411620h)
跳转到411620h之后发现居然还不是add的地址,而是
00411620 jmp dword ptr [__imp_add (4181E0h)]
依然是一个jmp指令,跳转到4181E0h指向的一个位置,看下内存,应该是跳转到0x100110f0去执行: 0x004181E0 f0 10 01 10
跟过去,居然还是一条跳转指令,这次是跳转到0x10011340:
100110F0 jmp add (10011340h)
再跟过去,这次终于对了:
int __stdcall add(int a, int b)
{
10011340 push ebp
10011341 mov ebp,esp
10011343 sub esp,0C0h
10011349 push ebx
这次很纠结,中间多了几个jmp指令,这个我们先不讲,我们先看看情况四之后再讨论。
情况四:EXE内部调用dll的add函数,add函数使用__declspec(dllimport)修饰
- #if IMPORTDLLDLL_EXPORTS
- #define API_DECLSPEC __declspec(dllexport)
- #else
- #define API_DECLSPEC __declspec(dllimport)
- #endif
- // -------------------------------------------------------------------------
- API_DECLSPEC int __stdcall add(int a, int b);
- int __stdcall add(int a, int b)
- {
- return a + b;
- }
#if IMPORTDLLDLL_EXPORTS
#define API_DECLSPEC __declspec(dllexport)
#else
#define API_DECLSPEC __declspec(dllimport)
#endif
// -------------------------------------------------------------------------
API_DECLSPEC int __stdcall add(int a, int b);
int __stdcall add(int a, int b)
{
return a + b;
}
对add的调用生成的汇编代码为:
add(m, n);
004113FC mov esi,esp
004113FE mov eax,dword ptr [n]
00411401 push eax
00411402 mov ecx,dword ptr [m]
00411405 push ecx
00411406 call dword ptr [__imp_add (4181E0h)]
这次情况好像有点不一样,直接call了4181E0h指向的一个位置,看下内存:
0x004181E0 f0 10 01 10
也就是说跳转到0x100110f0去执行,跟过去:
100110F0 jmp add (10011340h)
直接一个jmp指令到10011340h,再跟过去就是add的真正地址了:
int __stdcall add(int a, int b)
{
10011340 push ebp
10011341 mov ebp,esp
10011343 sub esp,0C0h
这种情况下,对比情况三,情况四少了2条jmp指令……
结论:
看来msdn说的没错o(∩_∩)o ,确实使用了__declspec(dllimport)之后,生成的代码更加高效。在对release代码的查看中发现,最终会少一条jmp指令。
其实整个来讲还是比较好理解的,对于编译器来讲,你没有__declspec(dllimport)这个来修饰函数声明,它开始就会把函数当成一个本地函数来生成调用代码(如:call add (411127h)),但是最后链接的时候发现这个函数是个动态链接库里面的函数,真正地址会存在输入表里面,因此中间就多了至少一条jmp指令来中转。如果使用了__declspec(dllimport)来修饰,那么编译器知道,哦,是个外部的函数,函数的地址在输入表里面,虽然现在不知道函数真正地址,但是装载之后该函数地址在输入表中的位置是固定的,因此编译器就直接生成调用输入表中某个地方的函数地址的代码了(如:call dword ptr [__imp_add (4181E0h)])。
【END】