DLL이 프로세스별 또는 쓰레드별로 초기화를 수행하거나 정리할 목적으로 호출되기도 한다.
DLL의 진입점 함수의 skelton은 다음과 같이 구현하면 된다.
// dllmain.cpp : DLL 응용 프로그램의 진입점을 정의합니다.
#include "stdafx.h"
BOOL APIENTRY DllMain(HMODULE hModule, DWORD fdwReason, LPVOID lpReserved)
{
switch (fdwReason)
{
// -- PROCESS --
case DLL_PROCESS_ATTACH:
break;
case DLL_PROCESS_DETACH:
break;
//-- THREAD --
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
DllMain 함수는 대소문자를 구분한다.
실수로 DLLMain이라고 함수를 작성하더라도, 컴파일과 링크에는 아무 문제가 없지만,
DLLMain이라고 정의한 진입점 함수는 절대 호출되지 않을 것이다.
이는 곧 살펴 볼 DllMain과 C/C++ 런타임 라이브러리의 내용을 보면 쉽게 이해할 수 있을 것이다.
2. 파라미터
DllMain 함수는 세 개의 인자를 가지며, 각 인자는 다음을 의미한다.
- hModule : DLL의 인스턴스 핸들. DLL 파일 이미지가 매핑된 가상 메모리 주소값이다.
- fdwReason
- lpReserved
DllMain은 프로세스나 쓰레드가 시작 또는 종료할 때 호출되므로,
fdwReason은 다음 네 가지 중 하나가 될 수 있다.
- DLL_ATTACH_PROCESS
- DLL_DETACH_PROCESS
- DLL_ATTACH_THREAD
- DLL_DETACH_THREAD
각각의 의미와 특징에 대해선 아래 각 챕터들에서 자세히 설명하겠다.
마지막 인자인 lpReserved는 다음과 같이 값이 설정된다.
fdwReason가 DLL_PROCESS_ATTACH인 경우,
DLL이 명시적 런타임 링크 방식으로 로드되면 NULL,
DLL이 암시적 로드타임 링크 방식으로 로드되면 not NULL의 값을 가진다.
fdwReason이 DLL_PROCESS_DETACH인 경우,
FreeLibrary가 호출되었거나 DLL 로드가 실패한 경우라면 NULL,
프로세스가 종료중이라면 not NULL의 값을 가진다.
3. 주의 사항
특정 DLL에 대해 DllMain 함수가 호출된 시점에
동일 주소 공간에 로드된 다른 DLL들은 자신의 DllMain 함수를 미처 호출 못했을 수도 있다.
이는 다른 DLL들은 아직 초기화되지 않은 상태일 수도 있다는 것을 의미하므로,
DllMain 함수 내에서는 다른 DLL이 export 하고 있는 함수를 호출해서는 안 된다.
예를 들어 또다른 사용자 DLL의 함수나 User, Shell, ODBC, COM, RPC, Socket 등의 함수를 호출해서는 안 되는 것이다.
또한, DllMain 내에서는 LoadLibrary(Ex)나 FreeLibrary와 같은 함수를 호출해서도 안 되는데,
만일 이러한 함수를 호출하게 되면 여러 DLL들 사이에 의존 관계 루프가 생길 수 있다.
4. 진입점 함수는 필수?
참고로, DLL을 구현할 때 진입점 함수를 반드시 구현해야 하는 것은 아니다.
만일 소스 코드 내에 사용자가 정의한 DllMain 함수가 존재하지 않으면,
C/C++ 런타임 라이브러리 내에 자체적으로 구현되어 있는 DllMain 함수가 사용된다.
이 함수는 dllmain.c에 다음과 같이 구현되어 있다.
BOOL WINAPI DllMain(HANDLE hDllHandle, DWORD dwReason, LPVOID lpreserved)
{
#if defined (CRTDLL)
if (DLL_PROCESS_ATTACH == dwReason && nullptr == _pRawDllMain)
{
DisableThreadLibraryCalls(hDllHandle);
}
#endif -- defined (CRTDLL) --
return TRUE ;
}
2. DllMain의 네 가지 통지
이제 DllMain 함수가 호출될 때 네 가지 통지에 대해 자세하게 알아보도록 하자.
1. DLL_PROCESS_ATTACH
DLL이 프로세스의 주소 공간에 최초로 매핑되면,
fdwReason 매개변수에 DLL_PROCESS_ATTACH 값을 전달하여 해당 DLL의 DllMain 함수를 호출해 준다.
이러한 동작은 DLL 파일이 처음으로 매핑될 때에 한해서만 발생한다.
만일 쓰레드가 프로세스의 주소 공간에 이미 매핑되어 있는 DLL에 대해 추가적으로 LoadLibrary(Ex)를 호출하게 되면,
운영체제는 단순히 DLL의 usage count 값을 증가시키는 작업만을 수행할 뿐이며,
DLL_PROCESS_ATTACH를 인자 값으로 DllMain 함수를 다시 호출해 주지는 않는다.
DllMain에 DLL_PROCESS_ATTACH가 전달된 경우 DllMain 함수의 반환 값은
DLL의 초기화가 성공적으로 수행되었는지의 여부를 나타내게 된다.
(fdwReason이 다른 값인 경우에는 DllMain의 반환값은 아무런 의미를 가지지 않는다)
새로운 프로세스가 생성되면 시스템은 프로세스 주소 공간을 생성하고,
.exe 파일과 수행에 필요한 모든 DLL 파일을 프로세스 주소 공간에 매핑한다.
이후 프로세스의 메인 쓰레드를 생성하게 되는데, 암시적 로드타임 링킹에서는 프로세스의 메인 쓰레드가
로드된 DLL들이 가지고 있는 DllMain 함수 각각을 DLL_PROCESS_ATTACH 값을 인자로 호출하게 된다.
프로세스 주소 공간에 매핑된 모든 DLL들이 DLL_PROCESS_ATTACH 통지에 대해 정상적으로 회신하게 되면,
시스템은 프로세스의 메인 쓰레드가 실행 모듈에 포함되어 있는 C/C++ 런타임 시작 코드를 수행할 수 있도록 하여,
이는 결국 실행 모듈의 진입점 함수를 호출하게 된다.
위 과정 중 하나의 DLL이라도 초기화에 실패하여, DllMain 함수가 FALSE를 반환하게 되면,
시스템은 해당 프로셋를 종료하게 된다.
이제 DLL을 명시적 런타임 링크하는 경우에 어떤 일이 일어나느지 살펴보자.
프로세스의 특정 쓰레드가 LoadLibrary(Ex)를 호출하게 되면,
시스템은 지정한 DLL을 찾아서 프로세스의 주소 공간에 해당 DLL을 매핑하게 된다.
이후 시스템은 LoadLibrary(Ex) 함수를 호출하였던 쓰레드로 하여금,
해당 DLL 내에 포함되어 있는 DllMain 함수를 DLL_ATTACH_PROCESS 값을 인자로 하여 호출하도록 한다.
DllMain 함수가 이러한 통지를 완전히 처리하고 나면 LoadLibrary(Ex) 함수가 반환되고,
쓰레드는 자신이 수행하던 코드를 계속 수행할 수 있게 된다.
암시적 로드타임 링크 방식이든, 명시적 런타임 링크 방식이든
DllMain을 호출하는 쓰레드는 DllMain 함수가 반환될 때까지 쓰레드 수행이 블록됨을 기억하자.
글로 길게 쓴 내용을 정리하는 차원에서 순서도를 첨부한다.
2. DLL_PROCESS_DETACH
DLL이 프로세스 주소 공간으로부터 매핑 해제되면,
fdwReason 매개변수에 DLL_PROCESS_DETACH 값을 전달하여, 해당 DLL의 DllMain 함수를 호출해 준다.
DLL이 프로세스 주소 공간으로부터 매핑이 해제되는 경우는 두 가지가 있다.
1. 프로세스 종료
프로세스가 종료되어 DLL을 매핑 해제해야 하는 경우에는
ExitProcess 함수를 호출한 쓰레드가 DllMain 함수를 호출할 책임이 있다.
보통의 상황에서는 사용자가 정의한 엔트리 포인트 함수가 반환되어,
C/C++ 런타임 라이브러리의 시작 코드로 제어가 돌아오게 되면,
시작 코드는 프로세스를 종료하기 위해 명시적으로 ExitProcess를 호출하는데 정상적인 상황이라면 메인 쓰레드에서 수행된다.
만일 특정 쓰레드가 TerminateProcess 함수를 호출하여 프로세스를 종료하는 경우에는
DLL_PROCESS_DETACH를 인자 값으로 어떤 DLL의 DllMain 함수도 호출되지 않음에 주의하라.
2. FreeLibrary 호출
프로세스 내의 쓰레드가 FreeLibrary 함수를 호출하여 DLL을 매핑 해제하는 경우에는
해당 함수를 호출한 쓰레드가 DllMain 함수를 수행하게 된다.
FreeLibrary 함수를 호출한 경우에는 DllMain 함수가
DLL_PROCESS_DETACH 통지에 대한 처리를 완료하기 전까지 해당 쓰레드는 반환되지 않는다. (즉, 수행되지 않는다)
FreeLibrary를 호출하는 경우의 순서도를 첨부한다.
그리고 만약에 시스템이 DLL_PROCESS_ATTACH 값을 인자로 DllMain을 호출하였을 때,
반환값이 FALSE로 돌아온 경우에는 DLL_PROCESS_DETACH 값을 인자로 DllMain을 호출하지 않음을 잊지 말아야 한다.
3. DLL_THREAD_ATTACH
프로세스 내에 새로운 쓰레드가 생성되면, 시스템은 fdwReason 매개변수에 DLL_THREAD_ATTACH 값을 전달하여
현재 프로세스 주소 공간에 매핑되어 있는 모든 DLL의 DllMain 함수를 호출해 준다.
이러한 통지 과정을 통해 모든 DLL들은 쓰레드별로 초기화 과정을 수행할 수 있게 된다.
새로이 생성된 쓰레드는 모든 DLL들의 DllMain 함수를 호출할 책임이 있으며,
모든 DLL들의 DllMain 함수가 반환된 이후 자신의 쓰레드 함수의 수행을 재개할 수 있다.
헤깔리면 안 되는 것이 있는데,
쓰레드 생성과 DLL 로드의 순서가 뒤바뀌는 경우엔 DllMain 함수가 호출되지 않는다는 점이다.
즉, 이미 쓰레드들이 생성되어 있는 상태에서 새로운 DLL이 로드된다 해도,
기존 쓰레드들에 대해 DLL_THREAD_ATTACH 값을 인자로 DllMain 함수를 호출하지 않는다는 뜻이다.
또한 시스템은 프로세스의 메인 쓰레드에 대해선 DLL_THREAD_ATTACH 값을 전달하여 DllMain 함수를 호출하지 않는다.
이 내용을 잘 추려보면 다음과 같은 결론을 얻을 수 있다.
메인 쓰레드 외 유저가 생성하는 쓰레드들에 대해 DLL 관련 초기화를 해야 한다면,
DLL을 먼저 로드하고, 그 DLL에 접근하는 쓰레드를 생성하는 것이 올바른 순서이다.
4. DLL_THREAD_DETACH
쓰레드를 종료하기 위해 최종적으로 ExitThread 함수를 호출하게 되는데,
이 함수는 시스템에게 쓰레드가 종료되길 원한다는 사실을 전달하게 된다.
하지만, 시스템은 쓰레드를 이 함수가 호출되는 순간에 바로 죽이지 않고,
프로세스의 주소 공간에 매핑된 모든 DLL의 DllMain 함수를 DLL_THREAD_DETACH 값을 인자로 하여 호출한 후에
모든 DLL들이 DllMain 함수를 반환하고 나면, 그제서야 쓰레드를 종료한다.
예를 들어, DLL 버전의 C/C++ 런타임 라이브러리의 경우 멀티 쓰레드 애플리케이션을 위해
사용하였던 데이터 블록 해제 작업을 바로 이 시점에 수행한다.
만일 특정 쓰레드가 TerminateThread 함수를 호출하여 쓰레드를 종료하는 경우에는
DLL_THREAD_DETACH를 인자 값으로 어떤 DLL의 DllMain 함수도 호출되지 않음에 주의하라.
3. DllMain과 C/C++ 런타임 라이브러리
C/C++ 런타임 라이브러리의 진입점 함수를 통해 DllMain이 호출되게 된다.
C++ 클래스의 인스턴스를 저장하기 위한 전역 변수를 가지는 DLL을 제작한다고 가정해 보자.
DllMain 함수 내에서 클래스 전역 변수를 안전하게 사용하려면,
해당 클래스의 생성자가 DllMain 함수보다 먼저 호출되어야 한다.
이러한 작업들이 모두 C/C++ 런타임 라이브러리 DLL 시작 코드에서 수행되는 것이다.
링커는 DLL을 링크하는 동안 파일 이미지에 DLL의 진입점 함수의 주소를 포함시켜 준다.
기본적으로 M$ 링커에 /DLL 스위치를 이용하여 링크를 수행하게 되면,
_DllMainCRTStartup이라는 이름의 함수를 진입점 함수로 지정하게 된다.
이 함수는 C/C++ 런타임 라이브러리에 포함되어 있으며,
DLL을 링크할 때 항상 DLL 파일 이미지 내에 정적으로 링크 된다.
프로세스의 주소 공간에 DLL 파일 이미지가 매핑되면, 실제로 시스템은 DllMain 함수를 호출하는 것이 아니라,
_DllMainCRTStartup 함수를 호출한다.
_DllMainCRTStartup 함수는 기본적으로 __DllMainCRTStartup 함수로 모든 통지 내용을 전달하는데,
DLL_PROCESS_ATTACH 통지가 전달된 경우에는 /GS 스위치가 제공하는 보안 기능을 추가로 수행한다.
_DllMainCRTStartup 함수가 DLL_PROCESS_ATTACH 통지를
__DllMainCRTStartup 함수로 전달하면, 아래 순서로 처리한다.
- C/C++ 런타임 라이브러리 초기화
- 전역 혹은 정적 C++ 객체를 생성
- DllMain 함수 호출
DLL에 DLL_PROCESS_DETACH 통지를 전달해야 하는 경우 시스템은
__DllMainCRTStartup 함수를 다시 한 번 호출해 주며, 아래 순서로 처리한다.
- DllMain 함수 호출 후 반환
- DLL 내의 전역 혹은 정적 C++ 오브젝트의 파괴자를 호출
참고로, __DLLMainCRTStartup 함수가 DLL_THREAD_ATTACH/DETACH 통지를 전달받은 경우엔
PTD(Per Thread Data)의 생성과 해제 작업이 수반된다.
4. 순차적인 DllMain 호출
시스템은 순차적으로 DLL의 DllMain 함수를 호출한다.
이것은 예를 통해 설명을 해야 편할 듯 하다.
어떤 프로세스가 A/B 쓰레드를 가지고 있고, X.dll을 프로세스 주소 공간에 로드/매핑 중이다.
이 상황에서 A 쓰레드는 C 쓰레드를, B 쓰레드는 D 쓰레드를 생성한다고 하면...
A 쓰레드가 C 쓰레드를 생성하기 위해 CreateThread를 수행하게 되면,
시스템은 DLL_THREAD_ATTACH 값을 인자로 X.dll의 DllMain 함수를 호출하게 된다.
새로 생성된 C 쓰레드가 DllMain 함수를 수행하는 동안,
B 쓰레드가 D 쓰레드를 생성하기 위해 CreateThread를 수행하였다.
이후 시스템은 D 쓰레드 역시 DLL_THREAD_ATTACH 값을 인자로 X.dll의 DllMain 함수를 호출하게 된다.
하지만, 시스템은 DllMain 함수를 순차적으로 수행할 것이기 때문에,
C 쓰레드가 DllMain 내부의 코드를 완전히 수행하고 빠져 나올 때까지 D 쓰레드를 일시 정지시키게 된다.
이후 C 쓰레드가 DllMain 함수를 모두 수행하고 나면,
시스템은 D 쓰레드를 깨워 DllMain 함수를 수행하도록 한다.