혼합형 어셈블리 초기화
Visual C++ .NET 및 Visual C++ 2003에서 /clr 컴파일러 옵션을 사용하여 컴파일한 DLL을 로드할 때 명확하지 않은 교착 상태에 빠질 수 있습니다. 이 문제를 혼합 DLL 로드 또는 로더 잠금 문제라고 합니다. 혼합 DLL 로드 프로세스의 불명확한 요소가 대부분 제거되었습니다. 그러나 일부 시나리오에서는 명확한 로더 잠금 문제가 발생할 수 있습니다. 이러한 문제에 대한 자세한 내용은 MSDN Library "Mixed DLL Loading Problem"을 참조하십시오.
DllMain 내의 코드에서 CLR에 액세스하면 안 됩니다. 즉, DllMain은 관리되는 함수를 직접적으로든 간접적으로든 호출하지 말아야 하고, DllMain에서 관리 코드를 선언하거나 구현하지 말아야 하고, DllMain 내에서 가비지 수집이나 자동 라이브러리 로드를 수행하지 말아야 합니다.
참고
Visual C++ 2003에서는 교착 상태가 발생할 수 있는 가능성을 최소화하고 DLL을 쉽게 초기화할 수 있도록 _vcclrit.h를 제공합니다. 더 이상 _vcclrit.h를 사용할 필요가 없습니다. 이를 사용하면 컴파일 과정에서 이 파일이 사용되지 않는다는 경고 메시지가 나타납니다. 가장 좋은 방법은 How To: Remove Dependency on _vcclrit.h에서 설명하는 단계에 따라 이 파일에 대한 종속성을 제거하는 것입니다. 또는 _vcclrit.h를 포함하기 전에 _CRT_VCCLRIT_NO_DEPRECATE를 정의하여 경고 메시지가 표시되지 않도록 할 수도 있고 이 경고 메시지를 단순히 무시할 수도 있습니다.
로더 잠금 원인
.NET 플랫폼이 개발되면서 실행 모듈(EXE 또는 DLL)을 로드하는 데 서로 다른 두 가지 메커니즘이 사용되기 시작했습니다. 그 중 하나는 Windows용으로 관리되는 모듈에 사용되고, 다른 하나는 .NET 어셈블리를 로드하는 .NET CLR(공용 언어 런타임)에 사용됩니다. 혼합 DLL 로드 문제는 Microsoft Windows OS 로더에 주로 관련된 것입니다.
.NET 구조체만 포함된 어셈블리를 프로세스에 로드하는 경우에는 필요한 로드 및 초기화 작업을 CLR 로더에서 모두 직접 수행할 수 있습니다. 그러나 혼합형 어셈블리의 경우 네이티브 코드와 데이터가 포함될 수 있으므로 Windows 로더도 함께 사용해야 합니다.
Windows 로더를 사용하면 DLL을 완전히 초기화하기 전에 어떠한 코드에서도 이 DLL의 코드나 데이터에 액세스할 수 없고 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을 통해 먼저 진행되고, 관리되는 초기화는 .cctor이라는 .NET 지원 구조체를 통해 나중에 진행됩니다. /Zl 또는 /NODEFAULTLIB를 사용하지 않으면 두 번째 단계가 사용자에게 완전히 노출됩니다. 자세한 내용은 /NODEFAULTLIB(라이브러리 무시) 및 /Zl(기본 라이브러리 이름 생략)을 참조하십시오.
로더 잠금이 여전히 발생할 수 있지만 이제 잠금이 일정하게 발생하며 이를 감지할 수 있습니다. DllMain에 MSIL 명령이 포함되어 있으면 컴파일러는 컴파일러 경고(수준 1) C4747 경고를 생성합니다. 또한 CRT 또는 CLR에서는 로더 잠금 상황에서 MSIL을 실행하려는 시도를 감지 및 보고합니다. CRT에서 이러한 상황이 감지되면 런타임 진단 C 런타임 오류 R6033이 발생합니다.
이 문서의 나머지 부분에서는 로더 잠금 상태에서 MSIL을 실행할 수 있는 다른 시나리오와 이러한 각 시나리오에서 발생하는 문제에 대한 해결책 및 디버깅 기술에 대해 설명합니다.
시나리오 및 해결 방법
로더 잠금 상태에서 사용자 코드가 MSIL을 실행할 수 있는 상황에는 여러 가지가 있습니다. 개발자는 이러한 상황에서 사용자 코드 구현이 MSIL 명령을 실행하지 않도록 주의해야 합니다. 다음 각 하위 단원에서는 가능한 모든 상황과 가장 일반적인 경우에 발생하는 문제를 해결하는 방법에 대해 설명합니다.
DllMain
정적 이니셜라이저
시작에 영향을 주는 사용자 제공 함수
사용자 지정 로캘
DllMain
DllMain 함수는 DLL에 대한 사용자 정의된 진입점입니다. 사용자가 별도로 지정하지 않으면 프로세스나 스레드를 포함 DLL에 연결하거나 해당 DLL에서 분리할 때마다 DllMain이 호출됩니다. 이러한 호출은 로더 잠금 상태에서 발생할 수 있으므로 사용자가 제공한 DllMain 함수를 MSI로 컴파일하지 말아야 합니다. 또한 DllMain에 뿌리를 둔 호출 트리의 어떠한 함수도 MSIL로 컴파일할 수 없습니다. 이 문제를 해결하려면 DllMain이 정의된 코드 블록을 #pragma unmanaged를 사용하여 한정해야 합니다. DllMain이 호출하는 모든 함수에 대해서도 동일한 작업을 수행해야 합니다.
다른 호출 컨텍스트에서 이러한 함수가 MSIL 구현이 필요한 함수를 호출해야 하는 경우 복제 전략을 사용하여 동일한 함수의 .NET 버전과 네이티브 버전을 모두 만들 수 있습니다.
또는 DllMain이 반드시 필요하지 않거나 로더 잠금 상태에서 실행할 필요가 없는 경우 사용자가 제공한 DllMain 구현을 제거하여 문제를 해결할 수 있습니다.
DllMain에서 MSIL을 직접 실행하려고 하면 컴파일러 경고(수준 1) C4747이 발생합니다. 그러나 MSIL을 실행하려는 다른 모듈의 함수를 DllMain이 호출하는 경우에는 컴파일러에서 이를 감지할 수 없습니다.
이 시나리오에 대한 자세한 내용은 "진단 장애 요소"를 참조하십시오.
정적 개체 초기화
동적 이니셜라이저가 필요한 경우 정적 개체를 초기화하면 교착 상태가 발생할 수 있습니다. 컴파일 타임에 알려진 값으로 정적 변수를 할당하는 경우와 같은 간단한 상황에서는 동적 초기화가 필요하지 않으므로 교착 상태의 위험이 없습니다. 그러나 컴파일 타임에 확인할 수 없는 식, 생성자 호출 또는 함수 호출을 통해 정적 변수를 초기화하는 경우에는 모듈 초기화 과정에서 코드를 실행해야 합니다.
아래 코드에서는 함수 호출, 개체 생성 및 포인터 초기화 같이 동적 초기화가 필요한 정적 이니셜라이저의 예를 보여 줍니다. 이러한 예제는 정적이 아니지만 전역 범위에 정의해야 하는 것으로 간주되므로 결과는 동일합니다.
// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);
CObject* op = new CObject(arg1, arg2);
교착 상태가 발생할 가능성은 포함하는 모듈이 /clr로 컴파일되는지 여부와 MSIL이 실행되는지 여부에 따라 달라집니다. 특히 정적 변수가 /clr를 사용하지 않고 컴파일되거나 #pragma unmanaged 블록 안에 있는 경우 정적 변수를 초기화하는 데 필요한 동적 이니셜라이저에서 MSIL 명령을 실행하게 되므로 교착 상태가 발생할 수 있습니다. 그 이유는 /clr를 사용하지 않고 컴파일된 모듈의 경우 DllMain에서 정적 변수를 초기화하기 때문입니다. 반면에 /clr를 사용하여 컴파일된 정적 변수는 관리되지 않는 초기화 단계가 완료되고 로더 잠금이 해제된 후에 .cctor에서 초기화됩니다.
정적 변수의 동적 초기화로 인해 발생하는 교착 상태를 해결하는 데는 여러 가지 방법이 있으며, 아래에는 문제를 해결하는 데 걸리는 대략적인 시간 순서에 따라 각 방법이 나열되어 있습니다.
정적 변수가 포함된 소스 파일을 /clr를 사용하여 컴파일할 수 있습니다.
정적 변수를 통해 호출되는 모든 함수를 #pragma unmanaged 지시문을 사용하여 네이티브 코드로 컴파일할 수 있습니다.
정적 변수가 의존하는 코드를 수동으로 복제하여 .NET 버전과 네이티브 버전에 서로 다른 이름을 지정합니다. 그런 다음 개발자는 네이티브 정적 이니셜라이저에서 네이티브 버전을 호출하고 .NET 버전은 다른 위치에서 호출할 수 있습니다.
시작에 영향을 주는 사용자 제공 함수
시작 시 초기화를 위하여 라이브러리가 의존하는 사용자 제공 함수에는 여러 가지 있습니다. 예를 들어, new 및 delete 연산자 같은 C++의 전역 오버로드 연산자의 경우 사용자 제공 버전은 STL 초기화 및 소멸을 비롯한 모든 경우에 사용됩니다. 따라서, STL 및 사용자 제공 정적 이니셜라이저는 이러한 연산자의 사용자 제공 버전을 호출합니다.
사용자 제공 버전을 MSIL로 컴파일하는 경우 이러한 이니셜라이저는 로더 잠금 상태에서도 MSIL 명령을 실행하려 합니다. 사용자 제공 malloc의 경우에도 동일한 결과를 낳습니다. 이 문제를 해결하려면 이러한 오버로드나 사용자 제공 정의를 모두 #pragma unmanaged 지시문을 사용하여 네이티브 코드로 구현해야 합니다.
이 시나리오에 대한 자세한 내용은 "진단 장애 요소"를 참조하십시오.
사용자 지정 로캘
사용자가 사용자 지정 전역 로캘을 제공하는 경우 이 로캘은 정적으로 초기화된 스트림을 비롯하여 이후의 모든 I/O 스트림을 초기화하는 데 사용됩니다. 이 전역 로캘 개체를 MSIL로 컴파일하는 경우 MSIL로 컴파일된 로캘 개체 멤버 함수가 로더 잠금 상태에서 호출될 수 있습니다.
이 문제를 해결하는 데는 세 가지 방법이 있습니다.
모든 전역 I/O 스트림 정의가 포함된 소스 파일을 /clr 옵션을 사용하여 컴파일할 수 있습니다. 이렇게 하면 해당 정적 이니셜라이저가 로더 잠금 상태에서 실행되는 것을 막을 수 있습니다.
사용자 지정 로캘 함수 정의를 #pragma unmanaged 지시문을 사용하여 네이티브 코드로 컴파일할 수 있습니다.
사용자 지정 로캘을 전역 로캘로 설정하는 것을 로더 잠금이 해제될 때까지 뒤로 미룹니다. 그런 다음 사용자 지정 로캘을 사용하여 초기화할 때 작성된 I/O 스트림을 명시적으로 구성합니다.
진단 장애 요소
경우에 따라서는 교착 상태의 원인을 파악하기가 어렵습니다. 다음 하위 단원에서는 이와 관련된 시나리오 및 이러한 문제를 해결하는 방법에 대해 설명합니다.
헤더의 구현
특수한 몇 가지 경우 헤더 파일 내의 함수 구현으로 인해 진단이 어려워질 수 있습니다. 인라인 함수와 템플릿 코드는 모두 헤더 파일에 해당 함수를 지정해야 합니다. C++ 언어는 이름이 동일한 모든 함수 구현을 의미론적으로 동일하게 파악하는 단일 정의 규칙을 사용합니다. 따라서 C++ 링커에서는 특정 함수의 구현이 중복된 개체 파일을 병합할 때 별다른 사항을 고려할 필요가 없습니다.
Visual C++ .NET 및 Visual C++ .NET 2003의 경우 링커에서는 전방 선언 및 여러 소스 파일에 서로 다른 최적화 옵션이 사용된 시나리오에 대비하기 위해 의미가 동일한 이러한 정의 중 단순히 가장 큰 정의를 선택합니다. 이 경우 혼합 네이티브/.NET DLL에서 문제가 발생합니다.
동일한 헤더가 /clr를 사용하는 CPP 파일과 사용하지 않는 CPP 파일에 모두 포함되거나 #include가 #pragma unmanaged 블록 내에 래핑될 수 있으므로 헤더에서 구현을 제공하는 함수의 MSIL 버전과 네이티브 버전이 모두 존재하게 될 수 있습니다. MSIL과 네이티브 구현은 로드 잠금 상태에서의 초기화라는 측면에서 볼 때 의미론이 서로 다르므로 결과적으로 단일 정의 규칙을 위반하게 됩니다. 따라서 링커가 가장 큰 구현을 선택할 때 MSIL 버전의 함수를 선택할 수 있지만 실제로 이는 다른 곳에서 #pragma unmanaged 지시문을 사용하여 네이티브 코드로 명시적으로 컴파일되었을 수 있습니다. 로더 잠금 상태에서 MSIL 버전의 템플릿이나 인라인 함수가 호출되지 않게 하려면 로더 잠금 상태에서 호출되는 이러한 모든 함수의 정의를 모두 unmanaged 지시문으로 한정해야 합니다. 타사 헤더 파일을 사용하는 경우 이를 수행하는 가장 쉬운 방법은 문제가 되는 헤더 파일에 대한 #include 지시문 주위에 #pragma unmanaged 지시문을 배치하는 것입니다. 자세한 예제는 managed, 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();
}
- Itanium 대상 컴파일의 경우 모든 함수 포인터의 구현에 버그가 있습니다. 위 코드 조각에서 myObject_p를 during_loaderlock() 내에 로컬로 정의하면 호출 대상이 관리되는 구현으로도 확인됩니다.
디버그 모드에서 진단
로드 잠금 문제에 대한 진단은 모두 디버그 빌드에서 수행해야 합니다. 릴리스 빌드에서는 진단 결과가 생성되지 않을 수 있고 릴리스 모드에서 수행한 최적화로 인해 로더 잠금 시나리오에서 일부 MSIL이 마스크될 수 있습니다.
로더 잠금 문제 디버깅 방법
MSIL 함수를 호출할 때 CLR에서 진단이 생성되면 CLR 실행이 일시 중단됩니다. 한편 디버기(debuggee) 프로세스를 실행 중인 경우 Visual C++ 혼합 모드 디버거가 일시 중단됩니다. 그러나 프로세스에 연결하려 하면 혼합 디버거를 사용하여 디버기에 대한 관리되는 호출 스택을 얻을 수 없습니다.
로더 잠금 상황에서 호출된 특정 MSIL 함수를 식별하려면 다음 단계를 수행해야 합니다.
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 기호 서버에서 다운로드할 수 있습니다. 기호 서버를 구성하려면 기호 검색 경로 옵션 대화 상자를 엽니다. (도구 메뉴에서 옵션을 클릭합니다. 옵션 대화 상자의 왼쪽 창에서 디버깅 노드를 열고 기호를 클릭합니다.) 검색 목록에 http://msdl.microsoft.com/download/symbols 검색 경로를 추가합니다. 기호 서버 캐시 텍스트 상자에 기호 캐시 디렉터리를 추가한 다음 확인을 클릭합니다.
디버거 모드를 네이티브 전용 모드로 설정합니다.
이렇게 하려면 솔루션의 시작 프로젝트에 대한 속성 표를 열어야 합니다. 구성 속성 하위 트리 아래에서 디버깅 노드를 선택합니다. 디버거 형식 필드를 네이티브 전용으로 설정합니다.
F5 키를 눌러 디버거를 시작합니다.
/clr 진단이 생성되면 다시 시도를 클릭하고 중단을 클릭합니다.
호출 스택 창을 엽니다. (디버그 메뉴에서 창을 클릭한 다음 호출 스택을 클릭합니다.) 위반하는 DllMain이나 정적 이니셜라이저는 녹색 화살표로 표시됩니다. 위반하는 함수를 식별할 수 없으면 다음 단계를 수행하여 이를 찾아야 합니다.
디버그 메뉴에서 창을 클릭한 다음 직접 실행을 클릭하여 직접 실행 창을 엽니다.
직접 실행 창에 .load sos.dll을 입력하여 SOS 디버깅 서비스를 로드합니다.
직접 실행 창에 !dumpstack을 입력하여 내부 /clr 스택의 전체 목록을 가져옵니다.
스택의 맨 아래쪽에서 _CorDllMain(DllMain에서 문제가 발생하는 경우), _VTableBootstrapThunkInitHelperStub 또는 GetTargetForVTableEntry(정적 이니셜라이저에서 문제가 발생하는 경우)가 처음 나오는 부분을 찾습니다. 이 호출 바로 아래에 있는 스택 엔트리가 로더 잠금 상태에서 실행하려 했던 MSIL 구현 함수의 호출입니다.
9단계에서 확인한 줄 번호를 소스 파일에서 찾은 다음 시나리오 단원에서 설명한 시나리오와 해결책을 사용하여 문제를 수정합니다.
예제
설명
다음 샘플에서는 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 does not 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();
}
Output
Module ctor initializing based on global instance of class.
Test called so linker does not throw away unused object.