다음을 통해 공유


DLL 및 Visual C++ 런타임 라이브러리 동작

Visual Studio를 사용하여 DLL(동적 연결 라이브러리)을 빌드하는 경우 기본적으로 링커는 VCRuntime(Visual C++ 런타임 라이브러리)을 포함합니다. VCRuntime에는 C/C++ 실행 파일을 초기화하고 종료하는 데 필요한 코드가 포함되어 있습니다. DLL에 연결된 경우 VCRuntime 코드는 DLL에 대한 Windows OS 메시지를 처리하여 프로세스 또는 스레드에 연결하거나 분리하는 _DllMainCRTStartup이라는 내부 DLL 진입점 함수를 제공합니다. _DllMainCRTStartup 함수는 스택 버퍼 보안 설정, CRT(C 런타임 라이브러리) 초기화 및 종료, 정적 및 전역 개체에 대한 생성자 및 소멸자 호출과 같은 필수 작업을 수행합니다. _DllMainCRTStartup은 WinRT, MFC, ATL 등의 다른 라이브러리에 대해 후크 함수를 호출하여 자체적으로 초기화 및 종료를 수행합니다. 이 초기화가 없으면 CRT 및 기타 라이브러리와 정적 변수는 초기화되지 않은 상태로 유지됩니다. DLL이 정적으로 연결된 CRT 또는 동적으로 연결된 CRT DLL을 사용하는지 관계없이 동일한 VCRuntime 내부 초기화 및 종료 루틴이 호출됩니다.

기본 DLL 진입점 _DllMainCRTStartup

Windows에서 모든 DLL에는 초기화 및 종료 모두를 위해 호출되는 선택적 진입점 함수(일반적으로 DllMain이라고 함)가 포함될 수 있습니다. 이를 통해 필요에 따라 추가 리소스를 할당하거나 해제할 수 있습니다. Windows에서는 프로세스 연결, 프로세스 분리, 스레드 연결 및 스레드 분리의 네 가지 상황에서 진입점 함수를 호출합니다. DLL을 사용하는 애플리케이션이 로드될 때 또는 애플리케이션이 런타임에 DLL을 요청하여 DLL이 프로세스 주소 공간에 로드되면 운영 체제에서 DLL 데이터의 개별 복사본을 만듭니다. 이를 프로세스 연결이라고 합니다. 스레드 연결 은 DLL이 로드되는 프로세스가 새 스레드를 만들 때 발생합니다. 스레드 분리 는 스레드가 종료될 때 발생하며 프로세스 분리 는 DLL이 더 이상 필요하지 않고 애플리케이션에서 해제되는 경우입니다. 운영 체제는 이러한 각 이벤트의 DLL 진입점에 대한 개별 호출을 수행하여 각 이벤트 유형에 대한 reason 인수를 전달합니다. 예를 들어 OS는 reason 인수로 DLL_PROCESS_ATTACH를 보내 프로세스 연결을 알립니다.

VCRuntime 라이브러리는 기본 초기화 및 종료 작업을 처리하는 _DllMainCRTStartup이라는 진입점 함수를 제공합니다. 프로세스 연결에서 _DllMainCRTStartup 함수는 버퍼 보안 검사를 설정하고, CRT 및 기타 라이브러리를 초기화하고, 런타임 형식 정보를 초기화하고, 정적 및 비 로컬 데이터의 생성자를 초기화 및 호출하고, 스레드 로컬 저장소를 초기화하고, 각 연결에 대한 내부 정적 카운터를 증가시키고, 사용자 또는 라이브러리에서 제공한 DllMain을 호출합니다. 프로세스 분리에서는 함수가 이러한 단계를 역순으로 진행합니다. DllMain을 호출하고, 내부 카운터를 감소시키고, 소멸자를 호출하고, CRT 종료 함수 및 등록된 atexit 함수를 호출하고, 다른 종료 라이브러리에 알립니다. 연결 카운터가 0에 도달하면 함수는 FALSE를 반환하여 Windows에 DLL을 언로드할 수 있음을 표시합니다. _DllMainCRTStartup 함수는 스레드 연결 및 스레드 분리 시에도 호출됩니다. 이러한 경우 VCRuntime 코드는 자체적으로 추가 초기화 또는 종료를 수행하지 않으며 DllMain을 호출하여 메시지를 전달하기만 합니다. DllMain이 프로세스 연결에서 FALSE를 반환하여 실패를 표시하는 경우, _DllMainCRTStartupDllMain을 다시 호출하고 reason 인수로 DLL_PROCESS_DETACH를 전달한 다음 나머지 종료 프로세스를 진행합니다.

Visual Studio에서 DLL을 빌드할 때 VCRuntime에서 제공하는 기본 진입점 _DllMainCRTStartup 이 자동으로 연결됩니다. /ENTRY(진입점 기호) 링커 옵션을 사용하여 DLL에 대한 진입점 함수를 지정할 필요가 없습니다.

참고 항목

/ENTRY: 링커 옵션을 사용하여 DLL에 대한 다른 진입점 함수를 지정할 수 있지만 진입점 함수는 _DllMainCRTStartup이 수행하는 모든 작업을 동일한 순서로 복제해야 하기 때문에 권장하지 않습니다. VCRuntime은 동작을 복제할 수 있는 함수를 제공합니다. 예를 들어 __security_init_cookie/gs (buffer security check) 버퍼 검사 옵션을 지원 하기 위해 프로세스 연결에서 즉시 호출할 수 있습니다. 진입점 함수와 동일한 매개 변수를 전달하여 _CRT_INIT 함수를 호출해 나머지 DLL 초기화 또는 종료 기능을 수행할 수 있습니다.

DLL 초기화

DLL에는 DLL 로드 시 실행되어야 하는 초기화 코드가 있을 수 있습니다. 사용자가 자체 DLL 초기화 및 종료 기능을 수행할 수 있도록 _DllMainCRTStartup은 사용자가 제공할 수 있는 DllMain이라는 함수를 호출합니다. DllMain에는 DLL 진입점에 필요한 서명이 있어야 합니다. 기본 진입점 함수 _DllMainCRTStartup은 Windows에서 전달하는 것과 동일한 매개 변수를 사용하여 DllMain을 호출합니다. DllMain 함수를 제공하지 않는 경우 기본적으로 Visual Studio는 _DllMainCRTStartup가 항상 호출할 수 있도록 사용자 대신 함수를 제공하고 연결합니다. 그러므로 DLL을 초기화할 필요가 없는 경우 DLL을 빌드할 때 특별한 작업을 수행할 필요가 없습니다.

다음은 DllMain에 사용되는 서명입니다.

#include <windows.h>

extern "C" BOOL WINAPI DllMain (
    HINSTANCE const instance,  // handle to DLL module
    DWORD     const reason,    // reason for calling function
    LPVOID    const reserved); // reserved

일부 라이브러리는 DllMain 함수를 래핑합니다. 예를 들어 일반 MFC DLL에서 CWinApp 개체의 InitInstanceExitInstance 멤버 함수를 구현하여 DLL에서 요구하는 초기화 및 종료 작업을 수행합니다. 자세한 내용은 일반 MFC DLL 초기화 섹션을 참조하세요.

Warning

DLL 진입점에서 안전하게 수행할 수 있는 작업은 상당히 제한적입니다. 호출 DllMain하기에 안전하지 않은 특정 Windows API에 대한 자세한 내용은 일반 모범 사례를 참조 하세요. 가장 간단한 초기화가 필요한 경우가 아닌 한 DLL에 대한 초기화 함수에서 이를 수행합니다. DllMain이 실행되고 DLL의 다른 함수를 호출하기 전에 애플리케이션에서 초기화 함수를 호출하도록 할 수 있습니다.

일반(비 MFC) DLL 초기화

VCRuntime에서 제공하는 _DllMainCRTStartup 진입점을 사용하는 일반(비 MFC) DLL에서 초기화를 수행하려면 DLL 소스 코드에 DllMain이라는 함수가 포함되어 있어야 합니다. 다음 코드는 DllMain 정의에 대한 기본 구조를 제공합니다.

#include <windows.h>

extern "C" BOOL WINAPI DllMain (
    HINSTANCE const instance,  // handle to DLL module
    DWORD     const reason,    // reason for calling function
    LPVOID    const reserved)  // reserved
{
    // Perform actions based on the reason for calling.
    switch (reason)
    {
    case DLL_PROCESS_ATTACH:
        // Initialize once for each new process.
        // Return FALSE to fail DLL load.
        break;

    case DLL_THREAD_ATTACH:
        // Do thread-specific initialization.
        break;

    case DLL_THREAD_DETACH:
        // Do thread-specific cleanup.
        break;

    case DLL_PROCESS_DETACH:
        // Perform any necessary cleanup.
        break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}

참고 항목

이전 Windows SDK 설명서에서는 링커 명령줄에서 /ENTRY 옵션을 사용하여 DLL 진입점 함수의 실제 이름을 지정해야 한다고 설명했습니다. Visual Studio에서는 진입점 함수의 이름이 DllMain인 경우 /ENTRY 옵션을 사용할 필요가 없습니다. 실제로는 /ENTRY 옵션을 사용하여 진입점 함수 이름을 DllMain 이외의 이름으로 지정하는 경우 진입점 함수가 _DllMainCRTStartup과 동일하게 초기화 호출을 수행하지 않으면 CRT가 제대로 초기화되지 않습니다.

일반 MFC DLL 초기화

일반 MFC DLL에는 CWinApp 개체가 있으므로 MFC 애플리케이션과 동일한 위치, 즉 DLL의 CWinApp 파생 클래스의 InitInstanceExitInstance 멤버 함수에서 초기화 및 종료 작업을 수행해야 합니다. MFC는 _DllMainCRTStartupDLL_PROCESS_ATTACHDLL_PROCESS_DETACH에 대해 호출하는 DllMain 함수를 제공하므로 자체 DllMain 함수를 작성하면 안됩니다. MFC 제공 DllMain 함수는 DLL이 로드될 때 InitInstance를 호출하고 DLL이 언로드되기 전에 ExitInstance를 호출합니다.

일반 MFC DLL은 InitInstance 함수에서 TlsAllocTlsGetValue를 호출하여 여러 스레드를 추적할 수 있습니다. 이러한 함수를 사용하면 DLL이 스레드별 데이터를 추적할 수 있습니다.

MFC에 동적으로 연결되는 일반 MFC DLL에서 MFC OLE, MFC 데이터베이스(또는 DAO) 또는 MFC 소켓 지원을 각각 사용하는 경우 디버그 MFC 확장 DLL인 MFCOversionD.dll, MFCDversionD.dll 및 MFCNversionD.dll(여기서 version은 버전 번호)가 자동으로 연결됩니다. 일반 MFC DLL의 CWinApp::InitInstance에서 사용하는 각 DLL에 대해 다음과 같은 미리 정의된 초기화 함수 중 하나를 호출해야 합니다.

MFC 형식 지원 호출할 초기화 함수
MFC OLE(MFCOversionD.dll) AfxOleInitModule
MFC 데이터베이스(MFCDversionD.dll) AfxDbInitModule
MFC 소켓(MFCNversionD.dll) AfxNetInitModule

MFC 확장 DLL 초기화

MFC 확장 DLL에는 일반 MFC DLL과 같은 CWinApp 파생 개체가 없으므로 MFC DLL 마법사가 생성하는 DllMain 함수에 초기화 및 종료 코드를 추가해야 합니다.

이 마법사는 MFC 확장 DLL에 대해 다음 코드를 제공합니다. 코드에서 PROJNAME은 프로젝트의 이름에 대한 자리 표시자입니다.

#include "pch.h" // For Visual Studio 2017 and earlier, use "stdafx.h"
#include <afxdllx.h>

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
static AFX_EXTENSION_MODULE PROJNAMEDLL;

extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
   if (dwReason == DLL_PROCESS_ATTACH)
   {
      TRACE0("PROJNAME.DLL Initializing!\n");

      // MFC extension DLL one-time initialization
      AfxInitExtensionModule(PROJNAMEDLL,
                                 hInstance);

      // Insert this DLL into the resource chain
      new CDynLinkLibrary(Dll3DLL);
   }
   else if (dwReason == DLL_PROCESS_DETACH)
   {
      TRACE0("PROJNAME.DLL Terminating!\n");
   }
   return 1;   // ok
}

초기화 도중 새 CDynLinkLibrary 개체를 만들면 MFC 확장 DLL에서 CRuntimeClass 개체 또는 리소스를 클라이언트 애플리케이션으로 내보낼 수 있습니다.

하나 이상의 일반 MFC DLL에서 MFC 확장 DLL을 사용하려는 경우 CDynLinkLibrary 개체를 만드는 초기화 함수를 내보내야 합니다. 이 함수는 MFC 확장 DLL을 사용하는 각 일반 MFC DLL에서 호출해야 합니다. 이 초기화 함수를 호출하는 적절한 위치는 MFC 확장 DLL의 내보낸 클래스 또는 함수를 사용하기 전에 일반 MFC DLL의 CWinApp 파생 개체의 InitInstance 멤버 함수입니다.

MFC DLL 마법사가 생성하는 DllMain에서 AfxInitExtensionModule을 호출하면 모듈의 런타임 클래스(CRuntimeClass 구조체)가 캡처되고 CDynLinkLibrary 개체가 만들어질 때 사용할 수 있도록 개체 팩터리(COleObjectFactory 개체)도 캡처됩니다. AfxInitExtensionModule의 반환 값을 확인해야 합니다. AfxInitExtensionModule에서 0 값을 반환하는 경우 DllMain 함수에서 0을 반환합니다.

MFC 확장 DLL이 실행 파일에 명시적으로 연결된 경우(즉 실행 파일이 AfxLoadLibrary를 호출하여 DLL로 연결됨) DLL_PROCESS_DETACH에서 AfxTermExtensionModule 호출을 추가해야 합니다. 이 함수를 사용하면 각 프로세스가 MFC 확장 DLL에서 분리될 때(프로세스가 종료되거나 DLL이 AfxFreeLibrary 호출로 인해 언로드되는 경우에 발생) MFC가 MFC 확장 DLL을 정리할 수 있습니다. MFC 확장 DLL이 애플리케이션에 암시적으로 연결되는 경우 AfxTermExtensionModule 호출이 필요하지 않습니다.

MFC 확장 DLL에 명시적으로 연결되는 애플리케이션은 DLL을 해제할 때 AfxTermExtensionModule을 호출해야 합니다. 또한 애플리케이션에서 여러 스레드를 사용하는 경우 Win32 함수 LoadLibraryFreeLibrary 대신 AfxLoadLibraryAfxFreeLibrary를 사용해야 합니다. AfxLoadLibraryAfxFreeLibrary를 사용하면 MFC 확장 DLL이 로드 및 언로드될 때 실행되는 시작 및 종료 코드가 전역 MFC 상태를 손상하지 않습니다.

MFCx0.dll은 DllMain이 호출될 때 완전히 초기화되기 때문에 DllMain 내에서 메모리를 할당하고 MFC 함수를 호출할 수 있습니다(16비트 버전 MFC와는 다름).

확장 DLL은 DllMain 함수의 DLL_THREAD_ATTACHDLL_THREAD_DETACH 사례를 처리하여 다중 스레딩을 관리할 수 있습니다. 이러한 사례는 스레드가 DLL에서 연결 및 분리될 때 DllMain에 전달됩니다. DLL이 연결될 때 TlsAlloc을 호출하면 DLL이 DLL에 연결된 모든 스레드의 TLS(스레드 로컬 스토리지) 인덱스를 유지할 수 있습니다.

헤더 파일 Afxdllx.h에는 AFX_EXTENSION_MODULECDynLinkLibrary 정의와 같은 MFC 확장 DLL에서 사용되는 구조체에 대한 특수 정의가 포함되어 있습니다. MFC 확장 DLL에 이 헤더 파일을 포함해야 합니다.

참고 항목

pch.h(Visual Studio 2017 및 이전 버전의stdafx.h)의 _AFX_NO_XXX 매크로는 정의하거나 정의 해제하지 않는 것이 중요합니다. 이러한 매크로는 특정 대상 플랫폼이 해당 기능을 지원하는지 여부를 확인하기 위한 목적으로만 존재합니다. 프로그램을 작성하여 이러한 매크로를 확인할 수 있지만(예: #ifndef _AFX_NO_OLE_SUPPORT) 프로그램이 이러한 매크로를 정의하거나 정의 해제해서는 안됩니다.

다중 스레딩을 처리하는 샘플 초기화 함수는 Windows SDK의 동적 연결 라이브러리에서 스레드 로컬 스토리지 사용에 포함되어 있습니다. 이 샘플은 LibMain이라는 진입점 함수를 포함하지만 MFC 및 C 런타임 라이브러리와 함께 작동하도록 이 함수의 이름을 DllMain으로 지정해야 합니다.

참고 항목

Visual Studio에서 C/C++ DLL 만들기
DllMain 진입점
동적 연결 라이브러리 모범 사례