Bagikan melalui


Praktik Terbaik Pustaka Dynamic-Link

**Diperbarui:**

  • 17 Mei 2006

API Penting

Membuat DLL menghadirkan sejumlah tantangan bagi pengembang. DLL tidak memiliki pengaturan versi yang diberlakukan oleh sistem. Ketika beberapa versi DLL ada pada sistem, kemudahan untuk ditimpa ditambah dengan kurangnya skema versi menciptakan dependensi dan konflik API. Kompleksitas di lingkungan pengembangan, implementasi loader, dan dependensi DLL telah menciptakan kerapuhan dalam urutan beban dan perilaku aplikasi. Terakhir, banyak aplikasi mengandalkan DLL dan memiliki serangkaian dependensi kompleks yang harus dihormati agar aplikasi berfungsi dengan baik. Dokumen ini menyediakan panduan bagi pengembang DLL untuk membantu membangun DLL yang lebih kuat, portabel, dan dapat diperluas.

Sinkronisasi yang tidak tepat dalamDllMaindapat menyebabkan aplikasi mengalami kebuntuan atau mengakses data atau kode dalam DLL yang tidak diinisialisasi. Memanggil fungsi tertentu dari dalam DllMain menyebabkan masalah tersebut.

apa yang terjadi saat pustaka dimuat

Praktik Terbaik Umum

DllMain dipanggil saat kunci pemuat ditahan. Oleh karena itu, pembatasan signifikan diberlakukan pada fungsi yang dapat dipanggil dalam DllMain. Dengan demikian, DllMain dirancang untuk melakukan tugas inisialisasi minimal, dengan menggunakan subset kecil Microsoft® Windows® API. Anda tidak dapat memanggil fungsi apa pun di DllMain yang secara langsung atau tidak langsung mencoba memperoleh kunci loader. Jika tidak, Anda akan memperkenalkan kemungkinan aplikasi Anda mengalami kebuntuan atau crash. Kesalahan dalam implementasi DllMain dapat membahayakan seluruh proses dan semua thread-nya.

DllMain yang ideal seharusnya hanya berupa stub kosong. Namun, mengingat kompleksitas banyak aplikasi, ini umumnya terlalu ketat. Aturan praktis yang baik untuk DllMain adalah menunda inisialisasi sebanyak mungkin. Inisialisasi malas meningkatkan ketahanan aplikasi karena inisialisasi ini tidak dilakukan saat kunci loader ditahan. Selain itu, inisialisasi malas memungkinkan Anda menggunakan lebih banyak API Windows dengan aman.

Beberapa tugas inisialisasi tidak dapat ditunda. Misalnya, DLL yang bergantung pada file konfigurasi harus gagal dimuat jika file salah bentuk atau berisi sampah. Untuk jenis inisialisasi ini, DLL harus mencoba tindakan dan gagal dengan cepat daripada membuang sumber daya dengan menyelesaikan pekerjaan lain.

Anda tidak boleh melakukan tugas berikut dari dalam DllMain:

  • Panggil LoadLibrary atau LoadLibraryEx (baik secara langsung maupun tidak langsung). Ini dapat menyebabkan kebuntuan atau kerusakan sistem.
  • Panggil GetStringTypeA, GetStringTypeEx, atau GetStringTypeW (baik secara langsung maupun tidak langsung). Ini dapat menyebabkan kebuntuan atau kerusakan.
  • Sinkronkan dengan utas lain. Ini dapat menyebabkan kebuntuan.
  • Dapatkan objek sinkronisasi yang dimiliki oleh kode yang menunggu untuk memperoleh kunci loader. Ini dapat menyebabkan kebuntuan.
  • Inisialisasi utas COM dengan menggunakan CoInitializeEx. Dalam kondisi tertentu, fungsi ini dapat memanggil LoadLibraryEx.
  • Panggil fungsi registri.
  • Panggil CreateProcess. Pembuatan proses memungkinkan pemuatan DLL lainnya.
  • Panggil ExitThread. Keluar dari thread selama pencopotan DLL dapat menyebabkan kunci loader diperoleh kembali, yang dapat mengakibatkan kebuntuan atau kerusakan.
  • Panggil CreateThread. Membuat utas dapat berfungsi jika Anda tidak menyinkronkan dengan utas lain, tetapi berisiko.
  • Panggil ShGetFolderPathW. Memanggil API folder shell/known dapat mengakibatkan sinkronisasi utas, dan karenanya dapat menyebabkan kebuntuan.
  • Buat pipa bernama atau objek bernama lainnya (hanya Windows 2000). Di Windows 2000, objek bernama disediakan oleh DLL Layanan Terminal. Jika DLL ini tidak diinisialisasi, panggilan ke DLL dapat menyebabkan proses crash.
  • Gunakan fungsi manajemen memori dari C Run-Time dinamis (CRT). Jika DLL CRT tidak diinisialisasi, panggilan ke fungsi-fungsi ini dapat menyebabkan proses crash.
  • Fungsi panggilan di User32.dll atau Gdi32.dll. Beberapa fungsi memuat DLL lain, yang mungkin tidak diinisialisasi.
  • Gunakan kode terkelola.

Tugas-tugas berikut aman untuk dilakukan dalam DllMain:

  • Menginisialisasi struktur dan anggota data statis pada waktu kompilasi.
  • Membuat dan menginisialisasi objek sinkronisasi.
  • Alokasikan memori dan inisialisasi struktur data dinamis (hindari fungsi yang tercantum di atas.)
  • Siapkan penyimpanan lokal utas (TLS).
  • Buka, baca dari, dan tulis ke file/berkas.
  • Fungsi panggilan dalam Kernel32.dll (kecuali fungsi yang tercantum di atas).
  • Atur pointer global ke NULL, menunda inisialisasi anggota dinamis. Di Microsoft Windows Vista™, Anda dapat menggunakan fungsi inisialisasi satu kali untuk memastikan bahwa blok kode dijalankan hanya sekali di lingkungan multithreaded.

Deadlock Yang Disebabkan oleh Inversi Urutan Penguncian

Saat Anda menerapkan kode yang menggunakan beberapa objek sinkronisasi seperti kunci, sangat penting untuk menghormati urutan kunci. Ketika perlu untuk memperoleh lebih dari satu kunci pada satu waktu, Anda harus menentukan prioritas eksplisit yang disebut hierarki kunci atau urutan kunci. Misalnya, jika kunci A diperoleh sebelum kunci B di suatu tempat dalam kode, dan kunci B diperoleh sebelum kunci C di tempat lain dalam kode, maka urutan kunci adalah A, B, C dan pesanan ini harus diikuti di seluruh kode. Inversi urutan kunci terjadi ketika urutan penguncian tidak diikuti—misalnya, jika kunci B diperoleh sebelum kunci A. Inversi urutan kunci dapat menyebabkan kebuntuan yang sulit di-debug. Untuk menghindari masalah tersebut, semua utas harus memperoleh kunci dalam urutan yang sama.

Penting untuk dicatat bahwa loader memanggil DllMain saat kunci pemuat sudah diperoleh, sehingga kunci pemuat harus memiliki prioritas tertinggi dalam hierarki penguncian. Perhatikan juga bahwa kode hanya harus memperoleh kunci yang diperlukan untuk sinkronisasi yang tepat; tidak harus memperoleh setiap kunci tunggal yang didefinisikan dalam hierarki. Misalnya, jika bagian kode hanya memerlukan kunci A dan C untuk sinkronisasi yang tepat, maka kode harus memperoleh kunci A sebelum memperoleh kunci C; tidak perlu kode juga memperoleh kunci B. Selain itu, kode DLL tidak dapat secara eksplisit memperoleh kunci loader. Jika kode harus memanggil API seperti GetModuleFileName yang secara tidak langsung dapat memperoleh kunci loader dan kode juga harus memperoleh kunci privat, maka kode harus memanggil GetModuleFileName sebelum memperoleh kunci P, sehingga memastikan bahwa urutan beban dihormati.

Gambar 2 adalah contoh yang menggambarkan inversi urutan kunci. Pertimbangkan DLL yang utas utamanya berisi DllMain. Pemuat pustaka memperoleh kunci pemuat L dan kemudian memanggil DllMain. Utas utama membuat objek sinkronisasi A, B, dan G untuk menserialisasikan akses ke struktur datanya dan kemudian mencoba memperoleh kunci G. Utas pekerja yang telah berhasil memperoleh kunci G kemudian memanggil fungsi seperti GetModuleHandle yang mencoba memperoleh kunci loader L. Dengan demikian, utas pekerja diblokir pada L dan utas utama diblokir pada G, mengakibatkan kebuntuan.

kebuntuan yang disebabkan oleh pembalikan urutan kunci

Untuk mencegah kebuntuan yang disebabkan oleh inversi urutan kunci, semua utas harus mencoba memperoleh objek sinkronisasi dalam urutan beban yang ditentukan setiap saat.

Praktik Terbaik untuk Sinkronisasi

Pertimbangkan sebuah DLL yang menciptakan thread pekerja sebagai bagian dari inisialisasinya. Setelah pembersihan DLL, perlu disinkronkan dengan semua utas pekerja untuk memastikan bahwa struktur data dalam keadaan konsisten dan kemudian mengakhiri utas pekerja. Saat ini, tidak ada cara mudah untuk menyelesaikan masalah sinkronisasi dan mematikan DLL secara bersih di lingkungan multithreaded. Bagian ini menjelaskan praktik terbaik terkini untuk sinkronisasi utas selama penonaktifan DLL.

Sinkronisasi Utas di DllMain selama Penghentian Proses

  • Pada saat DllMain dipanggil pada proses keluar, semua utas proses telah dibersihkan secara paksa dan ada kemungkinan bahwa ruang alamat tidak konsisten. Sinkronisasi tidak diperlukan dalam kasus ini. Dengan kata lain, handler DLL_PROCESS_DETACH yang ideal kosong.
  • Windows Vista memastikan bahwa struktur data inti (variabel lingkungan, direktori saat ini, timbunan proses, dan sebagainya) dalam keadaan konsisten. Namun, struktur data lain dapat rusak, sehingga membersihkan memori tidak aman.
  • Status persisten yang perlu disimpan harus dibersihkan ke penyimpanan permanen.

Sinkronisasi Thread di DllMain untuk DLL_THREAD_DETACH selama DLL Unload

  • Ketika DLL dibongkar, ruang alamat tidak dibuang. Oleh karena itu, DLL diharapkan untuk melakukan penutupan yang bersih. Ini termasuk sinkronisasi utas, pegangan terbuka, kondisi persisten, dan sumber daya yang dialokasikan.
  • Sinkronisasi utas sulit karena menunggu utas keluar di DllMain dapat menyebabkan deadlock. Misalnya, DLL A menahan kunci pemuatan. Ini memberi sinyal pada utas T untuk keluar dan menunggu sampai keluar. Thread T keluar dan pemuat mencoba memperoleh kunci pemuat untuk memanggil DllMain dari DLL A dengan DLL_THREAD_DETACH. Ini menyebabkan kebuntuan. Untuk meminimalkan risiko kebuntuan:
    • DLL A mendapatkan pesan DLL_THREAD_DETACH di DllMain dan mengatur sebuah kejadian untuk utas T, memberi sinyal untuk keluar.
    • Thread T menyelesaikan tugasnya saat ini, membawa dirinya ke keadaan yang konsisten, memberi sinyal DLL A, dan menunggu tanpa batas waktu. Perhatikan bahwa rutinitas pemeriksaan konsistensi harus mengikuti batasan yang sama seperti DllMain untuk menghindari kebuntuan.
    • DLL A mengakhiri T, mengetahui bahwa itu dalam kondisi konsisten.

Jika DLL dibongkar setelah semua utasnya dibuat, tetapi sebelum mulai dieksekusi, utas bisa mengalami crash. Jika DLL membuat utas di DllMain sebagai bagian dari inisialisasinya, beberapa utas mungkin belum selesai inisialisasi dan pesan DLL_THREAD_ATTACH mereka masih menunggu untuk dikirimkan ke DLL. Dalam situasi ini, jika DLL dibongkar, ia akan mulai menghentikan utas. Namun, beberapa utas dapat diblokir di belakang pengunci pemuat. Pesan DLL_THREAD_ATTACH mereka diproses setelah DLL dilepaskan dari pemetaan, menyebabkan prosesnya macet.

Rekomendasi

Berikut ini adalah panduan yang direkomendasikan:

  • Gunakan Pemverifikasi Aplikasi untuk menangkap kesalahan paling umum dalam DllMain.
  • Jika menggunakan kunci pribadi di dalam DllMain, tentukan hierarki pengaturan kunci dan gunakan secara konsisten. Kunci loader harus berada di bagian bawah hierarki ini.
  • Verifikasi bahwa tidak ada panggilan yang bergantung pada DLL lain yang mungkin belum dimuat sepenuhnya.
  • Lakukan inisialisasi sederhana secara statis pada waktu kompilasi, bukan di DllMain.
  • Tunda panggilan apa pun di DllMain yang dapat menunggu hingga nanti.
  • Tangguhkan tugas inisialisasi yang dapat menunggu hingga nanti. Kondisi kesalahan tertentu harus dideteksi lebih awal sehingga aplikasi dapat menangani kesalahan dengan anggun. Namun, ada tradeoff antara deteksi dini ini dan hilangnya ketahanan yang dapat diakibatkan olehnya. Menunda inisialisasi sering kali merupakan yang terbaik.