수정 이력(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을 건드리는 크래시가 생길 수 있습니다.1LoadLibrary/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에서 실행하는 것과 같습니다.
flowchart TB
accTitle: DllMain이 호출되는 네 가지 타이밍
accDescr: DLL 로드에서 DLL_PROCESS_ATTACH가, 프로세스 안 스레드 시작·종료마다 이미 로드된 모든 DLL에 DLL_THREAD_ATTACH와 DETACH가, 언로드나 프로세스 종료에서 DLL_PROCESS_DETACH가 호출되며, CRT를 거쳐 정적 객체의 생성자도 이 안에서 실행된다
load["DLL 로드"] --> pa["DLL_PROCESS_ATTACH"]
pa --> ta["DLL_THREAD_ATTACH(스레드 시작마다)"]
ta --> td["DLL_THREAD_DETACH(스레드 종료마다)"]
td --> pd["DLL_PROCESS_DETACH(언로드·종료 시)"]
pa -.-> crt["정적 객체의 생성도 여기서 실행"]
그림 1: DllMain은 로드 때뿐 아니라 스레드 시작·종료마다 호출되며, 정적 객체의 초기화도 그 일부로 실행됩니다.
3. 로더 락 ── 모든 알림을 직렬화하는 락 하나
왜 DllMain에만 이토록 심한 제약이 걸리는가. 답은 로더의 구조에 있습니다.
OS 로더는 DLL의 로드·언로드·각종 알림이라는 일련의 작업 정합성을 지키기 위해, 프로세스에 단 하나인 로더 락으로 처리를 직렬화합니다. 그리고 중요한 점은 DllMain이 이 로더 락을 쥔 채로 호출된다는 것입니다.1 DllMain 안에 있는 동안, 그 프로세스에서는 다른 DLL 로드도, 새 스레드의 시작 알림도, 모두 이 락이 풀리기를 기다립니다.
이 구조에서 금지 사항의 이유가 연쇄적으로 나옵니다.
LoadLibrary를 호출하면 안 되는 이유는 로더 락의 재진입이나 로드 순서의 순환 의존을 만들기 때문입니다. 초기화가 아직 끝나지 않은 DLL의 함수가 호출되는 사태도 일어날 수 있습니다.2- 다른 스레드와의 동기화가 위험한 이유는, 기다리는 상대 스레드가 로더 락이 필요한 순간(시작·종료 시의 알림,
GetModuleHandle계열 API 호출 등)이 있기 때문입니다. 이쪽은 로더 락을 쥐고 상대를 기다리고, 상대는 로더 락을 기다립니다 ── 고전적인 락 순서 역전입니다.6 - User·Shell·COM 함수가 위험한 이유는 이들이 내부에서 다른 시스템 구성 요소를 로드하기 때문입니다. 초기화 전·해제 후의 구성 요소를 건드려 액세스 위반이 납니다.2
sequenceDiagram
accTitle: DllMain에서 스레드를 기다리면 데드락이 나는 구조
accDescr: 로더 락을 쥔 DllMain이 워커 스레드의 종료를 기다리지만, 종료하려는 워커 스레드는 DLL_THREAD_DETACH 알림을 위해 로더 락이 풀리기를 기다려, 서로 기다리며 데드락이 된다
participant L as 로더(락 보유)
participant D as DllMain
participant W as 워커 스레드
L->>D: DLL_PROCESS_DETACH를 알림
D->>W: 종료를 요청하고 완료를 기다림
W->>W: 처리를 마치고 스레드 종료로
Note over W: 종료 알림에 로더 락이 필요
Note over D,W: DllMain은 락을 쥐고 기다리고, W는 락을 기다림
그림 2: 「DllMain이 스레드 종료를 기다린다」는, 스레드 종료 자체가 로더 락을 필요로 하므로 구조적으로 데드락이 납니다.
핵심은 이것이 「운이 나쁘면 일어나는」 종류가 아니라 구조적으로 성립해 버린다는 점입니다. 문서는 로더 락을, 앱이 정의하는 락 계층의 최상위(가장 먼저 잡는 것)로 다루라고 지시합니다. DllMain 안에서는 이미 그 최상위 락을 쥐고 있으므로, 거기서 더 나아가 무언가를 기다리러 가는 행위 전반이 위험해진다고 정리하면 기억하기 쉽습니다.6
flowchart TB
accTitle: 로더 락과 전용 락의 순서 역전
accDescr: DllMain은 로더 락을 쥔 채로 전용 락을 잡으러 가고, 워커 스레드는 전용 락을 쥔 채로 GetModuleHandle 등을 위해 로더 락을 잡으러 가므로, 획득 순서가 역전되어 데드락이 난다
d["DllMain:로더 락 보유 중"] --> dg["전용 락 G를 잡으러 감"]
w["워커:전용 락 G 보유 중"] --> wl["로더 락을 잡으러 감"]
dg -.-> dead["획득 순서 역전으로 데드락"]
wl -.-> dead
wl -.-> api["GetModuleHandle 등이 내부에서 요구"]
그림 3: GetModuleHandle처럼 대수롭지 않아 보이는 API도 내부에서 로더 락을 요구하므로, 전용 락과의 순서 역전이 성립할 수 있습니다.
참고로 DllMain 안에서 CreateThread를 호출하는 것 자체도 권장되지 않습니다. 만들어진 스레드는 DLL_THREAD_ATTACH 알림 처리에 로더 락이 필요하므로, 지금 실행 중인 DllMain이 반환되어 락을 풀 때까지 실행을 시작할 수 없습니다. 따라서 DllMain 안에서 그 스레드의 시작이나 완료를 기다리면 그 자리에서 데드락입니다. 수명 문제도 있습니다 ── DllMain이 반환된 뒤, 아직 시작하지 않은 스레드를 남긴 채 DLL이 언로드되면, 스레드의 시작 주소가 이미 해제된 코드를 가리킨 채로 실행되어 크래시합니다.3
4. C++ 개발자가 밟기 쉬운 두 지뢰
지뢰 1: 전역 객체의 동적 초기화. 2장에서 말했듯이, 정적 객체의 생성자는 DllMain의 제약 아래에서 실행됩니다. 설정 파일을 읽고, 로그 기능을 켜고, COM을 초기화하고, 스레드를 시작하는 ── 그런 일을 생성자에 가진 전역 변수를 DLL에 두는 순간, 그것은 「DllMain에서 해서는 안 되는 일」의 실행이 됩니다. 컴파일 시점에 정해지는 상수 초기화(constexpr로 만들 수 있는 것)는 안전하지만, 함수 호출이 따르는 초기화는 미루십시오.
flowchart TB
accTitle: 전역 객체 초기화가 지뢰가 되는 경로
accDescr: DLL 로드에서 로더 락이 획득되고, CRT를 거쳐 전역 객체의 생성자가 실행되므로, 그 안의 LoadLibrary나 스레드 동기화나 COM 초기화는 DllMain 금지 사항의 실행이 된다
load["DLL 로드(로더 락 획득)"] --> crt["CRT의 진입점"]
crt --> ctor["전역 객체의 생성자"]
ctor --> ng1["LoadLibrary에 해당하는 처리"]
ctor --> ng2["스레드 시작과 완료 대기"]
ctor --> ng3["COM이나 User32 사용"]
ng1 -.-> risk["모두 DllMain의 금지 사항에 해당"]
ng2 -.-> risk
ng3 -.-> risk
그림 4: 「DllMain은 비어 있으니 안전하다」여도, 복잡한 초기화를 가진 전역 변수가 있으면 같은 위험이 되살아납니다.
지뢰 2: C++/CLI(혼합 어셈블리). 네이티브 DLL을 C++/CLI로 감싸는 구성(래퍼 기사에서 다룬 형태)에서는, 로더 락 아래에서 MSIL(매니지드 코드)을 실행해 버리는 위험이 있습니다. MSIL 실행은 CLR 초기화나 다른 어셈블리 로드를 일으킬 수 있기 때문입니다. 컴파일러는 DllMain이 MSIL을 직접 실행하는 코드에는 경고 C4747을 내지만, 다른 모듈의 함수를 거친 간접 실행은 감지하지 못합니다. DllMain과 거기서 호출되는 함수는 #pragma unmanaged로 네이티브 컴파일하거나, DllMain 자체를 두지 않는 구성으로 합니다.4
flowchart TB
accTitle: 로더 락 아래 MSIL 실행의 감지 여부
accDescr: DllMain이 MSIL을 직접 실행하는 코드는 컴파일러가 경고 C4747로 감지할 수 있지만, 다른 모듈의 함수를 거친 간접 실행은 감지하지 못하므로, 호출 트리 검토와 네이티브 컴파일 철저로 막아야 한다
d2["DllMain에서의 호출"] --> dir["직접 MSIL을 실행"]
d2 --> ind["다른 모듈을 거쳐 실행"]
dir --> c47["경고 C4747로 감지할 수 있음"]
ind --> nc["컴파일러는 감지하지 못함"]
nc -.-> rv["검토와 #pragma unmanaged로 막음"]
그림 5: C4747이 지켜 주는 것은 직접 실행뿐입니다. 간접 경로는 검토로만 잡을 수 있습니다.
5. 올바른 설계 ── 「지연」을 기본 방침으로
공식 베스트 프랙티스의 권고는 분명합니다.1
- 가능한 초기화는 컴파일 시(정적)에 끝냅니다. 동적 초기화를 정적 초기화로 바꿀 수 없는지, 먼저 생각합니다.
- 나머지는 첫 사용 시까지 미룹니다. 첫 사용이 「DLL 로드가 끝난 뒤에 호출되는 보통 API」에서 일어나는 한, 초기화는 로더 락 밖에서 실행되어 Windows API 거의 전체를 안전하게 쓸 수 있습니다. 첫 접근의 배타에는
INIT_ONCE(일회 초기화)나 C++의 매직 스태틱(함수 안 static)을 쓸 수 있습니다. 다만 지연은 만능이 아닙니다 ── 그 첫 접근을DllMain이나 정적 초기화기 안에서 하면, 초기화기는 결국 로더 락 아래에서 실행되어 같은 제약으로 돌아갑니다. - 일찍 감지해야 할 실패만 예외로 둡니다. 설정 파일이 깨져 있으면 로드 자체를 실패시키고 싶다는 요구는 있을 수 있습니다. 그 경우에도 「시도하고 바로 실패하는」 최소에 머무릅니다.
- DLL_PROCESS_ATTACH에서
DisableThreadLibraryCalls를 검토합니다. 스레드 알림을 쓰지 않는 DLL이라면 알림 비용 자체를 없앨 수 있습니다(정적 CRT·정적 TLS 사용 시는 제외).5 - Application Verifier로 검사합니다.
DllMain안의 위험한 호출 상당수는 Application Verifier가 런타임에 감지해 줍니다.1
flowchart TB
accTitle: DLL 초기화의 설계 지침
accDescr: 초기화는 먼저 컴파일 시의 정적 초기화로 할 수 있는지 검토하고, 안 되면 첫 사용 시로의 지연을 기본으로 하며, 로드 실패로 일찍 감지해야 할 것만 최소한 DllMain에 남긴다
q1{"컴파일 시점에 정할 수 있는가?"} -->|"예"| s["정적 초기화로 한다"]
q1 -->|"아니오"| q2{"실패를 로드 시점에 감지해야 하는가?"}
q2 -->|"아니오"| lazy["첫 사용 시로 미룬다(기본)"]
q2 -->|"예"| min["최소한만 DllMain에서 한다"]
lazy -.-> once["INIT_ONCE나 함수 안 static으로 배타"]
그림 6: 판단 순서는 「정적으로 할 수 없는지 → 지연할 수 없는지」이고, DllMain에 남기는 것은 조기 감지가 필요한 최소뿐입니다.
DisableThreadLibraryCalls의 적용 여부는 다음 분기로 기계적으로 정할 수 있습니다.
flowchart TB
accTitle: DisableThreadLibraryCalls를 호출할지 판단
accDescr: 정적 CRT와 링크한 DLL에서는 호출하면 안 되고, 정적 TLS가 유효하면 호출 자체가 실패하므로 호출하지 않으며, 둘 다 아니고 스레드 알림을 쓰지 않는 DLL이면 DLL_PROCESS_ATTACH에서 반환값을 확인하며 호출해 알림 비용을 줄일 수 있다
q1{"정적 CRT와 링크?"} -->|"예"| no2["호출하면 안 된다"]
q1 -->|"아니오"| q2{"정적 TLS를 사용?"}
q2 -->|"예"| eff["호출해도 실패한다(FALSE)"]
q2 -->|"아니오"| q3{"스레드 알림이 필요한가?"}
q3 -->|"아니오"| yes["ATTACH에서 호출(반환값 확인)"]
q3 -->|"예"| keep["호출하지 않고 알림을 처리"]
그림 7: 정적 CRT·정적 TLS·알림 필요 여부 세 조건으로, 호출해야 하는지가 하나로 정해집니다.
언로드 시의 스레드 정지에는 공식 문서가 구체적인 프로토콜을 제시합니다. DLL_PROCESS_DETACH(FreeLibrary에 의한 언로드 시)에서 워커 스레드의 종료를 「기다리지」 말고, (1) 이벤트로 종료를 알리고, (2) 스레드 쪽은 일을 일관된 상태까지 마무리한 뒤 신호를 돌려주고 무한 대기에 들어가며, (3) DllMain 쪽은 일관된 상태를 확인한 뒤 TerminateThread로 끝내는 ── 형태입니다.3 거칠어 보이지만, 「DllMain 안에서 스레드의 자연 종료를 기다려서는 안 된다」는 제약 안에서의 현실적 해법으로 문서화되어 있습니다.
sequenceDiagram
accTitle: 언로드 시의 스레드 정지 프로토콜
accDescr: DllMain은 이벤트로 워커 스레드에 종료를 알리고, 워커는 일을 일관된 상태까지 마무리한 뒤 신호를 돌려주고 무한 대기에 들어가며, DllMain은 일관된 상태를 확인한 뒤 스레드를 종료시킨다
participant D as DllMain(DETACH 처리)
participant W as 워커 스레드
D->>W: 이벤트로 종료를 알림
W->>W: 일을 일관된 상태까지 마무리
W->>D: 일관 완료를 알리고 무한 대기
D->>W: TerminateThread로 종료시킴
Note over D,W: 자연 종료를 기다리지 않으므로 데드락이 나지 않음
그림 8: 「자연 종료를 기다리는」 대신 「일관 상태의 신호를 기다렸다가 끊는」 것으로, 로더 락과의 충돌을 피합니다.
애초에, 언로드될 수 있는 DLL에서 스레드를 가지는 설계 자체를 피하고, 스레드 소유를 EXE 쪽으로 두는 것이 가장 안전합니다.
프로세스 종료 시의 DLL_PROCESS_DETACH는 반대로, 아무것도 하지 않고 반환하는 것이 이상적입니다. 이 시점에 다른 스레드는 모두 강제 종료된 상태이고, 의존하는 DLL이나 런타임의 상태도 믿을 수 없습니다. 여기서 복잡한 처리를 하면 데드락이나 크래시의 원인만 됩니다. 영속화가 필요한 데이터는 앱 본래의 종료 처리 안에서 써 두어야 하며, 이 알림에 기대지 마십시오.3
6. 맞닥뜨렸을 때의 조사 방법
로더 락이 얽힌 hang에는 알아보기 쉬운 지문이 있습니다.
hang 중의 덤프에서 스택을 봅니다. 멈춘 순간의 덤프를 떠서 각 스레드의 스택을 확인합니다. ntdll.dll의 로더 함수(Ldr로 시작하는 함수군) 안에서 락을 기다리는 스레드와, DllMain이나 정적 초기화기(dynamic initializer) 안에서 다른 무언가를 기다리는 스레드의 쌍이 보이면 거의 확정입니다. LoadLibrary 호출 중에 멈춰 있는 스레드도 전형적인 등장 인물입니다.
flowchart TB
accTitle: 로더 락이 얽힌 hang의 지문
accDescr: hang 중의 덤프에서, ntdll의 로더 함수 안에서 락을 기다리는 스레드와, DllMain이나 정적 초기화기 안에서 다른 무언가를 기다리는 스레드의 쌍이 있으면, 로더 락 데드락으로 거의 확정할 수 있다
dump["hang 중의 덤프"] --> t1["Ldr 계열 함수에서 락 대기 중인 스레드"]
dump --> t2["DllMain·정적 초기화기 안에서 기다리는 스레드"]
t1 --> pair{"둘 다 있는가?"}
t2 --> pair
pair -->|"예"| conf["로더 락 데드락으로 거의 확정"]
pair -->|"아니오"| other["다른 계통의 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의 제약은 언뜻 무리한 금지 사항의 나열처럼 보입니다. 그러나 「로더 락이라는 최상위 락을 쥔 채로 호출된다」는 한 점을 잡으면, 모든 금지 사항은 같은 원리를 달리 말한 것임을 알 수 있습니다. 원리로 기억해 두면, 문서에 없는 경계 사례를 만났을 때도 「이것은 락을 쥔 채로 해도 되는 일인가?」라는 올바른 질문을 세울 수 있을 것입니다.
관련 기사
- Windows DLL 이름 해결의 구조 - 검색 순서와 SxS
- 네이티브 DLL을 C++/CLI로 래핑하는 실무
- 멀티스레드 실무 베스트 프랙티스 C++ 편
- 스퓨리어스 웨이크업 ── 조건 변수가 「알림 없이 깨어나는」 이유와 Windows에서의 올바른 대기 방법
- WinDbg + SOS로 크래시 덤프 읽기 ── 수집한 뒤의 실무 분석 입문
- COM STA/MTA의 기초 지식 - 스레드 모델과 hang을 피하는 사고방식
관련 상담 영역
합동회사 코무라소프트에서는 시작 시·DLL 로드 시의 hang이나 데드락의 원인 조사(덤프 분석), DllMain·정적 초기화 주변의 설계 리뷰, C++/CLI 래퍼나 플러그인 DLL의 안전한 초기화 설계로의 수정을 다루고 있습니다. 「특정 환경에서만 시작이 멈춘다」처럼 재현이 어려운 단계부터도 상담해 주십시오.
참고 링크
-
Microsoft Learn, Dynamic-Link Library Best Practices. DllMain이 로더 락 보유 중에 호출되므로 호출할 수 있는 함수에 중대한 제약이 있다는 점, 이상적인 DllMain은 빈 스텁이며 초기화는 가능한 한 지연해야 한다는 점, 컴파일 시 정적 초기화의 권고, 조기 감지가 필요한 실패만 최소한 한다는 점, Application Verifier로 DllMain 안의 전형적인 오류를 감지한다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, DllMain entry point. 진입점에서는 단순한 초기화·종료 처리만 해야 한다는 점, LoadLibrary / FreeLibrary를 호출하면 안 되는 이유(로드 순서의 순환과 초기화 전·종료 후 DLL 사용), Kernel32.dll은 반드시 로드되어 있으므로 다른 DLL을 로드하지 않는 범위에서 호출할 수 있다는 점, 안전한 함수의 망라 목록은 존재하지 않는다는 점, User·Shell·COM 함수가 액세스 위반을 부른다는 점, DLL 알림이 직렬화되어 있어 다른 스레드·다른 프로세스와의 통신이 데드락을 부른다는 점, CRT와 링크한 경우에는 정적 객체의 생성자·소멸자에도 같은 제약이 적용된다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. DllMain 안에서 스레드 종료를 기다리면 데드락이 나는 구조(스레드 종료의 DLL_THREAD_DETACH 알림이 로더 락을 필요로 함), 언로드 시의 스레드 정지 프로토콜(이벤트로 알리고 일관 상태를 확인한 뒤 종료), 프로세스 종료 시의 DLL_PROCESS_DETACH에서는 다른 스레드가 강제 종료된 상태이고 주소 공간의 일관성이 보장되지 않아 이상적인 핸들러는 비어 있다는 점, DllMain에서 스레드를 만들면 초기화가 끝나지 않은 채 알림이 밀려 문제를 만든다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Initialization of Mixed Assemblies. 로더 락 아래에서 MSIL을 실행하면 안 된다는 점, DllMain과 그 호출 트리를 MSIL로 컴파일하면 안 되며 #pragma unmanaged로 대처한다는 점, DllMain이 MSIL을 직접 실행하려고 하면 경고 C4747이 나오지만 다른 모듈을 거친 간접 실행은 감지되지 않는다는 점, 정적 객체의 동적 초기화기도 같은 문제를 일으킬 수 있다는 점에 대해. ↩ ↩2
-
Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). DLL_THREAD_ATTACH / DLL_THREAD_DETACH 알림을 꺼 스레드 생성·소멸 시의 오버헤드를 줄일 수 있다는 점, 정적 CRT와 링크한 DLL에서는 호출하면 안 된다는 점, 정적 TLS(thread_local이나 __declspec(thread))가 유효하면 최적화가 수행되지 않는다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. 락 계층을 정의하고 항상 같은 순서로 획득해야 한다는 점, 로더는 로더 락을 획득한 뒤 DllMain을 호출하므로 로더 락을 락 계층의 최상위에 두어야 한다는 점, GetModuleFileName처럼 간접적으로 로더 락을 잡는 API와 전용 락의 획득 순서를 지키라는 점, 락 순서 역전에 의한 데드락의 실예에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Win32 스레드 풀 API ── CreateThreadpoolWork로 「스레드를 만들지 않는」 병렬 처리
네이티브 코드에서 CreateThread를 마구 늘리고 있지는 않습니까. Vista에서 새로 설계된 Win32 스레드 풀 API의 work, timer, wait, io 네 가지 객체와 클린업 그룹, 콜백에서 해서는 안 되는 일까지 1차 자료를 ...
spurious wakeup ── 조건 변수가 「알림 없이 깨어나는」 이유와 Windows에서 올바르게 기다리는 방법
조건 변수의 wait는 알림이 오지 않아도 깨어날 수 있습니다(spurious wakeup). 사양이 이를 허용하는 이유를 Windows 구현에서 밝히고, while과 predicate로 쓰는 올바른 대기 방법을 Win32·C++·C# 코드로 보...
인수는 왜 깨지는가 ── Windows 명령줄 인수의 규칙
Windows에는 인수의 배열이 존재하지 않으며, CreateProcess에 전달되는 것은 한 줄의 문자열이고 분할은 받는 쪽이 합니다. CommandLineToArgvW·CRT·.NET의 분할 규칙과 .NET의 ArgumentList, C++에...
부모가 죽은 뒤에 무엇이 남는가 — Job Object로 자식 프로세스를 기르기
UI를 강제로 종료해도 SDK의 헬퍼가 남아 카메라나 COM 포트를 붙잡고 놓지 않는 이유는 무엇인가. Job Object로 프로세스 트리를 하나의 단위로 만들고, KillOnJobClose와 완료 포트로 자식 프로세스의 수명을 설계하는 방법을 ...
Named Pipe 실무 ── Windows 프로세스 간 통신의 정석을 설계부터 보안까지
Windows 프로세스 간 통신의 정석인 Named Pipe를 실무 관점에서 설명합니다. 바이트/메시지 모드의 선택, 여러 클라이언트를 처리하는 서버 설계, ACL과 impersonation 보안, .NET의 NamedPipeStream까지 일차...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 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 밖에서 끝내는 공식 프로토콜을 따르십시오.