C, C++ 문법

DLL - 함수 호출시 __declspec(dllimport)의 사용과 미사용간 코드생성 차이점

디버그정 2008. 9. 2. 19:05

DLL - 함수 호출시 __declspec(dllimport)의 사용

Level of Difficulty 1 2 3

DLL을 통해서 API를 제공하기 위해서 DLL(PE)의 Export Table에 함수를 나타내도록 __declspec(dllexport)지시자를 사용합니다.
반면에 함수를 호출하는 쪽에서는 특별히 __declspec(dllimport)으로 정의된 함수원형을 사용하지 않아도 DLL에서 제공된 함수를 사용 할 수 있습니다.


결론 부터 말씀드리면 DLL에서 제공되는 함수를 사용할 때에는 함수선언 앞에 __declspec(dllimport) 지시자를 사용할 것을 강력히 권합니다.

대부분 DLL의 API는 헤더에서 다음과 같이 선언 되어있습니다.

// API Export/Import Header

#ifdef CALLEE_EXPORTS
#define CALLEE_API __declspec(dllexport)
#else
#define CALLEE_API __declspec(dllimport)
#endif

CALLEE_API DWORD __stdcall no_no_stdcall(DWORD); // WINAPI
CALLEE_API DWORD __cdecl no_no_cdecl(DWORD); // CDECL
CALLEE_API DWORD __fastcall no_no_fastcall(DWORD);
위 헤더의 핵심은 동일한 헤더를 공유해서 사용하게 되는 것입니다.
(특히 ThirdParty 벤더에서 제공되는 API모듈이라면 더욱더 그렇겠죠)

즉, Preprocessor definitions에 CALLEE_EXPORTS가 선언되어 있지 않다면 헤더는 함수를 __declspec(dllimport)형식으로 정의하게 되겠죠.
(위의 API 제공하는 벤더는 프로젝트 설정에 CALLEE_EXPORTS를 정의해서 컴파일하고 있을 것입니다.)

위의 예는 가장 확실하게 __declspec(dllimport)를 선언해서 사용할 수 있는 경우이고 헤더가 중복해서 관리해야하는 어려움도 필요없는 최적의 케이스 입니다.

하지만 내부 프로젝트 모듈이건 다른 벤더의 모듈이던 이렇게 사용되지 않고 다음과 같이 import용으로 별도로 정의되어서 사용되는 경우가 많이 있습니다.

// API Import Header

DWORD __stdcall no_no_stdcall(DWORD); // WINAPI
DWORD __cdecl no_no_cdecl(DWORD); // CDECL
DWORD __fastcall no_no_fastcall(DWORD);
이 경우는 헤더 관리의 복잡성을 떠나서 별로 좋지 않은 케이스 입니다.
(최소한 다른 DLL에 선언되어 있는 함수를 호출하는 경우에...)

DWORD __stdcall no_no_stdcall(DWORD);
__declspec(dllimport) DWORD __stdcall no_no_stdcall(DWORD);
DLL을 static link 할 경우에 위의 두가지 경우 모두 정상적으로 수행됩니다.
그렇다면 차이점이 무엇일까요?


DWORD __stdcall no_no_stdcall(DWORD);
위와 같이 선언된 상태에서 링커는 DLL에서 생성된 빈 Lib에서 가르키고 있는 Stub을 직접 호출하도록 합니다. 왜냐하면 컴파일러는 no_no_stdcall() 이라는 함수가 내부에 선언된 함수인지 다른 모듈(DLL)에 선언되어 있는 함수인지를 구별 할 수 없기 때문입니다.

그렇다면 이 경우 실제 해당 DLL의 함수가 어떻게 호출 될까요?
DLL생성시 함께 생성되는 빈 Lib의 Stub내부에는 실제 모듈(DLL)의 함수로 점프할 수 있는 코드가 컴파일 타임에 이미 생성되서 Stub안에 존재하기 때문에 가능한 것입니다.
즉 해당 Stub을 내부 함수처럼 호출하면 그곳에서 실제 모듈(DLL)의 함수를 가르키는 IAT(Import Address Table)를 참조해서 실제 API가 점프하게 되는 코드가 수행되는 것입니다.


정리해서 말씀드리면, DLL의 함수를 호출할 경우에 아래와 같은 두가지 형태의 코드가 생성될 수 있습니다.

// 1. 함수원형 앞에 __declspec(dllimport) 지시자를 사용할 경우
0xXXXXXXXX : CALL DWORD PTR [0x56780000]; // CALL DWORD PTR[_imp__MyFunc];


// 2. 지시자 없이 그냥 사용할 경우
0xXXXXXXXX : CALL 0x12340000;
0x12340000 : JMP DWORD PTR[0x56780000];


(0x5678000 : 실제함수주소를 가르키고있는 IAT의 주소)
위에서 설명드렸듯이 당연히 1번의 경우 DLL 함수호출시 훨씬 효과적인 것을 알 수 있습니다.
2번의 경우에는 JMP를 위한 추가적인 5Byte의 코드가 더 필요하게 되는 경우이므로 비효율적이겠죠.

__declspec(dllimport)지시자를 사용함으로서 1번의 코드형태로 DLL함수가 호출 되는 것을 알았습니다.
좀 더 설명드리면 컴파일러는 __declspec(dllimport) 지시자를 보았을 경우에 실제함수 MyFunc을 참조하지 않고 __imp__MyFunc을 참조하는 코드를 만들어 놓습니다.
그후에 링커는 __imp_ Prefix를 보고 실제 DLL에 존재하는 함수를 직접 호출하는 것임을 알게 되는 것입니다.

즉, 컴파일리가 링커에게 DLL에 존재하는 함수를 호출한다는 것을 알려주기 위해 __imp_ Prefix를 함수앞에 추가해 주는 것입니다.


: error LNK2001: unresolved external symbol __imp__MyFunc

: fatal error LNK1120: 1 unresolved externals
위의 에러를 보면 실제 함수앞에 __imp__ Prefix가 존재하는 것을 알 수 있습니다.

그런데 이 경우에 링크에러가 발생했는데 링크에러가 발생하지 않으려면 빈 Lib안에 __imp__MyFunc에 대한 Stub이 존재하고 있어야 겠죠.
DLL생성시 만들어진 Lib의 내용을 보면 MyFunc, __imp__MyFunc에대한 두가지 Stub이 모두 존재하고 있음을 볼 수 있습니다.

----------------------------------------------------------------------------------
// 확장DLL사용시 참고 사항

AFX_EXT_CLASS는  요렇게 정의되어 있습니다..


#ifdef _AFXEXT

    #define AFX_EXT_CLASS    __declspec(dllexport)

#else

    #define AFX_EXT_CALSS    __declspec(dllimport)

#endif // _AFXEXT


이 선언은 Extended DLL에서 export, 다른 프로젝트에서는 import로 해석됩니다...



그래서 결론만 말하면...........


#ifdef _MYEXT

    #define MY_EXT_CLASS    __declspec(dllexport)

#else

    #define MY_EXT_CLASS    __declspec(dllimport)

#endif


이렇게 헤더파일 처음에 정의하고,


class AFX_EXT_CALSS AAA

{

};



class MY_EXT_CLASS AAA

{

};


이렇게 선언합니다.......


그리고 DLL의 프로젝트 Setting에서 _MYEXT를 선언합니다.

(Project Menu의 Setting 다이얼로그에서 C++ tab의 Preprocessor

definitions에 _MYEXT를 추가합니다.)



이렇게 하면 class AAA는 DLL에서는 export, 다른 곳에서는 import가

됩니다.....