DllMain dan loader lock — alasan sebenarnya Anda diminta "jangan lakukan apa pun di inisialisasi DLL"
· Go Komura · Windows, DLL, Pengembangan Windows, C++, Pemecahan masalah, Multithreading, Win32 API
“Aplikasi hang saat start, tetapi hanya di lingkungan tertentu.” “Ketika kita memuat DLL sendiri, LoadLibrary kadang tidak pernah kembali.” “Ia deadlock hanya pada waktu start layanan.” — Ikuti investigasi seperti ini cukup jauh dan, lebih sering daripada tidak, Anda tiba di tempat yang sama. Kode inisialisasi DLL — yaitu, DllMain.
Dokumentasi Microsoft memperingatkan tentang DllMain dengan nada yang luar biasa tegas. Jangan panggil LoadLibrary. Jangan sinkronkan dengan thread lain. Jangan panggil fungsi User, Shell, atau COM. DllMain yang ideal adalah stub kosong — mengapa bahasanya sekuat ini? Alasannya terkonsentrasi pada satu mekanisme internal, loader lock. Ditujukan kepada pengembang yang menulis DLL, plug-in, dan wrapper C++/CLI di Windows, artikel ini menjelaskan, dari sumber primer, cara kerja loader lock, struktur yang membuat deadlock bertahan, serta desain aman dan prosedur investigasi.
1. Kesimpulan lebih dulu
DllMaindipanggil sambil memegang loader lock, kunci bersama yang persis ada satu per proses. Jadi memanggil, dariDllMain, pekerjaan yang mencoba mengambil loader lock (langsung atau tidak langsung) menciptakan kemungkinan deadlock, atau crash dari menyentuh DLL yang belum diinisialisasi.1- Memanggil
LoadLibrary/FreeLibrarydilarang. Itu menciptakan ketergantungan urutan muat yang sirkular dan dapat menyebabkan kode inisialisasi berjalan terhadap DLL yang inisialisasinya sendiri belum berjalan.2 - Menyinkronkan dengan thread lain juga dilarang. Notifikasi DLL diserialisasi, jadi menunggu di dalam
DllMainagar thread mulai atau keluar membiarkan thread itu sendiri berhenti menunggu loader lock, dan Anda deadlock.23 - Yang dapat Anda panggil dengan aman, dalam praktik, hanyalah subset Kernel32.dll. Dan dokumentasi resmi menyatakan dengan gamblang bahwa “daftar lengkap fungsi aman tidak ada”. Fungsi User, Shell, dan COM memuat komponen lain dan menyebabkan access violation.2
- Di DLL yang ditautkan dengan CRT, pembatasan yang sama berlaku untuk konstruktor dan destruktor global. Mereka berjalan sebagai bagian de facto dari
DllMain.2 - Desain yang benar adalah “tunda”. Lakukan inisialisasi yang dapat Anda lakukan pada waktu kompilasi (secara statis); tunda yang tidak bisa sampai pemakaian pertama. Itu praktik terbaik resmi.1
- DLL campuran C++/CLI sangat berbahaya. Untuk menghindari menjalankan MSIL di bawah loader lock,
DllMaindan pohon panggilannya harus dikompilasi native.4
2. Kapan dan bagaimana DllMain dipanggil
DllMain adalah entry point yang loader OS panggil ketika DLL masuk atau keluar dari proses atau thread. Ada empat notifikasi.
| Notifikasi | Waktu |
|---|---|
| DLL_PROCESS_ATTACH | Ketika DLL dimuat ke proses |
| DLL_THREAD_ATTACH | Ketika thread baru dimulai di proses |
| DLL_THREAD_DETACH | Ketika thread keluar secara normal |
| DLL_PROCESS_DETACH | Ketika DLL di-unload, atau ketika proses keluar |
Dua fakta mudah terlewat. Pertama, setiap kali satu thread dibuat, DllMain setiap DLL yang sudah dimuat dipanggil dengan DLL_THREAD_ATTACH. Dengan kata lain DllMain bukan “sesuatu yang berjalan sekali ketika DLL saya dimuat”; itu kode yang terus dipanggil untuk aktivitas thread proses. Jika Anda tidak membutuhkannya, Anda dapat menghentikannya dengan memanggil DisableThreadLibraryCalls di dalam DLL_PROCESS_ATTACH (jangan panggil dari DLL yang ditautkan dengan CRT statis).5
Kedua, di DLL yang ditautkan dengan CRT (runtime C/C++), konstruktor dan destruktor objek C++ global dan statis berjalan, lewat entry point CRT, sebagai bagian dari DllMain.2 Bahkan jika Anda pikir “DllMain kita kosong, jadi kita aman”, objek global dengan inisialisasi yang rumit sama dengan menjalankan pekerjaan itu di DllMain.
flowchart TB
accTitle: Empat waktu di mana DllMain dipanggil
accDescr: DLL_PROCESS_ATTACH berjalan saat muat DLL; DLL_THREAD_ATTACH dan DETACH berjalan pada setiap DLL yang sudah dimuat di setiap start dan keluar thread di proses; DLL_PROCESS_DETACH berjalan saat unload atau keluar proses; dan konstruktor objek statis juga berjalan di dalam ini lewat CRT
load["Muat DLL"] --> pa["DLL_PROCESS_ATTACH"]
pa --> ta["DLL_THREAD_ATTACH (pada setiap start thread)"]
ta --> td["DLL_THREAD_DETACH (pada setiap keluar thread)"]
td --> pd["DLL_PROCESS_DETACH (saat unload atau keluar)"]
pa -.-> crt["Konstruksi objek statis juga berjalan di sini"]
Gambar 1: DllMain dipanggil bukan hanya pada waktu muat tetapi pada setiap start dan keluar thread, dan inisialisasi objek statis juga berjalan sebagai bagian dari itu.
3. Loader lock — satu kunci yang menserialisasi setiap notifikasi
Mengapa pembatasan pada DllMain saja seberat ini? Jawabannya ada di struktur loader.
Untuk menjaga serangkaian operasi — muat DLL, unload, dan berbagai notifikasi — tetap konsisten, loader OS menserialisasi pekerjaan dengan satu loader lock per proses. Dan poin pentingnya adalah bahwa DllMain dipanggil sambil loader lock ini dipegang.1 Selama Anda berada di dalam DllMain, setiap muat DLL lain di proses itu, dan setiap notifikasi start-of-thread, menunggu kunci ini dilepas.
Dari struktur itu, alasan larangan mengikuti satu demi satu.
- Anda tidak boleh memanggil
LoadLibrarykarena itu menciptakan reentrancy loader-lock, atau ketergantungan urutan muat yang sirkular. Itu juga dapat berakibat memanggil fungsi pada DLL yang inisialisasinya belum selesai.2 - Menyinkronkan dengan thread lain berbahaya karena thread yang Anda tunggu punya momen ketika ia membutuhkan loader lock (notifikasi saat start dan keluar, panggilan ke API keluarga
GetModuleHandle, dan sebagainya). Anda memegang loader lock dan menunggu sisi lain; sisi lain menunggu loader lock — inversi urutan kunci yang klasik.6 - Fungsi User, Shell, dan COM berbahaya karena mereka memuat komponen sistem lain secara internal. Anda menyentuh komponen sebelum diinisialisasi, atau setelah dirobohkan, dan Anda mendapat access violation.2
sequenceDiagram
accTitle: Mengapa menunggu thread di dalam DllMain deadlock
accDescr: DllMain, memegang loader lock, menunggu thread worker keluar, tetapi worker yang mencoba keluar menunggu loader lock dilepas agar dapat menerima DLL_THREAD_DETACH, jadi mereka menunggu satu sama lain dan deadlock
participant L as Loader(memegang kunci)
participant D as DllMain
participant W as Worker
L->>D: DLL_PROCESS_DETACH
D->>W: Minta keluar dan tunggu
W->>W: Selesaikan pekerjaan, lalu keluar
Note over W: Notifikasi keluar membutuhkan kunci
Note over D,W: DllMain memegang kunci, W menunggu
Gambar 2: “DllMain menunggu thread keluar” adalah deadlock struktural, karena keluar thread itu sendiri membutuhkan loader lock.
Poinnya adalah ini bukan jenis hal yang “terjadi jika Anda sial”; itu dijamin secara struktural untuk bertahan. Dokumentasi menyuruh Anda memperlakukan loader lock sebagai puncak hierarki kunci yang aplikasi tetapkan (yang diambil lebih dulu). Di dalam DllMain Anda sudah memegang kunci tingkat atas itu, jadi setiap tindakan lanjut dari situ untuk menunggu sesuatu yang lain berbahaya — itu cara yang berguna untuk mengingatnya.6
flowchart TB
accTitle: Inversi urutan kunci antara loader lock dan kunci privat
accDescr: DllMain, memegang loader lock, pergi mengambil kunci privat, sementara worker, memegang kunci privat itu, pergi mengambil loader lock untuk GetModuleHandle atau serupa, jadi urutan perolehan terbalik dan mereka deadlock
d["DllMain: memegang loader lock"] --> dg["Pergi mengambil kunci privat G"]
w["Worker: memegang kunci privat G"] --> wl["Pergi mengambil loader lock"]
dg -.-> dead["Deadlock dari urutan perolehan terbalik"]
wl -.-> dead
wl -.-> api["GetModuleHandle dan serupa mensyaratkannya secara internal"]
Gambar 3: Bahkan API yang tampak tidak berbahaya seperti GetModuleHandle mensyaratkan loader lock secara internal, jadi inversi urutan dengan kunci privat dapat bertahan.
Juga, memanggil CreateThread dari dalam DllMain itu sendiri tidak direkomendasikan. Thread yang dibuat membutuhkan loader lock untuk memproses notifikasi DLL_THREAD_ATTACH, jadi ia tidak dapat mulai berjalan sampai DllMain yang sedang dieksekusi kembali dan melepas kunci. Oleh karena itu menunggu di dalam DllMain agar thread itu mulai atau selesai adalah deadlock segera. Ada masalah masa hidup juga — jika, setelah DllMain kembali, DLL di-unload sementara thread yang belum mulai berjalan masih tertinggal, alamat start thread masih menunjuk ke kode yang sudah dibebaskan dan Anda crash.3
4. Dua ranjau yang mudah dilangkahi pengembang C++
Ranjau 1: Inisialisasi dinamis objek global. Seperti dikatakan Bab 2, konstruktor objek statis berjalan di bawah pembatasan DllMain. Membaca berkas konfigurasi, mendirikan fasilitas logging, menginisialisasi COM, memulai thread — saat Anda menaruh global di DLL yang konstruktornya melakukan pekerjaan semacam itu, Anda mengeksekusi “hal yang tidak boleh dilakukan di DllMain”. Inisialisasi konstanta yang ditetapkan pada waktu kompilasi (apa pun yang dapat Anda buat constexpr) aman; inisialisasi yang melibatkan panggilan fungsi sebaiknya ditunda.
flowchart TB
accTitle: Jalur di mana menginisialisasi objek global menjadi ranjau
accDescr: Loader lock diambil saat muat DLL, dan konstruktor objek global berjalan lewat entry point CRT, jadi LoadLibrary, sinkronisasi thread, dan inisialisasi COM di dalam konstruktor itu adalah eksekusi larangan DllMain
load["Muat DLL (loader lock diperoleh)"] --> crt["Entry point CRT"]
crt --> ctor["Konstruktor objek global"]
ctor --> ng1["Pekerjaan setara LoadLibrary"]
ctor --> ng2["Memulai thread dan menunggu sampai selesai"]
ctor --> ng3["Memakai COM atau User32"]
ng1 -.-> risk["Semua ini jatuh di bawah larangan DllMain"]
ng2 -.-> risk
ng3 -.-> risk
Gambar 4: Bahkan “DllMain kosong, jadi kita aman” menghidupkan kembali bahaya yang sama saat Anda punya global dengan inisialisasi yang rumit.
Ranjau 2: C++/CLI (assembly campuran). Dalam konfigurasi yang membungkus DLL native dengan C++/CLI (bentuk yang dibahas di artikel wrapper), ada bahaya menjalankan MSIL (kode managed) di bawah loader lock. Menjalankan MSIL dapat memicu inisialisasi CLR atau muat assembly lain. Kompiler mengeluarkan peringatan C4747 pada kode di mana DllMain mengeksekusi MSIL secara langsung, tetapi tidak dapat mendeteksi eksekusi tidak langsung lewat fungsi di modul lain. Kompilasi DllMain dan fungsi yang dipanggil darinya sebagai native dengan #pragma unmanaged, atau pakai konfigurasi yang tidak punya DllMain sama sekali.4
flowchart TB
accTitle: Apakah eksekusi MSIL di bawah loader lock dapat dideteksi
accDescr: Kode di mana DllMain mengeksekusi MSIL secara langsung dapat dideteksi kompiler dengan peringatan C4747, tetapi eksekusi tidak langsung lewat fungsi di modul lain tidak, jadi Anda harus mencegahnya dengan meninjau pohon panggilan dan bersikeras pada kompilasi native
d2["Panggilan dari DllMain"] --> dir["Eksekusi MSIL secara langsung"]
d2 --> ind["Eksekusi lewat modul lain"]
dir --> c47["Dapat dideteksi dengan peringatan C4747"]
ind --> nc["Kompiler tidak dapat mendeteksinya"]
nc -.-> rv["Cegah dengan tinjauan dan #pragma unmanaged"]
Gambar 5: C4747 hanya melindungi Anda terhadap eksekusi langsung. Jalur tidak langsung hanya dapat ditangkap dengan tinjauan.
5. Desain yang benar — jadikan “tunda” kebijakan bawaan
Rekomendasi praktik terbaik resmi jelas.1
- Selesaikan inisialisasi yang dapat Anda lakukan pada waktu kompilasi (secara statis). Pertama tanya apakah inisialisasi dinamis dapat diganti dengan yang statis.
- Tunda sisanya sampai pemakaian pertama. Selama pemakaian pertama terjadi dari API biasa yang dipanggil setelah DLL selesai dimuat, inisialisasi berjalan di luar loader lock dan Anda dapat memakai hampir seluruh Windows API dengan aman. Untuk eksklusi pada akses pertama Anda dapat memakai
INIT_ONCE(inisialisasi sekali) atau C++ magic statics (static lokal fungsi). Penundaan bukan obat mujarab, walaupun — jika akses pertama itu sendiri dibuat dariDllMainatau initializer statis, initializer tetap berjalan di bawah loader lock dan Anda kembali di bawah pembatasan yang sama. - Buat pengecualian hanya untuk kegagalan yang harus Anda deteksi lebih awal. Anda mungkin punya persyaratan bahwa berkas konfigurasi yang rusak harus membuat muat itu sendiri gagal. Bahkan kemudian, batasi pada minimum “coba dan gagal segera”.
- Pertimbangkan
DisableThreadLibraryCallsdi DLL_PROCESS_ATTACH. Jika DLL tidak memakai notifikasi thread, Anda dapat menghapus biaya notifikasi itu sendiri (kecuali saat memakai CRT statis atau TLS statis).5 - Periksa dengan Application Verifier. Banyak panggilan berbahaya di dalam
DllMainadalah yang Application Verifier akan deteksi saat runtime.1
flowchart TB
accTitle: Panduan desain untuk inisialisasi DLL
accDescr: Pertama pertimbangkan apakah inisialisasi dapat menjadi inisialisasi statis waktu kompilasi; jika tidak, bawaan adalah menunda ke pemakaian pertama, dan tinggalkan di DllMain hanya minimum yang harus dideteksi lebih awal sebagai kegagalan muat
q1{"Dapat diputuskan pada waktu kompilasi?"} -->|"ya"| s["Jadikan inisialisasi statis"]
q1 -->|"tidak"| q2{"Harus kegagalan dideteksi pada waktu muat?"}
q2 -->|"tidak"| lazy["Tunda ke pemakaian pertama (bawaan)"]
q2 -->|"ya"| min["Lakukan hanya minimum di DllMain"]
lazy -.-> once["Eksklusi dengan INIT_ONCE atau static lokal fungsi"]
Gambar 6: Urutan keputusan adalah “dapatkah statis → dapatkah ditunda”, dan yang Anda tinggalkan di DllMain hanyalah minimum yang harus dideteksi lebih awal.
Apakah menerapkan DisableThreadLibraryCalls dapat diputuskan secara mekanis dengan cabang berikut.
flowchart TB
accTitle: Apakah memanggil DisableThreadLibraryCalls
accDescr: Jangan panggil dari DLL yang ditautkan dengan CRT statis; jika TLS statis berlaku panggilan itu sendiri gagal jadi Anda tidak memanggilnya; jika tidak keduanya dan DLL tidak memakai notifikasi thread, panggil di DLL_PROCESS_ATTACH, memeriksa nilai kembalian, untuk memotong biaya notifikasi
q1{"Ditautkan dengan CRT statis?"} -->|"ya"| no2["Tidak boleh memanggilnya"]
q1 -->|"tidak"| q2{"Memakai TLS statis?"}
q2 -->|"ya"| eff["Panggilan gagal saja (FALSE)"]
q2 -->|"tidak"| q3{"Notifikasi thread dibutuhkan?"}
q3 -->|"tidak"| yes["Panggil di ATTACH (periksa nilai kembalian)"]
q3 -->|"ya"| keep["Jangan panggil; tangani notifikasi"]
Gambar 7: Tiga syarat CRT statis, TLS statis, dan apakah notifikasi dibutuhkan memutuskan secara unik apakah Anda harus memanggilnya.
Untuk menghentikan thread saat unload, dokumentasi resmi memberi protokol konkret. Daripada “menunggu” thread worker keluar di DLL_PROCESS_DETACH (pada unload lewat FreeLibrary), bentuknya adalah (1) sinyal keluar dengan event, (2) sisi thread merapikan pekerjaannya ke keadaan konsisten, memberi sinyal balik, dan masuk tunggu tak terbatas, (3) sisi DllMain mengonfirmasi keadaan konsisten lalu merapikan thread dengan TerminateThread.3 Terlihat kasar, tetapi didokumentasikan sebagai jawaban realistis di dalam kendala “Anda tidak boleh menunggu keluar alami thread di dalam DllMain”.
sequenceDiagram
accTitle: Protokol untuk menghentikan thread saat unload
accDescr: DllMain memberi sinyal thread worker untuk keluar dengan event; worker merapikan pekerjaannya ke keadaan konsisten, memberi sinyal balik, dan masuk tunggu tak terbatas; DllMain mengonfirmasi keadaan konsisten lalu menghentikan thread
participant D as DllMain (penanganan DETACH)
participant W as Thread worker
D->>W: Sinyal keluar dengan event
W->>W: Rapikan pekerjaan ke keadaan konsisten
W->>D: Sinyal konsistensi selesai dan tunggu selamanya
D->>W: Hentikan dengan TerminateThread
Note over D,W: Tidak menunggu keluar alami, jadi tidak deadlock
Gambar 8: Alih-alih “tunggu keluar alami”, “tunggu sinyal konsistensi lalu potong” menghindari tabrakan dengan loader lock.
Sebagai prinsip pertama, desain paling aman adalah menghindari memiliki thread di DLL yang dapat di-unload, dan menjaga kepemilikan thread di sisi EXE.
DLL_PROCESS_DETACH saat keluar proses adalah kebalikannya: tidak melakukan apa pun dan kembali adalah yang ideal. Pada titik ini setiap thread lain sudah dihentikan secara paksa, dan Anda juga tidak dapat mengandalkan keadaan DLL dependen atau runtime. Pekerjaan rumit di sini hanya menyebabkan deadlock dan crash. Data yang harus dipertahankan sebaiknya ditulis di jalur shutdown aplikasi sendiri; jangan bergantung pada notifikasi ini.3
6. Cara menyelidiki ketika Anda menabraknya
Hang loader-lock punya sidik jari yang dikenali.
Lihat stack di dump hang. Ambil dump dari momen beku dan periksa stack setiap thread. Jika Anda menemukan pasangan thread yang menunggu kunci di dalam fungsi loader ntdll.dll (keluarga yang namanya dimulai dengan Ldr) dan thread yang menunggu sesuatu lain di dalam DllMain atau initializer statis (dynamic initializer), Anda hampir pasti. Thread yang berhenti di tengah panggilan LoadLibrary adalah karakter khas lain.
flowchart TB
accTitle: Sidik jari hang loader-lock
accDescr: Di dump hang, jika Anda menemukan baik thread yang menunggu kunci di dalam fungsi loader ntdll maupun thread yang menunggu sesuatu lain di dalam DllMain atau initializer statis, Anda dapat memperlakukannya sebagai deadlock loader-lock dengan hampir pasti
dump["Dump hang"] --> t1["Thread menunggu kunci di fungsi keluarga Ldr"]
dump --> t2["Thread menunggu di dalam DllMain atau initializer statis"]
t1 --> pair{"Keduanya ada?"}
t2 --> pair
pair -->|"ya"| conf["Hampir pasti deadlock loader-lock"]
pair -->|"tidak"| other["Selidiki sebagai hang jenis lain"]
Gambar 9: Hang loader-lock punya sidik jari yang dikenali “menunggu di Ldr + menunggu di dalam DllMain”.
Curigai karakter “bergantung waktu”. Deadlock loader-lock hanya bertahan pada momen muat DLL bertepatan dengan start atau keluar thread. Kondisi repro seperti “kadang saat start”, “hanya di mesin tertentu”, dan “hanya ketika dijalankan sebagai layanan” adalah tanda masalah jenis ini.
Jalankan pemeriksaan pencegahan. Aktifkan Application Verifier dan jalankan pengujian Anda, dan Anda dapat mendeteksi panggilan berbahaya di dalam DllMain saat runtime.1 Untuk C++/CLI, jangan abaikan peringatan C4747; dalam tinjauan fungsi yang dapat dijangkau dari DllMain, tambahkan sudut “fungsi yang memanggil LoadLibrary secara tidak langsung” (inisialisasi COM, beberapa fitur CRT, import delay-loaded, dan sebagainya) ke daftar periksa tinjauan, dan Anda akan menangkap kecelakaan sebelum mengirimkannya. Panggilan pertama import delay-loaded yang menjadi LoadLibrary secara internal adalah poin yang mudah terlewat.
7. Ringkasan
DllMaindipanggil sambil memegang loader lock (satu per proses, kunci yang menserialisasi setiap notifikasi DLL). Setiap pembatasan mengikuti dari itu.- Inti larangan adalah “jangan panggil
LoadLibrary/FreeLibrary”, “jangan sinkronkan dengan thread lain”, dan “jangan panggil fungsi yang bergantung pada DLL selain Kernel32”. Konstruktor dan destruktor objek statis yang berjalan lewat CRT jatuh di bawah pembatasan yang sama. - Kebijakan desain dasar adalah penundaan. Jadikan statis inisialisasi yang dapat Anda jadikan statis; tunda sisanya ke pemakaian pertama. Pakai
DisableThreadLibraryCallsdan Application Verifier. - Menghentikan thread saat unload mengikuti protokol resmi (sinyal → konfirmasi konsistensi → hentikan). DLL_PROCESS_DETACH saat keluar proses idealnya kosong.
- Di C++/CLI, menjalankan MSIL di bawah loader lock adalah ranjau tersendiri. Bersikeras pada kompilasi native pohon panggilan
DllMain.
Pembatasan DllMain pada awalnya tampak seperti daftar larangan yang tidak masuk akal. Tetapi begitu Anda memegang satu poin bahwa “ia dipanggil sambil memegang loader lock, kunci tingkat atas”, setiap larangan adalah pernyataan ulang prinsip yang sama. Ingat sebagai prinsip dan, ketika Anda menemui kasus tepi yang tidak ada di dokumentasi, Anda seharusnya tetap dapat mengajukan pertanyaan yang tepat: “Apakah ini pekerjaan yang boleh saya lakukan sambil memegang kunci?”
Artikel terkait
- Cara kerja resolusi nama DLL Windows - urutan pencarian dan SxS
- Memanggil DLL native dari C#: wrapper C++/CLI vs P/Invoke
- Praktik terbaik multithreading praktis: edisi C++ — menghilangkan kecelakaan lewat struktur dengan RAII dan jthread
- Spurious wakeup — mengapa condition variable bangun “tanpa diberitahu” dan cara menunggu dengan benar di Windows
- Membaca crash dump dengan WinDbg + SOS — panduan praktis analisis setelah pengumpulan
- Dasar COM STA/MTA - model threading dan cara menghindari hang
Area konsultasi terkait
KomuraSoft LLC menangani investigasi akar hang dan deadlock saat start atau muat DLL (analisis dump), tinjauan desain seputar DllMain dan inisialisasi statis, serta meremediasi wrapper C++/CLI dan DLL plug-in menuju desain inisialisasi yang aman. Anda dapat berkonsultasi bahkan pada tahap yang sulit direproduksi “hanya hang saat start di lingkungan tertentu”.
- Investigasi bug dan akar masalah
- Konsultasi teknis dan tinjauan desain
- Pengembangan aplikasi Windows
- Hubungi kami
Tautan referensi
-
Microsoft Learn, Dynamic-Link Library Best Practices. Tentang DllMain dipanggil sambil loader lock dipegang, sehingga fungsi yang dapat Anda panggil sangat dibatasi; DllMain yang ideal adalah stub kosong dan inisialisasi ditunda sejauh mungkin; rekomendasi inisialisasi statis waktu kompilasi; melakukan hanya minimum untuk kegagalan yang harus dideteksi lebih awal; dan mendeteksi kesalahan DllMain khas dengan Application Verifier. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, DllMain entry point. Tentang melakukan hanya inisialisasi dan terminasi sederhana di entry point; mengapa Anda tidak boleh memanggil LoadLibrary / FreeLibrary (urutan muat sirkular dan pemakaian DLL sebelum inisialisasi atau setelah terminasi); Kernel32.dll dijamin sudah dimuat, sehingga Anda dapat memanggilnya dalam rentang yang tidak memuat DLL lain; tidak adanya daftar lengkap fungsi aman; fungsi User, Shell, dan COM menyebabkan access violation; notifikasi DLL diserialisasi, sehingga komunikasi dengan thread atau proses lain menyebabkan deadlock; dan pembatasan yang sama berlaku untuk konstruktor dan destruktor objek statis ketika CRT ditautkan. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. Tentang struktur yang deadlock jika Anda menunggu thread keluar di dalam DllMain (notifikasi DLL_THREAD_DETACH keluar thread membutuhkan loader lock); protokol untuk menghentikan thread saat unload (sinyal dengan event, konfirmasi keadaan konsisten, lalu hentikan); DLL_PROCESS_DETACH saat keluar proses memiliki thread lain sudah dihentikan secara paksa dan tidak ada jaminan konsistensi ruang alamat, sehingga handler yang ideal kosong; dan membuat thread di DllMain meninggalkan notifikasi terantre dengan inisialisasi tidak lengkap dan menyebabkan masalah. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Initialization of Mixed Assemblies. Tentang tidak mengeksekusi MSIL di bawah loader lock; tidak mengompilasi DllMain dan pohon panggilannya ke MSIL dan menanganinya lewat #pragma unmanaged; peringatan C4747 dikeluarkan ketika DllMain mencoba mengeksekusi MSIL secara langsung, tetapi eksekusi tidak langsung lewat modul lain tidak dapat dideteksi; dan initializer dinamis objek statis dapat menyebabkan masalah yang sama. ↩ ↩2
-
Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). Tentang menonaktifkan notifikasi DLL_THREAD_ATTACH / DLL_THREAD_DETACH untuk mengurangi overhead saat membuat dan menghancurkan thread; tidak memanggilnya dari DLL yang ditautkan dengan CRT statis; dan optimasi tidak dilakukan ketika TLS statis (thread_local atau __declspec(thread)) berlaku. ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. Tentang mendefinisikan hierarki kunci dan selalu memperoleh dalam urutan yang sama; loader memperoleh loader lock sebelum memanggil DllMain, sehingga loader lock harus duduk di puncak hierarki kunci; mengamati urutan perolehan antara API yang mengambil loader lock secara tidak langsung, seperti GetModuleFileName, dan kunci privat; dan contoh konkret deadlock dari inversi urutan kunci. ↩ ↩2
Artikel terkait
Artikel terbaru dengan tag yang sama untuk mendalami topik-topik terdekat.
Win32 Thread Pool API — konkurensi tanpa membuat thread, lewat CreateThreadpoolWork
Apakah Anda menebar panggilan CreateThread di seluruh kode native? Artikel ini menjelaskan Win32 thread pool API yang didesain ulang di V...
Apa sebenarnya "Tidak Merespons" — cara Windows memutuskan aplikasi hang, dan cara merancang aplikasi yang tidak hang
"Tidak Merespons" Windows adalah mekanisme di mana OS menilai bahwa jendela belum mengambil pesan selama 5 detik dan menggantinya dengan ...
Spurious wakeup — mengapa condition variable bangun "tanpa diberitahu" dan cara menunggu dengan benar di Windows
Tunggu condition variable dapat kembali bahkan ketika tidak ada notifikasi yang datang (spurious wakeup). Artikel ini menjelaskan, dari i...
Named pipes dalam praktik — IPC standar Windows dari desain hingga keamanan
Panduan praktis tentang named pipe, komunikasi antarpproses standar di Windows. Artikel ini menata, dari sumber primer, pilihan antara mo...
Aplikasi yang rusak saat bangun dari tidur — cara kerja event daya Windows dan cara membangun aplikasi bisnis yang bertahan
Anda membuka laptop dan koneksi aplikasi bisnis sudah mati — penyebabnya adalah desain yang tidak pernah memperhitungkan tidur. Artikel i...
Topik terkait
Halaman-halaman ini menempatkan topik dalam konteks layanan dan keputusan yang lebih luas.
Topik teknis Windows
Portal tentang pengembangan Windows, investigasi bug, dan pemanfaatan aset yang ada.
Layanan yang terkait dengan topik ini
Artikel ini berkaitan langsung dengan layanan berikut.
Pengembangan aplikasi Windows
Aplikasi bisnis, integrasi perangkat, dan alat komunikasi, dari kebutuhan hingga pengembangan.
Pertanyaan yang sering diajukan
Pertanyaan yang sering muncul dalam konsultasi tentang topik artikel ini.
- Benarkah Anda tidak boleh melakukan apa pun sama sekali di DllMain?
- "Jangan lakukan apa pun" bukan hiperbola; itu adalah sikap desain resmi, dan Microsoft sendiri mengatakan bahwa DllMain yang ideal adalah stub yang hampir kosong. Yang aman adalah subset fungsi Kernel32.dll — Kernel32 dijamin sudah dimuat pada saat DllMain berjalan — dalam rentang yang tidak memuat DLL lain. Membuat critical section atau mutex, dan memakai TLS, adalah contoh yang boleh Anda lakukan. Sebaliknya, LoadLibrary/FreeLibrary, menyinkronkan dengan thread lain, dan memanggil fungsi di User32, Shell, COM, dan sejenisnya dilarang karena menyebabkan deadlock dan access violation. Inisialisasi yang Anda ragu sebaiknya tidak dilakukan di DllMain; tunda sampai pertama kali dipakai.
- Apakah konstruktor global C++ (objek statis) juga jatuh di bawah pembatasan DllMain?
- Ya. Ketika DLL ditautkan dengan CRT (runtime C++), konstruktor dan destruktor objek global dan statis berjalan, lewat entry point yang CRT sediakan, sebagai bagian de facto dari DllMain. Itu berarti memanggil LoadLibrary dari konstruktor, memulai thread lain dan menunggu sampai selesai, menginisialisasi COM, dan sebagainya semuanya membawa bahaya yang sama seperti melakukan hal itu di DllMain. Untuk objek global dengan inisialisasi yang tidak sepele, simpan pointer dan konstruk pada akses pertama, atau pakai static lokal fungsi, agar pekerjaan berjalan di luar DllMain.
- Haruskah saya memanggil DisableThreadLibraryCalls?
- Bersyarat, ya. Jika DLL tidak membutuhkan notifikasi DLL_THREAD_ATTACH/DETACH, memanggil DisableThreadLibraryCalls di DLL_PROCESS_ATTACH menghentikan notifikasi per-pembuatan-thread dan per-keluar-thread dan mengurangi overhead di proses yang sering membuat thread. Ada dua pengecualian. Jangan panggil dari DLL yang ditautkan dengan CRT statis (CRT statis membutuhkan notifikasi thread). Dan jika TLS statis lewat thread_local atau __declspec(thread) berlaku, panggilan itu sendiri gagal dan mengembalikan FALSE, jadi biasakan memeriksa nilai kembalian. Pakai pada DLL tipikal yang memakai CRT tertaut dinamis, setelah Anda mengonfirmasi bahwa tidak ada yang bergantung pada notifikasi thread.
- Mengapa DLL C++/CLI (mixed managed) hang saat start?
- Penyebab khasnya adalah mencoba menjalankan MSIL (kode managed) sementara loader lock dipegang. Di assembly campuran C++/CLI, jika DllMain, fungsi yang dipanggil darinya, atau initializer dinamis global dikompilasi ke MSIL, inisialisasi CLR atau muat assembly lain dapat dibutuhkan di bawah loader lock, dan itu dapat deadlock. Kompiler mengeluarkan peringatan C4747 ketika DllMain sendiri mencoba mengeksekusi MSIL secara langsung, tetapi tidak dapat mendeteksi eksekusi tidak langsung lewat modul lain. Mitigasinya adalah mengompilasi DllMain dan pohon panggilannya sebagai native dengan #pragma unmanaged — atau tidak punya DllMain sama sekali.
- Bolehkah saya membersihkan sumber daya di DLL_PROCESS_DETACH?
- Jawabannya berubah antara "keluar proses" dan "unload lewat FreeLibrary". Pada DLL_PROCESS_DETACH saat keluar proses, thread lain sudah dihentikan, dan tidak ada jaminan bahwa ruang alamat masih konsisten, jadi pembersihan seperti membebaskan memori justru berbahaya; panduan resmi adalah bahwa "handler yang ideal kosong". Tulis data yang harus dipertahankan di jalur shutdown aplikasi sendiri, dan di sini pada dasarnya jangan lakukan apa pun dan kembali. Pada unload lewat FreeLibrary, proses berlanjut, jadi Anda memang perlu pembersihan lengkap — menghentikan thread, menutup handle, dan sebagainya. Menunggu thread keluar di dalam DllMain deadlock, walaupun, jadi Anda harus mengikuti protokol resmi: sinyal, tunggu sampai keadaan konsisten, dan selesaikan pekerjaan di luar DllMain.
Profil penulis
Halaman perkenalan penulis artikel.
Go Komura
Direktur KomuraSoft LLC
Berspesialisasi dalam pengembangan perangkat lunak Windows, konsultasi teknis, dan investigasi bug, terutama pada proyek dengan sistem yang sudah ada dan bug yang sulit direproduksi.