DllMain과 로더 락 ── 「DLL 초기화에서는 아무것도 하지 말라」는 말의 진짜 이유

· 업데이트: · · Windows, DLL, Windows 개발, C++, 불량 조사, 멀티스레드, Win32 API

수정 이력(1건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

한국어 전면 재작성에 맞춰 본문 표현을 바로잡았습니다. 기술적인 주장은 일본어판과 같습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22176713)

아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.

Go Komura (2026). 「DllMain과 로더 락 ── 「DLL 초기화에서는 아무것도 하지 말라」는 말의 진짜 이유」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/dllmain-loader-lock/

DOI(등록된 아카이브)
10.5281/zenodo.22176713
DOI(마지막 등록 버전)
10.5281/zenodo.22176714

「특정 환경에서만 앱 시작 시 hang이 난다」「직접 만든 DLL을 로드하면 LoadLibrary에서 돌아오지 않는 경우가 있다」「서비스 시작 타이밍에서만 데드락이 난다」── 이런 조사를 이어 가면, 꽤 높은 확률로 도착하는 곳이 있습니다. DLL의 초기화 코드, 즉 DllMain입니다.

Microsoft 문서는 DllMain에 대해 이례적일 만큼 강한 어조로 경고합니다. 말하자면 LoadLibrary를 호출하지 말라. 다른 스레드와 동기화하지 말라. User·Shell·COM 함수를 호출하지 말라. 이상적인 DllMain은 빈 스텁이다 ── 왜 이 정도로까지 말하는가. 그 이유는 하나의 내부 메커니즘, 로더 락(loader lock)으로 모입니다. 이 글에서는 Windows에서 DLL이나 플러그인, C++/CLI 래퍼를 작성하는 개발자를 대상으로, 로더 락의 구조와 데드락이 성립하는 구조, 그리고 안전한 설계와 조사 절차를 1차 정보에 근거해 설명합니다.

1. 먼저 결론

  • DllMain은 프로세스마다 하나뿐인 공유 락, 로더 락을 쥔 채로 호출됩니다. 로더 락을 (직접·간접으로) 잡으려는 처리를 DllMain에서 호출하면 데드락이나, 초기화 전 DLL을 건드리는 크래시가 생길 수 있습니다.1
  • LoadLibrary / FreeLibrary 호출은 금지입니다. 로드 순서 의존이 순환하고, 초기화 코드가 실행되기 전의 DLL이 쓰입니다.2
  • 다른 스레드와의 동기화도 금지입니다. DLL 알림은 직렬화되므로, DllMain 안에서 스레드 시작·종료를 기다리면 그 스레드가 로더 락을 기다리며 멈춰 데드락이 납니다.23
  • 안전하게 호출할 수 있는 것은 사실상 Kernel32.dll의 일부뿐입니다. 「안전한 함수의 완전한 목록은 존재하지 않는다」고 공식 문서가 말합니다. User·Shell·COM 함수는 다른 구성 요소를 로드해 액세스 위반의 원인이 됩니다.2
  • CRT와 링크한 DLL에서는 전역 변수의 생성자·소멸자에도 같은 제약이 걸립니다. 사실상 DllMain의 일부로 실행되기 때문입니다.2
  • 올바른 설계는 「지연」입니다. 가능한 초기화는 컴파일 시(정적)에, 나머지는 첫 사용 시로 미룹니다. 공식 베스트 프랙티스입니다.1
  • C++/CLI 혼합 DLL은 특히 위험합니다. 로더 락 아래 MSIL을 실행하지 않도록 DllMain과 호출 트리는 네이티브 컴파일이 필수입니다.4

2. DllMain은 언제, 어떻게 호출되는가

DllMain은 DLL이 프로세스나 스레드에 들어가거나 나올 때 OS 로더가 호출하는 진입점이며, 알림은 네 가지입니다.

알림 타이밍
DLL_PROCESS_ATTACH DLL이 프로세스에 로드되었을 때
DLL_THREAD_ATTACH 프로세스 안에서 새 스레드가 시작되었을 때
DLL_THREAD_DETACH 스레드가 정상 종료할 때
DLL_PROCESS_DETACH DLL 언로드 시, 또는 프로세스 종료 시

놓치기 쉬운 사실이 둘 있습니다. 첫째, 스레드가 하나 만들어질 때마다, 이미 로드된 모든 DLL의 DllMain이 DLL_THREAD_ATTACH로 호출된다는 점입니다. 즉 DllMain은 「내 DLL이 로드될 때 한 번만 도는 코드」가 아니라, 프로세스의 스레드 활동마다 계속 호출되는 코드입니다. 필요 없다면 DLL_PROCESS_ATTACH 안에서 DisableThreadLibraryCalls를 호출해 막을 수 있습니다(정적 CRT와 링크한 DLL에서는 호출하면 안 됩니다).5

둘째, CRT(C/C++ 런타임)와 링크한 DLL에서는 전역·정적 C++ 객체의 생성자와 소멸자가 CRT의 진입점을 거쳐 DllMain의 일부로 실행된다는 점입니다.2 「우리 DllMain은 비어 있으니 안전하다」고 생각해도, 복잡한 초기화를 하는 전역 객체가 있으면 그것은 DllMain에서 실행하는 것과 같습니다.

DllMain이 호출되는 네 가지 타이밍DLL 로드에서 DLL_PROCESS_ATTACH가, 프로세스 안 스레드 시작·종료마다 이미 로드된 모든 DLL에 DLL_THREAD_ATTACH와 DETACH가, 언로드나 프로세스 종료에서 DLL_PROCESS_DETACH가 호출되며, CRT를 거쳐 정적 객체의 생성자도 이 안에서 실행된다DLL 로드DLL_PROCESS_ATTACHDLL_THREAD_ATTACH(스레드 시작마다)DLL_THREAD_DETACH(스레드 종료마다)DLL_PROCESS_DETACH(언로드·종료 시)정적 객체의 생성도 여기서 실행

그림 1: DllMain은 로드 때뿐 아니라 스레드 시작·종료마다 호출되며, 정적 객체의 초기화도 그 일부로 실행됩니다.

3. 로더 락 ── 모든 알림을 직렬화하는 락 하나

왜 DllMain에만 이토록 심한 제약이 걸리는가. 답은 로더의 구조에 있습니다.

OS 로더는 DLL의 로드·언로드·각종 알림이라는 일련의 작업 정합성을 지키기 위해, 프로세스에 단 하나인 로더 락으로 처리를 직렬화합니다. 그리고 중요한 점은 DllMain이 이 로더 락을 쥔 채로 호출된다는 것입니다.1 DllMain 안에 있는 동안, 그 프로세스에서는 다른 DLL 로드도, 새 스레드의 시작 알림도, 모두 이 락이 풀리기를 기다립니다.

이 구조에서 금지 사항의 이유가 연쇄적으로 나옵니다.

  • LoadLibrary를 호출하면 안 되는 이유는 로더 락의 재진입이나 로드 순서의 순환 의존을 만들기 때문입니다. 초기화가 아직 끝나지 않은 DLL의 함수가 호출되는 사태도 일어날 수 있습니다.2
  • 다른 스레드와의 동기화가 위험한 이유는, 기다리는 상대 스레드가 로더 락이 필요한 순간(시작·종료 시의 알림, GetModuleHandle 계열 API 호출 등)이 있기 때문입니다. 이쪽은 로더 락을 쥐고 상대를 기다리고, 상대는 로더 락을 기다립니다 ── 고전적인 락 순서 역전입니다.6
  • User·Shell·COM 함수가 위험한 이유는 이들이 내부에서 다른 시스템 구성 요소를 로드하기 때문입니다. 초기화 전·해제 후의 구성 요소를 건드려 액세스 위반이 납니다.2
DllMain에서 스레드를 기다리면 데드락이 나는 구조로더 락을 쥔 DllMain이 워커 스레드의 종료를 기다리지만, 종료하려는 워커 스레드는 DLL_THREAD_DETACH 알림을 위해 로더 락이 풀리기를 기다려, 서로 기다리며 데드락이 된다워커 스레드DllMain로더(락 보유)워커 스레드DllMain로더(락 보유)종료 알림에 로더 락이 필요DllMain은 락을 쥐고 기다리고, W는 락을 기다림DLL_PROCESS_DETACH를 알림종료를 요청하고 완료를 기다림처리를 마치고 스레드 종료로

그림 2: 「DllMain이 스레드 종료를 기다린다」는, 스레드 종료 자체가 로더 락을 필요로 하므로 구조적으로 데드락이 납니다.

핵심은 이것이 「운이 나쁘면 일어나는」 종류가 아니라 구조적으로 성립해 버린다는 점입니다. 문서는 로더 락을, 앱이 정의하는 락 계층의 최상위(가장 먼저 잡는 것)로 다루라고 지시합니다. DllMain 안에서는 이미 그 최상위 락을 쥐고 있으므로, 거기서 더 나아가 무언가를 기다리러 가는 행위 전반이 위험해진다고 정리하면 기억하기 쉽습니다.6

로더 락과 전용 락의 순서 역전DllMain은 로더 락을 쥔 채로 전용 락을 잡으러 가고, 워커 스레드는 전용 락을 쥔 채로 GetModuleHandle 등을 위해 로더 락을 잡으러 가므로, 획득 순서가 역전되어 데드락이 난다DllMain:로더 락 보유 중전용 락 G를 잡으러 감워커:전용 락 G 보유 중로더 락을 잡으러 감획득 순서 역전으로 데드락GetModuleHandle 등이 내부에서 요구

그림 3: GetModuleHandle처럼 대수롭지 않아 보이는 API도 내부에서 로더 락을 요구하므로, 전용 락과의 순서 역전이 성립할 수 있습니다.

참고로 DllMain 안에서 CreateThread를 호출하는 것 자체도 권장되지 않습니다. 만들어진 스레드는 DLL_THREAD_ATTACH 알림 처리에 로더 락이 필요하므로, 지금 실행 중인 DllMain이 반환되어 락을 풀 때까지 실행을 시작할 수 없습니다. 따라서 DllMain 안에서 그 스레드의 시작이나 완료를 기다리면 그 자리에서 데드락입니다. 수명 문제도 있습니다 ── DllMain이 반환된 뒤, 아직 시작하지 않은 스레드를 남긴 채 DLL이 언로드되면, 스레드의 시작 주소가 이미 해제된 코드를 가리킨 채로 실행되어 크래시합니다.3

4. C++ 개발자가 밟기 쉬운 두 지뢰

지뢰 1: 전역 객체의 동적 초기화. 2장에서 말했듯이, 정적 객체의 생성자는 DllMain의 제약 아래에서 실행됩니다. 설정 파일을 읽고, 로그 기능을 켜고, COM을 초기화하고, 스레드를 시작하는 ── 그런 일을 생성자에 가진 전역 변수를 DLL에 두는 순간, 그것은 「DllMain에서 해서는 안 되는 일」의 실행이 됩니다. 컴파일 시점에 정해지는 상수 초기화(constexpr로 만들 수 있는 것)는 안전하지만, 함수 호출이 따르는 초기화는 미루십시오.

전역 객체 초기화가 지뢰가 되는 경로DLL 로드에서 로더 락이 획득되고, CRT를 거쳐 전역 객체의 생성자가 실행되므로, 그 안의 LoadLibrary나 스레드 동기화나 COM 초기화는 DllMain 금지 사항의 실행이 된다DLL 로드(로더 락 획득)CRT의 진입점전역 객체의 생성자LoadLibrary에 해당하는 처리스레드 시작과 완료 대기COM이나 User32 사용모두 DllMain의 금지 사항에 해당

그림 4: 「DllMain은 비어 있으니 안전하다」여도, 복잡한 초기화를 가진 전역 변수가 있으면 같은 위험이 되살아납니다.

지뢰 2: C++/CLI(혼합 어셈블리). 네이티브 DLL을 C++/CLI로 감싸는 구성(래퍼 기사에서 다룬 형태)에서는, 로더 락 아래에서 MSIL(매니지드 코드)을 실행해 버리는 위험이 있습니다. MSIL 실행은 CLR 초기화나 다른 어셈블리 로드를 일으킬 수 있기 때문입니다. 컴파일러는 DllMain이 MSIL을 직접 실행하는 코드에는 경고 C4747을 내지만, 다른 모듈의 함수를 거친 간접 실행은 감지하지 못합니다. DllMain과 거기서 호출되는 함수는 #pragma unmanaged로 네이티브 컴파일하거나, DllMain 자체를 두지 않는 구성으로 합니다.4

로더 락 아래 MSIL 실행의 감지 여부DllMain이 MSIL을 직접 실행하는 코드는 컴파일러가 경고 C4747로 감지할 수 있지만, 다른 모듈의 함수를 거친 간접 실행은 감지하지 못하므로, 호출 트리 검토와 네이티브 컴파일 철저로 막아야 한다DllMain에서의 호출직접 MSIL을 실행다른 모듈을 거쳐 실행경고 C4747로 감지할 수 있음컴파일러는 감지하지 못함검토와 #pragma unmanaged로 막음

그림 5: C4747이 지켜 주는 것은 직접 실행뿐입니다. 간접 경로는 검토로만 잡을 수 있습니다.

5. 올바른 설계 ── 「지연」을 기본 방침으로

공식 베스트 프랙티스의 권고는 분명합니다.1

  1. 가능한 초기화는 컴파일 시(정적)에 끝냅니다. 동적 초기화를 정적 초기화로 바꿀 수 없는지, 먼저 생각합니다.
  2. 나머지는 첫 사용 시까지 미룹니다. 첫 사용이 「DLL 로드가 끝난 뒤에 호출되는 보통 API」에서 일어나는 한, 초기화는 로더 락 밖에서 실행되어 Windows API 거의 전체를 안전하게 쓸 수 있습니다. 첫 접근의 배타에는 INIT_ONCE(일회 초기화)나 C++의 매직 스태틱(함수 안 static)을 쓸 수 있습니다. 다만 지연은 만능이 아닙니다 ── 그 첫 접근을 DllMain이나 정적 초기화기 안에서 하면, 초기화기는 결국 로더 락 아래에서 실행되어 같은 제약으로 돌아갑니다.
  3. 일찍 감지해야 할 실패만 예외로 둡니다. 설정 파일이 깨져 있으면 로드 자체를 실패시키고 싶다는 요구는 있을 수 있습니다. 그 경우에도 「시도하고 바로 실패하는」 최소에 머무릅니다.
  4. DLL_PROCESS_ATTACH에서 DisableThreadLibraryCalls를 검토합니다. 스레드 알림을 쓰지 않는 DLL이라면 알림 비용 자체를 없앨 수 있습니다(정적 CRT·정적 TLS 사용 시는 제외).5
  5. Application Verifier로 검사합니다. DllMain 안의 위험한 호출 상당수는 Application Verifier가 런타임에 감지해 줍니다.1
DLL 초기화의 설계 지침초기화는 먼저 컴파일 시의 정적 초기화로 할 수 있는지 검토하고, 안 되면 첫 사용 시로의 지연을 기본으로 하며, 로드 실패로 일찍 감지해야 할 것만 최소한 DllMain에 남긴다예아니오아니오예컴파일 시점에 정할 수 있는가?정적 초기화로 한다실패를 로드 시점에 감지해야 하는가?첫 사용 시로 미룬다(기본)최소한만 DllMain에서 한다INIT_ONCE나 함수 안 static으로 배타

그림 6: 판단 순서는 「정적으로 할 수 없는지 → 지연할 수 없는지」이고, DllMain에 남기는 것은 조기 감지가 필요한 최소뿐입니다.

DisableThreadLibraryCalls의 적용 여부는 다음 분기로 기계적으로 정할 수 있습니다.

DisableThreadLibraryCalls를 호출할지 판단정적 CRT와 링크한 DLL에서는 호출하면 안 되고, 정적 TLS가 유효하면 호출 자체가 실패하므로 호출하지 않으며, 둘 다 아니고 스레드 알림을 쓰지 않는 DLL이면 DLL_PROCESS_ATTACH에서 반환값을 확인하며 호출해 알림 비용을 줄일 수 있다예아니오예아니오아니오예정적 CRT와 링크?호출하면 안 된다정적 TLS를 사용?호출해도 실패한다(FALSE)스레드 알림이 필요한가?ATTACH에서 호출(반환값 확인)호출하지 않고 알림을 처리

그림 7: 정적 CRT·정적 TLS·알림 필요 여부 세 조건으로, 호출해야 하는지가 하나로 정해집니다.

언로드 시의 스레드 정지에는 공식 문서가 구체적인 프로토콜을 제시합니다. DLL_PROCESS_DETACH(FreeLibrary에 의한 언로드 시)에서 워커 스레드의 종료를 「기다리지」 말고, (1) 이벤트로 종료를 알리고, (2) 스레드 쪽은 일을 일관된 상태까지 마무리한 뒤 신호를 돌려주고 무한 대기에 들어가며, (3) DllMain 쪽은 일관된 상태를 확인한 뒤 TerminateThread로 끝내는 ── 형태입니다.3 거칠어 보이지만, 「DllMain 안에서 스레드의 자연 종료를 기다려서는 안 된다」는 제약 안에서의 현실적 해법으로 문서화되어 있습니다.

언로드 시의 스레드 정지 프로토콜DllMain은 이벤트로 워커 스레드에 종료를 알리고, 워커는 일을 일관된 상태까지 마무리한 뒤 신호를 돌려주고 무한 대기에 들어가며, DllMain은 일관된 상태를 확인한 뒤 스레드를 종료시킨다워커 스레드DllMain(DETACH 처리)워커 스레드DllMain(DETACH 처리)자연 종료를 기다리지 않으므로 데드락이 나지 않음이벤트로 종료를 알림일을 일관된 상태까지 마무리일관 완료를 알리고 무한 대기TerminateThread로 종료시킴

그림 8: 「자연 종료를 기다리는」 대신 「일관 상태의 신호를 기다렸다가 끊는」 것으로, 로더 락과의 충돌을 피합니다.

애초에, 언로드될 수 있는 DLL에서 스레드를 가지는 설계 자체를 피하고, 스레드 소유를 EXE 쪽으로 두는 것이 가장 안전합니다.

프로세스 종료 시의 DLL_PROCESS_DETACH는 반대로, 아무것도 하지 않고 반환하는 것이 이상적입니다. 이 시점에 다른 스레드는 모두 강제 종료된 상태이고, 의존하는 DLL이나 런타임의 상태도 믿을 수 없습니다. 여기서 복잡한 처리를 하면 데드락이나 크래시의 원인만 됩니다. 영속화가 필요한 데이터는 앱 본래의 종료 처리 안에서 써 두어야 하며, 이 알림에 기대지 마십시오.3

6. 맞닥뜨렸을 때의 조사 방법

로더 락이 얽힌 hang에는 알아보기 쉬운 지문이 있습니다.

hang 중의 덤프에서 스택을 봅니다. 멈춘 순간의 덤프를 떠서 각 스레드의 스택을 확인합니다. ntdll.dll의 로더 함수(Ldr로 시작하는 함수군) 안에서 락을 기다리는 스레드와, DllMain이나 정적 초기화기(dynamic initializer) 안에서 다른 무언가를 기다리는 스레드의 쌍이 보이면 거의 확정입니다. LoadLibrary 호출 중에 멈춰 있는 스레드도 전형적인 등장 인물입니다.

로더 락이 얽힌 hang의 지문hang 중의 덤프에서, ntdll의 로더 함수 안에서 락을 기다리는 스레드와, DllMain이나 정적 초기화기 안에서 다른 무언가를 기다리는 스레드의 쌍이 있으면, 로더 락 데드락으로 거의 확정할 수 있다예아니오hang 중의 덤프Ldr 계열 함수에서 락 대기 중인 스레드DllMain·정적 초기화기 안에서 기다리는 스레드둘 다 있는가?로더 락 데드락으로 거의 확정다른 계통의 hang으로 조사

그림 9: 로더 락이 얽힌 hang에는 「Ldr 대기 + DllMain 안 대기」라는 알아보기 쉬운 지문이 있습니다.

「타이밍 의존」 성격을 의심합니다. 로더 락 데드락은 DLL 로드와 스레드의 시작·종료가 겹친 순간에만 성립합니다. 「시작 시 가끔」, 「특정 머신에서만」, 「서비스로 돌렸을 때만」 같은 재현 조건은 이 종류 문제의 신호입니다.

예방 검사를 돌립니다. Application Verifier를 켜고 테스트를 돌리면 DllMain 안의 위험한 호출을 런타임에 감지할 수 있습니다.1 C++/CLI라면 경고 C4747을 무시하지 말고, DllMain에서 도달하는 함수의 검토에서는 「간접적으로 LoadLibrary를 호출하는 함수」(COM 초기화, 일부 CRT 기능, delay-load import 등)에 주의하는 관점을 검토 항목에 넣어 두면, 사고를 만들어 넣기 전에 잡을 수 있습니다. delay-load된 import 함수의 첫 호출이 내부에서 LoadLibrary가 되는 점은 놓치기 쉬운 포인트입니다.

7. 정리

  • DllMain은 로더 락(프로세스에 하나, 모든 DLL 알림을 직렬화하는 락)을 쥔 채로 호출됩니다. 제약은 모두 여기서 나옵니다.
  • 금지 사항의 핵은 「LoadLibrary / FreeLibrary를 호출하지 않는다」, 「다른 스레드와 동기화하지 않는다」, 「Kernel32 이외의 DLL에 의존하는 함수를 호출하지 않는다」입니다. CRT를 거쳐 도는 정적 객체의 생성자·소멸자도 같은 제약을 받습니다.
  • 설계의 기본 방침은 지연입니다. 정적으로 할 수 있는 초기화는 정적으로, 나머지는 첫 사용 시에. DisableThreadLibraryCalls와 Application Verifier를 활용합니다.
  • 언로드 시의 스레드 정지는 공식 프로토콜(신호 → 일관 확인 → 종료)을 따릅니다. 프로세스 종료 시의 DLL_PROCESS_DETACH는 빈 것이 이상적입니다.
  • C++/CLI에서는 로더 락 아래의 MSIL 실행이 고유의 지뢰입니다. DllMain의 호출 트리는 네이티브 컴파일을 철저히 합니다.

DllMain의 제약은 언뜻 무리한 금지 사항의 나열처럼 보입니다. 그러나 「로더 락이라는 최상위 락을 쥔 채로 호출된다」는 한 점을 잡으면, 모든 금지 사항은 같은 원리를 달리 말한 것임을 알 수 있습니다. 원리로 기억해 두면, 문서에 없는 경계 사례를 만났을 때도 「이것은 락을 쥔 채로 해도 되는 일인가?」라는 올바른 질문을 세울 수 있을 것입니다.

관련 기사

관련 상담 영역

합동회사 코무라소프트에서는 시작 시·DLL 로드 시의 hang이나 데드락의 원인 조사(덤프 분석), DllMain·정적 초기화 주변의 설계 리뷰, C++/CLI 래퍼나 플러그인 DLL의 안전한 초기화 설계로의 수정을 다루고 있습니다. 「특정 환경에서만 시작이 멈춘다」처럼 재현이 어려운 단계부터도 상담해 주십시오.

참고 링크

  1. Microsoft Learn, Dynamic-Link Library Best Practices. DllMain이 로더 락 보유 중에 호출되므로 호출할 수 있는 함수에 중대한 제약이 있다는 점, 이상적인 DllMain은 빈 스텁이며 초기화는 가능한 한 지연해야 한다는 점, 컴파일 시 정적 초기화의 권고, 조기 감지가 필요한 실패만 최소한 한다는 점, Application Verifier로 DllMain 안의 전형적인 오류를 감지한다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. Microsoft Learn, DllMain entry point. 진입점에서는 단순한 초기화·종료 처리만 해야 한다는 점, LoadLibrary / FreeLibrary를 호출하면 안 되는 이유(로드 순서의 순환과 초기화 전·종료 후 DLL 사용), Kernel32.dll은 반드시 로드되어 있으므로 다른 DLL을 로드하지 않는 범위에서 호출할 수 있다는 점, 안전한 함수의 망라 목록은 존재하지 않는다는 점, User·Shell·COM 함수가 액세스 위반을 부른다는 점, DLL 알림이 직렬화되어 있어 다른 스레드·다른 프로세스와의 통신이 데드락을 부른다는 점, CRT와 링크한 경우에는 정적 객체의 생성자·소멸자에도 같은 제약이 적용된다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  3. Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. DllMain 안에서 스레드 종료를 기다리면 데드락이 나는 구조(스레드 종료의 DLL_THREAD_DETACH 알림이 로더 락을 필요로 함), 언로드 시의 스레드 정지 프로토콜(이벤트로 알리고 일관 상태를 확인한 뒤 종료), 프로세스 종료 시의 DLL_PROCESS_DETACH에서는 다른 스레드가 강제 종료된 상태이고 주소 공간의 일관성이 보장되지 않아 이상적인 핸들러는 비어 있다는 점, DllMain에서 스레드를 만들면 초기화가 끝나지 않은 채 알림이 밀려 문제를 만든다는 점에 대해. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, Initialization of Mixed Assemblies. 로더 락 아래에서 MSIL을 실행하면 안 된다는 점, DllMain과 그 호출 트리를 MSIL로 컴파일하면 안 되며 #pragma unmanaged로 대처한다는 점, DllMain이 MSIL을 직접 실행하려고 하면 경고 C4747이 나오지만 다른 모듈을 거친 간접 실행은 감지되지 않는다는 점, 정적 객체의 동적 초기화기도 같은 문제를 일으킬 수 있다는 점에 대해. ↩ ↩2

  5. Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). DLL_THREAD_ATTACH / DLL_THREAD_DETACH 알림을 꺼 스레드 생성·소멸 시의 오버헤드를 줄일 수 있다는 점, 정적 CRT와 링크한 DLL에서는 호출하면 안 된다는 점, 정적 TLS(thread_local이나 __declspec(thread))가 유효하면 최적화가 수행되지 않는다는 점에 대해. ↩ ↩2

  6. Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. 락 계층을 정의하고 항상 같은 순서로 획득해야 한다는 점, 로더는 로더 락을 획득한 뒤 DllMain을 호출하므로 로더 락을 락 계층의 최상위에 두어야 한다는 점, GetModuleFileName처럼 간접적으로 로더 락을 잡는 API와 전용 락의 획득 순서를 지키라는 점, 락 순서 역전에 의한 데드락의 실예에 대해. ↩ ↩2

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

DllMain에서는 정말로 아무것도 할 수 없나요?
「아무것도 하지 말라」는 과장이 아니라 공식 설계 방침이고, 이상적인 DllMain은 거의 빈 스텁이라고 Microsoft가 말합니다. 안전한 것은 DllMain 시점에 이미 로드된 Kernel32.dll의, 다른 DLL을 로드하지 않는 범위의 함수뿐입니다. 크리티컬 섹션·뮤텍스 생성과 TLS는 가능합니다. LoadLibrary/FreeLibrary, 다른 스레드 동기화, User32·Shell·COM 호출은 데드락·액세스 위반의 원인이므로 금지입니다. 판단이 어려운 초기화는 DllMain에서 하지 말고 처음 사용될 때까지 미루는 것이 정답입니다.
C++ 전역 변수(정적 객체)의 생성자도 DllMain 제약을 받나요?
받습니다. CRT(C++ 런타임)와 링크된 DLL에서는 전역·정적 객체의 생성자·소멸자가 CRT 진입점을 거쳐 사실상 DllMain의 일부로 실행됩니다. 생성자에서 LoadLibrary 호출, 다른 스레드를 시작해 완료 대기, COM 초기화는 DllMain과 같은 위험이 있습니다. 복잡한 초기화는 포인터로 두어 첫 접근 때 생성하거나 함수 안 static으로 시점을 DllMain 밖으로 옮기십시오.
DisableThreadLibraryCalls는 호출하는 편이 좋나요?
조건부입니다. DLL_THREAD_ATTACH/DETACH가 필요 없으면 DLL_PROCESS_ATTACH에서 호출해 스레드 생성·소멸 알림을 막고, 스레드를 자주 만드는 프로세스의 오버헤드를 줄입니다. 정적 CRT와 링크한 DLL에서는 호출하면 안 됩니다(정적 CRT가 스레드 알림을 필요로 합니다). thread_local·__declspec(thread) 정적 TLS가 유효하면 실패하고 FALSE가 반환되므로 반환값을 확인하십시오. 동적 링크 CRT의 일반 DLL에서 스레드 알림에 의존하지 않음을 확인한 뒤 쓰십시오.
C++/CLI(매니지드 혼합) DLL이 시작 시 hang이 나는 이유는 무엇인가요?
로더 락을 쥔 채 MSIL을 실행하려는 것이 전형입니다. 혼합 어셈블리에서 DllMain·호출 함수·전역의 동적 초기화기가 MSIL이면 CLR 초기화나 다른 어셈블리 로드가 필요해 데드락이 날 수 있습니다. 직접 실행은 경고 C4747이 나지만 다른 모듈을 거친 간접 실행은 감지되지 않습니다. DllMain과 호출 트리를 #pragma unmanaged로 네이티브 컴파일하거나 DllMain을 두지 마십시오.
DLL_PROCESS_DETACH에서 리소스를 뒷정리해도 되나요?
프로세스 종료와 FreeLibrary 언로드에서 답이 다릅니다. 종료 시에는 다른 스레드가 이미 강제 종료되었고 주소 공간 일관성도 보장되지 않아 메모리 해제 같은 뒷정리는 오히려 위험합니다. 이상적인 핸들러는 비어 있습니다. 영속 데이터는 앱 종료 처리에서 쓰고 여기서는 아무것도 하지 않고 반환하는 것이 안전합니다. FreeLibrary 언로드는 프로세스가 살아 있으므로 스레드 정지·핸들 닫기가 필요하지만, DllMain 안에서 스레드 종료를 기다리면 데드락이 납니다. 신호 후 일관 상태까지 기다렸다가 DllMain 밖에서 끝내는 공식 프로토콜을 따르십시오.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기