Share via


동적 연결 라이브러리 모범 사례

**업데이트:**

  • 2006년 5월 17일

중요 API

DLL을 만들면 개발자에게 여러 가지 문제가 발생합니다. DLL에는 시스템 적용 버전 관리가 없습니다. 시스템에 여러 버전의 DLL이 있는 경우 버전 관리 스키마가 부족하여 쉽게 덮어쓸 수 있으므로 종속성 및 API 충돌이 발생합니다. 개발 환경의 복잡성, 로더 구현 및 DLL 종속성으로 인해 부하 순서 및 애플리케이션 동작의 취약성이 발생했습니다. 마지막으로, 많은 애플리케이션이 DLL을 사용하며 애플리케이션이 제대로 작동하려면 반드시 적용해야 하는 복잡한 종속성 집합이 있습니다. 이 문서에서는 DLL 개발자가 보다 강력하고 이식 가능하며 확장 가능한 DLL을 빌드하는 데 도움이 되는 지침을 제공합니다.

DllMain 내에서 부적절한 동기화로 인해 애플리케이션이 교착 상태에 빠지거나 초기화되지 않은 DLL의 데이터 또는 코드에 액세스할 수 있습니다. DllMain 내에서 특정 함수를 호출하면 이러한 문제가 발생합니다.

what happens when a library is loaded

일반 모범 사례

로더 잠금이 유지되는 동안 DllMain 이 호출됩니다. 따라서 DllMain 내에서 호출할 수 있는 함수에 상당한 제한이 적용됩니다. 따라서 DllMain 은 Microsoft® Windows® API의 작은 하위 집합을 사용하여 최소한의 초기화 작업을 수행하도록 설계되었습니다. 직접 또는 간접적으로 로더 잠금을 획득하려고 시도하는 함수는 DllMain에서 호출할 수 없습니다. 그렇지 않으면 애플리케이션이 교착 상태 또는 충돌할 가능성이 있습니다. DllMain 구현의 오류로 인해 전체 프로세스와 모든 스레드가 위태로워질 수 있습니다.

이상적인 DllMain 은 빈 스텁일 뿐입니다. 그러나 많은 애플리케이션의 복잡성을 감안할 때 일반적으로 너무 제한적입니다. DllMain에 대한 엄지 손가락의 좋은 규칙은 가능한 한 많은 초기화를 연기하는 것입니다. 지연 초기화는 로더 잠금이 유지되는 동안 이 초기화가 수행되지 않으므로 애플리케이션의 견고성을 높입니다. 또한 지연 초기화를 사용하면 훨씬 더 많은 Windows API를 안전하게 사용할 수 있습니다.

일부 초기화 작업은 연기할 수 없습니다. 예를 들어 구성 파일에 의존하는 DLL은 파일 형식이 잘못되었거나 가비지를 포함하는 경우 로드하지 못합니다. 이러한 유형의 초기화의 경우 DLL은 작업을 시도하고 다른 작업을 완료하여 리소스를 낭비하지 않고 신속하게 실패해야 합니다.

DllMain 내에서 다음 작업을 수행해서는 안 됩니다.

  • LoadLibrary 또는 LoadLibraryEx를 호출합니다(직접 또는 간접적으로). 이로 인해 교착 상태 또는 충돌이 발생할 수 있습니다.
  • GetStringTypeA, GetStringTypeEx 또는 GetStringTypeW를 직접 또는 간접적으로 호출합니다. 이로 인해 교착 상태 또는 충돌이 발생할 수 있습니다.
  • 다른 스레드와 동기화합니다. 이로 인해 교착 상태가 발생할 수 있습니다.
  • 로더 잠금을 획득하기 위해 대기 중인 코드가 소유한 동기화 개체를 가져옵니다. 이로 인해 교착 상태가 발생할 수 있습니다.
  • CoInitializeEx를 사용하여 COM 스레드를 초기화합니다. 특정 조건에서 이 함수는 LoadLibraryEx를 호출할 수 있습니다.
  • 레지스트리 함수를 호출합니다.
  • CreateProcess를 호출 합니다. 프로세스를 만들면 다른 DLL을 로드할 수 있습니다.
  • ExitThread를 호출합니다. DLL 분리 중에 스레드를 종료하면 로더 잠금이 다시 획득되어 교착 상태 또는 충돌이 발생할 수 있습니다.
  • CreateThread를 호출합니다. 다른 스레드와 동기화하지 않으면 스레드를 만들 수 있지만 위험합니다.
  • ShGetFolderPathW를 호출합니다. 셸/알려진 폴더 API를 호출하면 스레드 동기화가 발생할 수 있으므로 교착 상태가 발생할 수 있습니다.
  • 명명된 파이프 또는 다른 명명된 개체를 만듭니다(Windows 2000에만 해당). Windows 2000에서는 터미널 서비스 DLL에서 명명된 개체를 제공합니다. 이 DLL이 초기화되지 않은 경우 DLL을 호출하면 프로세스가 충돌할 수 있습니다.
  • 동적 CRT(C 런타임)의 메모리 관리 함수를 사용합니다. CRT DLL이 초기화되지 않은 경우 이러한 함수를 호출하면 프로세스가 충돌할 수 있습니다.
  • User32.dll 또는 Gdi32.dll에서 함수를 호출합니다. 일부 함수는 초기화되지 않을 수 있는 다른 DLL을 로드합니다.
  • 관리 코드를 사용합니다.

다음 작업은 DllMain 내에서 안전하게 수행할 수 있습니다.

  • 컴파일 시간에 정적 데이터 구조 및 멤버를 초기화합니다.
  • 동기화 개체를 만들고 초기화합니다.
  • 메모리 할당 및 동적 데이터 구조 초기화(위에 나열된 함수 방지)
  • TLS(스레드 로컬 스토리지)를 설정합니다.
  • 파일을 열고 읽고 씁니다.
  • Kernel32.dll에서 함수를 호출합니다(위에 나열된 함수 제외).
  • NULL에 대한 전역 포인터를 설정하여 동적 멤버의 초기화를 연기합니다. Microsoft Windows Vista™에서는 일회성 초기화 함수를 사용하여 코드 블록이 다중 스레드 환경에서 한 번만 실행되도록 할 수 있습니다.

잠금 순서 반전으로 인한 교착 상태

잠금과 같은 여러 동기화 개체를 사용하는 코드를 구현하는 경우 잠금 순서를 준수하는 것이 중요합니다. 한 번에 둘 이상의 잠금을 획득해야 하는 경우 잠금 계층 또는 잠금 순서라고 하는 명시적 우선 순위를 정의해야 합니다. 예를 들어 코드의 어딘가에 잠금 B를 가져오기 전에 잠금 A를 획득하고 코드의 다른 위치에서 C를 잠그기 전에 잠금 B를 획득하는 경우 잠금 순서는 A, B, C이며 이 순서는 코드 전체에서 따라야 합니다. 잠금 순서 반전은 잠금 순서를 따르지 않을 때 발생합니다. 예를 들어 잠금 A 전에 잠금 B를 획득한 경우. 잠금 순서 반전으로 인해 디버그하기 어려운 교착 상태가 발생할 수 있습니다. 이러한 문제를 방지하려면 모든 스레드가 동일한 순서로 잠금을 획득해야 합니다.

로더 잠금은 이미 획득한 로더 잠금으로 DllMain을 호출하므로 로더 잠금은 잠금 계층에서 가장 높은 우선 순위를 가져야 합니다. 또한 코드는 적절한 동기화에 필요한 잠금만 획득해야 합니다. 계층 구조에 정의된 모든 잠금을 획득할 필요는 없습니다. 예를 들어 코드 섹션에 적절한 동기화를 위해 잠금 A 및 C만 필요한 경우 코드는 잠금 C를 획득하기 전에 잠금 A를 획득해야 합니다. 코드에서 잠금 B를 획득할 필요는 없습니다. 또한 DLL 코드는 로더 잠금을 명시적으로 획득할 수 없습니다. 코드가 간접적으로 로더 잠금을 획득할 수 있는 GetModuleFileName과 같은 API를 호출해야 하고 코드도 프라이빗 잠금을 획득해야 하는 경우 코드는 잠금 P를 획득하기 전에 GetModuleFileName을 호출하여 부하 순서를 준수해야 합니다.

그림 2는 잠금 순서 반전을 보여 주는 예제입니다. 기본 스레드에 DllMain이 포함된 DLL을 고려합니다. 라이브러리 로더는 로더 잠금 L을 획득한 다음 DllMain호출합니다. 기본 스레드는 동기화 개체 A, B 및 G를 만들어 데이터 구조에 대한 액세스를 직렬화한 다음 잠금 G를 획득하려고 시도합니다. 잠금 G를 이미 성공적으로 획득한 작업자 스레드는 로더 잠금 L을 획득하려고 시도하는 GetModuleHandle과 같은 함수를 호출합니다. 따라서 작업자 스레드가 L에서 차단되고 기본 스레드가 G에서 차단되어 교착 상태가 발생합니다.

deadlock caused by lock order inversion

잠금 순서 반전으로 인한 교착 상태를 방지하기 위해 모든 스레드는 항상 정의된 부하 순서로 동기화 개체를 획득하려고 시도해야 합니다.

동기화 모범 사례

초기화의 일부로 작업자 스레드를 만드는 DLL을 고려합니다. DLL 클린 작동 시 모든 작업자 스레드와 동기화하여 데이터 구조가 일관된 상태인지 확인하고 작업자 스레드를 종료해야 합니다. 현재 다중 스레드 환경에서 DLL을 클린 동기화하고 종료하는 문제를 완전히 해결할 수 있는 간단한 방법은 없습니다. 이 섹션에서는 DLL 종료 중 스레드 동기화에 대한 현재 모범 사례를 설명합니다.

프로세스 종료 중 DllMain스레드 동기화

  • 프로세스 종료 시 DllMain이 호출될 때까지 모든 프로세스의 스레드가 강제로 클린 주소 공간이 일치하지 않을 가능성이 있습니다. 이 경우에는 동기화가 필요하지 않습니다. 즉, 이상적인 DLL_PROCESS_DETACH 처리기는 비어 있습니다.
  • Windows Vista는 핵심 데이터 구조(환경 변수, 현재 디렉터리, 프로세스 힙 등)가 일관된 상태인지 확인합니다. 그러나 다른 데이터 구조는 손상될 수 있으므로 메모리를 클린 것은 안전하지 않습니다.
  • 저장해야 하는 영구 상태는 영구 스토리지로 플러시되어야 합니다.

DLL 언로드 중 DLL_THREAD_DETACH DllMain의 스레드 동기화

  • DLL이 언로드되면 주소 공간이 버려지지 않습니다. 따라서 DLL은 클린 종료를 수행해야 합니다. 여기에는 스레드 동기화, 열린 핸들, 영구 상태 및 할당된 리소스가 포함됩니다.
  • DllMain에서 스레드가 종료될 때까지 기다리면 교착 상태가 발생할 수 있으므로 스레드 동기화가 까다롭습니다. 예를 들어 DLL A는 로더 잠금을 보유합니다. 스레드 T가 종료되도록 신호를 표시하고 스레드가 종료되기를 기다립니다. 스레드 T가 종료되고 로더가 로더 잠금을 획득하여 DLL_THREAD_DETACH DLL A의 DllMain 을 호출하려고 합니다. 이로 인해 {b>‘교착 상태가 발생’
  • DLL A는 DllMain에서 DLL_THREAD_DETACH 메시지를 가져오고 스레드 T에 대한 이벤트를 설정하여 종료하라는 신호를 표시합니다.
  • 스레드 T는 현재 작업을 완료하고, 일관된 상태로 전환하고, DLL A에 신호를 표시하고, 무한히 기다립니다. 일관성 검사 루틴은 교착 상태를 방지하기 위해 DllMain동일한 제한을 따라야 합니다.
  • DLL A는 T가 일관된 상태임을 알고 T를 종료합니다.

모든 스레드를 만든 후 DLL이 언로드되지만 실행을 시작하기 전에 스레드가 충돌할 수 있습니다. DLL이 초기화의 일부로 DllMain에서 스레드를 만든 경우 일부 스레드가 초기화를 완료하지 않았을 수 있으며 해당 DLL_THREAD_ATTACH 메시지가 DLL에 배달되기를 기다리고 있습니다. 이 경우 DLL이 언로드되면 스레드 종료가 시작됩니다. 그러나 일부 스레드는 로더 잠금 뒤에서 차단될 수 있습니다. 해당 DLL_THREAD_ATTACH 메시지는 DLL의 매핑을 해제한 후 처리되어 프로세스가 중단됩니다.

권장 사항

권장 지침은 다음과 같습니다.

  • 애플리케이션 검증 도구를 사용하여 DllMain에서 가장 일반적인 오류를 catch합니다.
  • DllMain 내에서 프라이빗 잠금을 사용하는 경우 잠금 계층 구조를 정의하고 일관되게 사용합니다. 로더 잠금은 이 계층 구조의 맨 아래에 있어야 합니다.
  • 아직 완전히 로드되지 않았을 수 있는 다른 DLL에 따라 호출이 달라지지 않는지 확인합니다.
  • DllMain이 아닌 컴파일 시간에 정적으로 간단한 초기화를 수행합니다.
  • DllMain에서 나중에 대기할 수 있는 모든 호출을 연기합니다.
  • 나중에 대기할 수 있는 초기화 작업을 연기합니다. 애플리케이션에서 오류를 정상적으로 처리할 수 있도록 특정 오류 조건을 조기에 검색해야 합니다. 그러나 이 조기 발견과 그로 인해 발생할 수 있는 견고성의 손실 사이에는 절충이 있습니다. 초기화 지연이 가장 좋은 경우가 많습니다.