혼합형 어셈블리 초기화

Windows 개발자는 코드를 실행하는 동안 DllMain항상 로더 잠금을 주의해야 합니다. 그러나 C++/CLI 혼합 모드 어셈블리를 처리할 때 고려해야 할 몇 가지 추가 문제가 있습니다.

DllMain의 코드는 .NET CLR(공용 언어 런타임)에 액세스해서는 안 됩니다. 즉 DllMain , 직접 또는 간접적으로 관리되는 함수를 호출하지 않아야 하며, 관리 코드를 선언하거나 구현 DllMain해서는 안 되며, 가비지 수집 또는 자동 라이브러리 로드가 수행되지 않아야 합니다 DllMain.

로더 잠금 원인

.NET 플랫폼이 도입되면 실행 모듈(EXE 또는 DLL)을 로드하는 두 가지 메커니즘, 즉 관리되지 않는 모듈에 사용되는 Windows용 메커니즘과 .NET 어셈블리를 로드하는 CLR용 메커니즘이 있습니다. 혼합 DLL 로드 문제는 Microsoft Windows OS 로더에 주로 관련된 것입니다.

.NET 구문만 포함하는 어셈블리가 프로세스에 로드되면 CLR 로더는 필요한 모든 로드 및 초기화 작업 자체를 수행할 수 있습니다. 그러나 네이티브 코드와 데이터를 포함할 수 있는 혼합 어셈블리를 로드하려면 Windows 로더도 사용해야 합니다.

Windows 로더는 코드가 초기화되기 전에 해당 DLL의 코드 또는 데이터에 액세스할 수 없음을 보장합니다. 또한 부분적으로 초기화되는 동안 어떤 코드도 DLL을 중복으로 로드할 수 없습니다. 이를 위해 Windows 로더는 모듈을 초기화하는 동안 안전하지 않은 액세스를 방지하는 프로세스 전역 중요 섹션("로더 잠금"이라고도 함)을 사용합니다. 따라서 로드 프로세스는 기존의 여러 교착 시나리오에 취약합니다. 혼합형 어셈블리의 경우 다음과 같은 두 가지 시나리오에서 특히 교착 상태의 위험이 증가합니다.

  • 첫째, 로더 잠금이 고정 이니셜라이저에서 DllMain 또는 정적 이니셜라이저에서 유지될 때 사용자가 MSIL(Microsoft Intermediate Language)로 컴파일된 함수를 실행하려고 하면 교착 상태가 발생할 수 있습니다. MSIL 함수가 아직 로드되지 않은 어셈블리의 형식을 참조하는 경우를 고려합니다. CLR은 이 어셈블리를 자동으로 로드하려고 하며, 이를 위해서는 Windows 로더에서 로드 잠금을 걸어야 합니다. 로더 잠금이 호출 시퀀스의 앞부분에 있는 코드에 의해 이미 유지되었으므로 교착 상태가 발생합니다. 그러나 로더 잠금에서 MSIL을 실행해도 교착 상태가 발생하지는 않습니다. 이것이 이 시나리오를 진단하고 수정하기 어렵게 만드는 이유입니다. 참조된 형식의 DLL에 네이티브 구문이 없고 모든 종속성에 네이티브 구문이 없는 경우와 같은 경우에 Windows 로더는 참조된 형식의 .NET 어셈블리를 로드할 필요가 없습니다. 또한 필수 어셈블리나 해당 혼합 네이티브/.NET 종속 항목이 이미 다른 코드를 통해 로드되었을 수도 있습니다. 따라서 교착 상태는 예측하기가 매우 어려우며 대상 컴퓨터의 구성에 따라 크게 달라질 수 있습니다.

  • 둘째, .NET Framework 버전 1.0 및 1.1에서 DLL을 로드할 때 CLR은 로더 잠금이 유지되지 않았다고 가정하고 로더 잠금에서 잘못된 몇 가지 작업을 수행했습니다. 로더 잠금이 유지되지 않는다고 가정하는 것은 순수 .NET DLL에 대한 유효한 가정입니다. 그러나 혼합 DLL은 네이티브 초기화 루틴을 실행하기 때문에 네이티브 Windows 로더 및 결과적으로 로더 잠금이 필요합니다. 따라서 개발자가 DLL 초기화 중에 MSIL 함수를 실행하려고 하지 않더라도 .NET Framework 버전 1.0 및 1.1에서는 비결정적 교착 상태가 발생할 가능성이 적습니다.

혼합 DLL 로드 프로세스의 모든 불명확한 요소가 제거되었습니다. 이 작업은 다음과 같은 변경으로 수행되었습니다.

  • CLR이 혼합 DLL을 로드할 때 잘못된 가정을 내리지 않습니다.

  • 관리되지 않는 초기화와 관리되는 초기화는 별도의 두 단계로 수행됩니다. 관리되지 않는 초기화는 먼저(통해 DllMain) 발생하며 관리되는 초기화는 나중에 .를 통해 이루어집니다. NET 지원 .cctor 구문입니다. 후자는 사용하지 않는 한 /Zl/NODEFAULTLIB 사용자에게 완전히 투명합니다. 자세한 내용은/NODEFAULTLIB (라이브러리 무시)/Zl (기본 라이브러리 이름 생략)을 참조하세요.

로더 잠금이 여전히 발생할 수 있지만 이제 잠금이 일정하게 발생하며 이를 감지할 수 있습니다. MSIL 명령이 포함된 경우 DllMain 컴파일러는 경고 컴파일러 경고(수준 1) C4747을 생성합니다. 또한 CRT 또는 CLR에서는 로더 잠금 상황에서 MSIL을 실행하려는 시도를 감지 및 보고합니다. CRT에서 이러한 상황이 감지되면 런타임 진단 C 런타임 오류 R6033이 발생합니다.

이 문서의 나머지 부분에는 MSIL이 로더 잠금에서 실행할 수 있는 다시 기본 시나리오에 대해 설명합니다. 이러한 각 시나리오 및 디버깅 기술에서 문제를 해결하는 방법을 보여 줍니다.

시나리오 및 해결 방법

로더 잠금 상태에서 사용자 코드가 MSIL을 실행할 수 있는 상황에는 여러 가지가 있습니다. 개발자는 사용자 코드 구현이 이러한 각 상황에서 MSIL 지침을 실행하려고 시도하지 않는지 확인해야 합니다. 다음 각 하위 섹션에서는 가능한 모든 상황과 가장 일반적인 경우에 발생하는 문제를 해결하는 방법에 대해 설명합니다.

DllMain

함수 DllMain 는 DLL에 대한 사용자 정의 진입점입니다. 사용자가 별도로 지정하지 않으면 프로세스나 스레드를 포함 DLL에 연결하거나 해당 DLL에서 분리할 때마다 DllMain 이 호출됩니다. 이러한 호출은 로더 잠금 상태에서 발생할 수 있으므로 사용자가 제공한 DllMain 함수를 MSI로 컴파일하지 않아야 합니다. 또한 루트가 DllMain 에 있는 호출 트리의 어떠한 함수도 MSIL로 컴파일할 수 없습니다. 여기서 문제를 해결하려면 정의하는 DllMain 코드 블록을 .로 #pragma unmanaged수정해야 합니다. DllMain 을 호출하는 모든 함수에 대해 동일한 작업을 수행해야 합니다.

이러한 함수가 다른 호출 컨텍스트에 대해 MSIL 구현이 필요한 함수를 호출해야 하는 경우 .NET과 동일한 함수의 네이티브 버전이 모두 만들어지는 중복 전략을 사용할 수 있습니다.

또는 DllMain 필요하지 않거나 로더 잠금에서 실행할 필요가 없는 경우 사용자가 제공한 DllMain 구현을 제거하여 문제를 제거할 수 있습니다.

DllMain MSIL을 직접 실행하려고 하면 컴파일러 경고(수준 1) C4747이 발생합니다. 그러나 컴파일러는 다른 모듈에서 함수를 DllMain 호출하여 MSIL을 실행하려고 하는 경우를 검색할 수 없습니다.

이 시나리오에 대한 자세한 내용은 진단 장애를 참조 하세요.

정적 개체 초기화

동적 이니셜라이저가 필요한 경우 정적 개체를 초기화하면 교착 상태가 발생할 수 있습니다. 컴파일 시 알려진 값을 정적 변수에 할당하는 경우와 같은 간단한 경우는 동적 초기화가 필요하지 않으므로 교착 상태가 발생할 위험이 없습니다. 그러나 일부 정적 변수는 컴파일 시간에 평가할 수 없는 함수 호출, 생성자 호출 또는 식에 의해 초기화됩니다. 이러한 변수는 모두 모듈 초기화 중에 코드를 실행해야 합니다.

아래 코드에서는 함수 호출, 개체 생성 및 포인터 초기화 같이 동적 초기화가 필요한 정적 이니셜라이저의 예를 보여 줍니다. (이러한 예제는 정적이지는 않지만 전역 범위에 동일한 효과가 있는 정의가 있는 것으로 간주됩니다.)

// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);
CObject* op = new CObject(arg1, arg2);

이 교착 상태의 위험은 포함된 모듈이 컴파일되는지 /clr 여부와 MSIL이 실행될지 여부에 따라 달라집니다. 특히 정적 변수가 없이 /clr 컴파일되거나 블록에 #pragma unmanaged 있는 경우 이를 초기화하는 데 필요한 동적 이니셜라이저로 인해 MSIL 명령이 실행되면 교착 상태가 발생할 수 있습니다. DllMain에서 정적 변수의 초기화를 수행하지 않고 /clr컴파일된 모듈의 경우이기 때문입니다. 반면에 컴파일된 /clr 정적 변수는 관리되지 않는 초기화 단계가 완료되고 로더 잠금이 해제된 후에 초기화 .cctor됩니다.

정적 변수의 동적 초기화로 인해 교착 상태에 대한 여러 솔루션이 있습니다. 문제를 해결하는 데 필요한 시간 순서로 대략 여기에 정렬됩니다.

  • 정적 변수를 포함하는 소스 파일은 .을 /clr사용하여 컴파일할 수 있습니다.

  • 정적 변수에 의해 호출된 모든 함수는 지시문을 사용하여 네이티브 코드로 #pragma unmanaged 컴파일할 수 있습니다.

  • 정적 변수가 의존하는 코드를 수동으로 복제하여 .NET 버전과 네이티브 버전에 서로 다른 이름을 지정합니다. 그런 다음 개발자는 네이티브 정적 이니셜라이저에서 네이티브 버전을 호출하고 .NET 버전은 다른 위치에서 호출할 수 있습니다.

시작에 영향을 주는 사용자 제공 함수

시작 시 초기화를 위하여 라이브러리가 의존하는 사용자 제공 함수에는 여러 가지 있습니다. 예를 들어 C++에서 연산자 및 delete 연산자와 같은 new 연산자를 전역적으로 오버로드하는 경우 C++ 표준 라이브러리 초기화 및 소멸을 포함하여 사용자가 제공한 버전이 모든 곳에서 사용됩니다. 따라서 C++ 표준 라이브러리 및 사용자가 제공한 정적 이니셜라이저는 사용자가 제공한 모든 버전의 이러한 연산자를 호출합니다.

사용자 제공 버전을 MSIL로 컴파일하는 경우 이러한 이니셜라이저는 로더 잠금 상태에서도 MSIL 명령을 실행하려 합니다. 사용자가 제공한 malloc 결과는 동일합니다. 이 문제를 해결하려면 이러한 오버로드 또는 사용자 제공 정의를 지시문을 사용하여 #pragma unmanaged 네이티브 코드로 구현해야 합니다.

이 시나리오에 대한 자세한 내용은 진단 장애를 참조 하세요.

사용자 지정 로캘

사용자가 사용자 지정 전역 로캘을 제공하는 경우 이 로캘은 정적으로 초기화된 스트림을 포함하여 향후 모든 I/O 스트림을 초기화하는 데 사용됩니다. 이 전역 로캘 개체를 MSIL로 컴파일하는 경우 MSIL로 컴파일된 로캘 개체 멤버 함수가 로더 잠금 상태에서 호출될 수 있습니다.

이 문제를 해결하는 데는 세 가지 방법이 있습니다.

모든 전역 I/O 스트림 정의를 포함하는 원본 파일은 이 옵션을 사용하여 /clr 컴파일할 수 있습니다. 로더 잠금에서 정적 이니셜라이저가 실행되지 않도록 합니다.

사용자 지정 로캘 함수 정의는 지시문을 사용하여 #pragma unmanaged 네이티브 코드로 컴파일할 수 있습니다.

사용자 지정 로캘을 전역 로캘로 설정하는 것을 로더 잠금이 해제될 때까지 뒤로 미룹니다. 그런 다음 사용자 지정 로캘을 사용하여 초기화할 때 작성된 I/O 스트림을 명시적으로 구성합니다.

진단 장애 요소

경우에 따라 교착 상태의 원인을 감지하기가 어렵습니다. 다음 하위 섹션에서는 이와 관련된 시나리오 및 이러한 문제를 해결하는 방법에 대해 설명합니다.

헤더의 구현

특수한 몇 가지 경우 헤더 파일 내의 함수 구현으로 인해 진단이 어려워질 수 있습니다. 인라인 함수와 템플릿 코드는 모두 헤더 파일에 해당 함수를 지정해야 합니다. C++ 언어는 이름이 동일한 모든 함수 구현을 의미론적으로 동일하게 파악하는 단일 정의 규칙을 사용합니다. 따라서 C++ 링커에서는 특정 함수의 구현이 중복된 개체 파일을 병합할 때 별다른 사항을 고려할 필요가 없습니다.

Visual Studio 2005 이전의 Visual Studio 버전에서 링커는 의미상 동등한 정의 중 가장 큰 정의를 선택하기만 하면 됩니다. 이 작업은 전달 선언을 수용하기 위해 수행되며, 다른 원본 파일에 대해 다른 최적화 옵션이 사용되는 시나리오를 수용합니다. 혼합 네이티브 및 .NET DLL에 대한 문제를 만듭니다.

사용 및 사용 안 함으로 설정된 C++ 파일 /clr 에서 동일한 헤더를 포함하거나 블록 내부에 #pragma unmanaged #include 래핑할 수 있으므로 헤더에 구현을 제공하는 MSIL 및 네이티브 버전의 함수를 모두 포함할 수 있습니다. MSIL 및 네이티브 구현에는 로더 잠금 아래의 초기화에 대한 의미 체계가 다르며 이는 하나의 정의 규칙을 효과적으로 위반합니다. 따라서 링커가 가장 큰 구현을 선택하면 지시문을 사용하여 #pragma unmanaged 다른 곳에서 네이티브 코드로 명시적으로 컴파일된 경우에도 함수의 MSIL 버전을 선택할 수 있습니다. 템플릿 또는 인라인 함수의 MSIL 버전이 로더 잠금에서 호출되지 않도록 하려면 로더 잠금에서 호출된 모든 함수의 모든 정의를 지시문으로 #pragma unmanaged 수정해야 합니다. 헤더 파일이 타사에서 온 경우 이 변경을 수행하는 가장 쉬운 방법은 잘못된 헤더 파일에 대한 #include 지시문 주위에 지시문을 푸시하고 팝 #pragma unmanaged 하는 것입니다. 예를 들어 관리형, 관리되지 않는 예제를 참조하세요. 그러나 .NET API를 직접 호출해야 하는 다른 코드가 포함된 헤더에는 이 전략이 작동하지 않습니다.

로더 잠금 문제를 해결하려는 사용자의 편의를 위해 네이티브 구현과 관리되는 구현이 둘 다 있으면 링커에서 네이티브 구현을 선택합니다. 이 기본값은 위의 문제를 방지합니다. 그러나 컴파일러에서 해결되지 않은 두 가지 문제로 인해 이 릴리스에서 이 규칙에는 두 가지 예외가 있습니다.

  • 인라인 함수에 대한 호출은 전역 정적 함수 포인터를 통해서입니다. 가상 함수가 전역 함수 포인터를 통해 호출되기 때문에 이 시나리오는 테이블이 아닙니다. 예를 들면 다음과 같습니다.
#include "definesmyObject.h"
#include "definesclassC.h"

typedef void (*function_pointer_t)();

function_pointer_t myObject_p = &myObject;

#pragma unmanaged
void DuringLoaderlock(C & c)
{
    // Either of these calls could resolve to a managed implementation,
    // at link-time, even if a native implementation also exists.
    c.VirtualMember();
    myObject_p();
}

디버그 모드에서 진단

로드 잠금 문제에 대한 진단은 모두 디버그 빌드에서 수행해야 합니다. 릴리스 빌드는 진단 생성하지 않을 수 있습니다. 또한 릴리스 모드에서 수행된 최적화는 로더 잠금 시나리오에서 일부 MSIL을 마스킹할 수 있습니다.

로더 잠금 문제를 디버그하는 방법

MSIL 함수를 호출할 때 CLR에서 진단이 생성되면 CLR 실행이 일시 중단됩니다. 그러면 디버기 in-process를 실행할 때도 Visual C++ 혼합 모드 디버거가 일시 중단됩니다. 그러나 프로세스에 연결할 때 혼합 디버거를 사용하여 디버그기에 대한 관리형 호출 스택을 가져올 수는 없습니다.

로더 잠금 상황에서 호출된 특정 MSIL 함수를 식별하려면 다음 단계를 수행해야 합니다.

  1. mscoree.dll 및 mscorwks.dll에 대한 기호를 사용할 수 있는지 확인합니다.

    기호를 두 가지 방법으로 사용할 수 있도록 할 수 있습니다. 첫 번째 방법으로 mscoree.dll 및 mscorwks.dll에 대한 PDB를 기호 검색 경로에 추가할 수 있습니다. 추가하려면 기호 검색 경로 옵션 대화 상자를 엽니다. (에서 도구 메뉴에서 옵션을 선택합니다. 옵션 대화 상자의 왼쪽 창에서 디버깅 노드를 열고 기호를 선택합니다.) mscoree.dll 및 mscorwks.dll PDB 파일의 경로를 검색 목록에 추가합니다. 이러한 PDB는 %VSINSTALLDIR%\SDK\v2.0\symbols에 설치됩니다. 확인을 선택합니다.

    두 번째 방법으로 mscoree.dll 및 mscorwks.dll에 대한 PDB를 Microsoft 기호 서버에서 다운로드할 수 있습니다. 기호 서버를 구성하려면 기호 검색 경로 옵션 대화 상자를 엽니다. (에서 도구 메뉴에서 옵션을 선택합니다. 옵션 대화 상자의 왼쪽 창에서 디버깅 노드를 열고 기호를 선택합니다.) 검색 목록에 다음 검색 경로를 추가합니다https://msdl.microsoft.com/download/symbols. 기호 서버 캐시 텍스트 상자에 기호 캐시 디렉터리를 추가한 다음 확인을 선택합니다.

  2. 디버거 모드를 네이티브 전용 모드로 설정합니다.

    솔루션에서 시작 프로젝트의 속성 표를 엽니다. 구성 속성>디버깅을 선택합니다. 디버거 형식 속성을 네이티브 전용으로 설정합니다.

  3. 디버거(F5)를 시작합니다.

  4. 진단이 /clr 생성되면 다시 시도를 선택한 다음 중단을 선택합니다.

  5. 호출 스택 창을 엽니다. (메뉴 모음 에서Windows 호출 스택을 디버그>합니다>.) 잘못된 DllMain 또는 정적 이니셜라이저는 녹색 화살표로 식별됩니다. 잘못된 함수가 식별되지 않은 경우 다음 단계를 수행하여 찾아야 합니다.

  6. 직접 실행 창을 엽니다(메뉴 모음에서 Windows> 직접 실행 디버그>선택).

  7. 직접 실행 창에 입력 .load sos.dll 하여 SOS 디버깅 서비스를 로드합니다.

  8. 직접 실행 창에 입력 !dumpstack 하여 내부 /clr 스택의 전체 목록을 가져옵니다.

  9. _CorDllMain(문제가 발생하는 경우 DllMain ) 또는 _VTableBootstrapThunkInitHelperStub 또는 GetTargetForVTableEntry(정적 이니셜라이저로 인해 문제가 발생하는 경우)의 첫 번째 인스턴스(스택 맨 아래에 가장 가까운)를 찾습니다. 이 호출 바로 아래에 있는 스택 항목이 로더 잠금 상태에서 실행하려 했던 MSIL 구현 함수의 호출입니다.

  10. 이전 단계에서 식별된 원본 파일 및 줄 번호로 이동하여 시나리오 섹션에 설명된 시나리오 및 솔루션을 사용하여 문제를 해결합니다.

예제

설명

다음 샘플에서는 전역 개체의 생성자로 코드를 DllMain 이동하여 로더 잠금을 방지하는 방법을 보여줍니다.

이 샘플에는 생성자에 원래 있던 관리되는 개체가 포함된 전역 관리 개체가 있습니다 DllMain. 이 샘플의 두 번째 부분에서는 어셈블리를 참조하여 초기화를 수행하는 모듈 생성자를 호출하는 관리되는 개체의 인스턴스를 만듭니다.

코드

// initializing_mixed_assemblies.cpp
// compile with: /clr /LD
#pragma once
#include <stdio.h>
#include <windows.h>
struct __declspec(dllexport) A {
   A() {
      System::Console::WriteLine("Module ctor initializing based on global instance of class.\n");
   }

   void Test() {
      printf_s("Test called so linker doesn't throw away unused object.\n");
   }
};

#pragma unmanaged
// Global instance of object
A obj;

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
   // Remove all managed code from here and put it in constructor of A.
   return true;
}

이 예제에서는 혼합 어셈블리를 초기화하는 데 발생하는 문제를 보여 줍니다.

// initializing_mixed_assemblies_2.cpp
// compile with: /clr initializing_mixed_assemblies.lib
#include <windows.h>
using namespace System;
#include <stdio.h>
#using "initializing_mixed_assemblies.dll"
struct __declspec(dllimport) A {
   void Test();
};

int main() {
   A obj;
   obj.Test();
}

이 코드는 다음 출력을 생성합니다.

Module ctor initializing based on global instance of class.

Test called so linker doesn't throw away unused object.

참고 항목

혼합형(네이티브 및 관리) 어셈블리