DllMain dan loader lock — alasan sebenarnya Anda diminta "jangan lakukan apa pun di inisialisasi DLL"

· · 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

  • DllMain dipanggil sambil memegang loader lock, kunci bersama yang persis ada satu per proses. Jadi memanggil, dari DllMain, pekerjaan yang mencoba mengambil loader lock (langsung atau tidak langsung) menciptakan kemungkinan deadlock, atau crash dari menyentuh DLL yang belum diinisialisasi.1
  • Memanggil LoadLibrary / FreeLibrary dilarang. 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 DllMain agar 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, DllMain dan 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.

Empat waktu di mana DllMain dipanggilDLL_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 CRTMuat DLLDLL_PROCESS_ATTACHDLL_THREAD_ATTACH (pada setiap start thread)DLL_THREAD_DETACH (pada setiap keluar thread)DLL_PROCESS_DETACH (saat unload atau keluar)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 LoadLibrary karena 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
Mengapa menunggu thread di dalam DllMain deadlockDllMain, 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 deadlockWorkerDllMainLoader(memegang kunci)WorkerDllMainLoader(memegang kunci)Notifikasi keluar membutuhkan kunciDllMain memegang kunci, W menungguDLL_PROCESS_DETACHMinta keluar dan tungguSelesaikan pekerjaan, lalu keluar

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

Inversi urutan kunci antara loader lock dan kunci privatDllMain, 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 deadlockDllMain: memegang loader lockPergi mengambil kunci privat GWorker: memegang kunci privat GPergi mengambil loader lockDeadlock dari urutan perolehan terbalikGetModuleHandle 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.

Jalur di mana menginisialisasi objek global menjadi ranjauLoader 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 DllMainMuat DLL (loader lock diperoleh)Entry point CRTKonstruktor objek globalPekerjaan setara LoadLibraryMemulai thread dan menunggu sampai selesaiMemakai COM atau User32Semua ini jatuh di bawah larangan DllMain

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

Apakah eksekusi MSIL di bawah loader lock dapat dideteksiKode 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 nativePanggilan dari DllMainEksekusi MSIL secara langsungEksekusi lewat modul lainDapat dideteksi dengan peringatan C4747Kompiler tidak dapat mendeteksinyaCegah 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

  1. Selesaikan inisialisasi yang dapat Anda lakukan pada waktu kompilasi (secara statis). Pertama tanya apakah inisialisasi dinamis dapat diganti dengan yang statis.
  2. 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 dari DllMain atau initializer statis, initializer tetap berjalan di bawah loader lock dan Anda kembali di bawah pembatasan yang sama.
  3. 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”.
  4. Pertimbangkan DisableThreadLibraryCalls di 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
  5. Periksa dengan Application Verifier. Banyak panggilan berbahaya di dalam DllMain adalah yang Application Verifier akan deteksi saat runtime.1
Panduan desain untuk inisialisasi DLLPertama 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 muatyatidaktidakyaDapat diputuskan pada waktu kompilasi?Jadikan inisialisasi statisHarus kegagalan dideteksi pada waktu muat?Tunda ke pemakaian pertama (bawaan)Lakukan hanya minimum di DllMainEksklusi 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.

Apakah memanggil DisableThreadLibraryCallsJangan 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 notifikasiyatidakyatidaktidakyaDitautkan dengan CRT statis?Tidak boleh memanggilnyaMemakai TLS statis?Panggilan gagal saja (FALSE)Notifikasi thread dibutuhkan?Panggil di ATTACH (periksa nilai kembalian)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”.

Protokol untuk menghentikan thread saat unloadDllMain 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 threadThread workerDllMain (penanganan DETACH)Thread workerDllMain (penanganan DETACH)Tidak menunggu keluar alami, jadi tidak deadlockSinyal keluar dengan eventRapikan pekerjaan ke keadaan konsistenSinyal konsistensi selesai dan tunggu selamanyaHentikan dengan TerminateThread

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.

Sidik jari hang loader-lockDi 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 pastiyatidakDump hangThread menunggu kunci di fungsi keluarga LdrThread menunggu di dalam DllMain atau initializer statisKeduanya ada?Hampir pasti deadlock loader-lockSelidiki 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

  • DllMain dipanggil 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 DisableThreadLibraryCalls dan 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

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”.

Tautan referensi

  1. 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

  2. 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

  3. 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

  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

  5. 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

  6. 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 terbaru dengan tag yang sama untuk mendalami topik-topik terdekat.

Halaman-halaman ini menempatkan topik dalam konteks layanan dan keputusan yang lebih luas.

Artikel ini berkaitan langsung dengan layanan berikut.

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.

Kembali ke blog