Inisialisasi Rakitan Campuran

Pengembang Windows harus selalu waspada terhadap kunci loader saat menjalankan kode selama DllMain. Namun, ada beberapa masalah tambahan yang perlu dipertimbangkan saat berhadapan dengan rakitan mode campuran C++/CLI.

Kode dalam DllMain tidak boleh mengakses .NET Common Language Runtime (CLR). Itu berarti bahwa DllMain tidak boleh melakukan panggilan ke fungsi terkelola, langsung atau tidak langsung; tidak ada kode terkelola yang boleh dideklarasikan atau diimplementasikan di DllMain; dan tidak ada pengumpulan sampah atau pemuatan pustaka otomatis yang boleh dilakukan dalam DllMain.

Penyebab Penguncian Loader

Dengan pengenalan platform .NET, ada dua mekanisme berbeda untuk memuat modul eksekusi (EXE atau DLL): satu untuk Windows, yang digunakan untuk modul yang tidak dikelola, dan satu untuk CLR, yang memuat rakitan .NET. Masalah utama pemuatan campuran DLL terkait dengan pemuat OS Microsoft Windows.

Ketika rakitan yang hanya berisi konstruksi .NET dimuat ke dalam proses, loader CLR dapat melakukan semua tugas pemuatan dan inisialisasi yang diperlukan itu sendiri. Namun, untuk memuat rakitan campuran yang dapat berisi kode dan data asli, pemuat Windows juga harus digunakan.

Pemuat Windows menjamin bahwa tidak ada kode yang dapat mengakses kode atau data di DLL tersebut sebelum diinisialisasi. Dan memastikan bahwa tidak ada kode yang dapat memuat ulang DLL secara berulang saat proses inisialisasi sebagian. Untuk melakukannya, pemuat Windows menggunakan bagian penting proses-global (sering disebut "kunci pemuat") yang mencegah akses tidak aman selama inisialisasi modul. Akibatnya, proses pemuatan rentan terhadap banyak skenario kebuntuan klasik. Untuk rakitan campuran, dua skenario berikut meningkatkan risiko kebuntuan:

  • Pertama, jika pengguna mencoba menjalankan fungsi yang dikompilasi ke bahasa perantara Microsoft (MSIL) ketika kunci pemuat ditahan (dari DllMain atau di inisialisasi statis, misalnya), itu dapat menyebabkan kebuntuan. Pertimbangkan kasus di mana fungsi MSIL mereferensikan tipe dalam suatu assembly yang belum dimuat. CLR akan mencoba untuk secara otomatis memuat rakitan tersebut, yang mungkin mengharuskan pemuat Windows memblokir pada kunci pemuat. Kebuntuan terjadi karena kunci pemuat sudah dipegang oleh kode sebelumnya dalam urutan panggilan. Namun, menjalankan MSIL di bawah kunci loader tidak menjamin bahwa kebuntuan akan terjadi. Itulah yang membuat skenario ini sulit didiagnosis dan diperbaiki. Dalam beberapa keadaan, seperti ketika DLL dari jenis yang dirujuk tidak berisi konstruksi asli dan semua dependensinya tidak berisi konstruksi asli, pemuat Windows tidak diperlukan untuk memuat rakitan .NET dari jenis yang dirujuk. Selain itu, rakitan yang dibutuhkan atau dependensi native/.NET campurannya mungkin sudah dimuat oleh kode lain. Akibatnya, kebuntuan dapat sulit diprediksi, dan dapat bervariasi tergantung pada konfigurasi komputer target.

  • Kedua, saat memuat DLL dalam versi 1.0 dan 1.1 dari .NET Framework, CLR mengasumsikan bahwa kunci loader tidak ditahan, dan mengambil beberapa tindakan yang tidak valid di bawah kunci loader. Dengan asumsi bahwa kunci loader tidak ditahan adalah asumsi yang valid untuk DLL .NET murni. Namun, karena DLL campuran menjalankan rutinitas inisialisasi asli, mereka memerlukan loader Windows asli, dan akibatnya terjadi penguncian loader. Jadi, bahkan jika pengembang tidak mencoba menjalankan fungsi MSIL apa pun selama inisialisasi DLL, masih ada kemungkinan kecil dari kebuntuan nondeterministik dalam .NET Framework versi 1.0 dan 1.1.

Semua non-determinisme telah dihapus dari proses pemuatan DLL campuran. Itu dicapai dengan perubahan ini:

  • CLR tidak lagi membuat asumsi palsu saat memuat DLL campuran.

  • Inisialisasi yang tidak dikelola dan terkelola dilakukan dalam dua tahap terpisah dan berbeda. Inisialisasi yang tidak dikelola berlangsung terlebih dahulu (melalui DllMain), dan inisialisasi terkelola terjadi setelahnya, melalui konstruksi yang didukung .NET .cctor. Yang terakhir sepenuhnya transparan untuk pengguna kecuali /Zl atau /NODEFAULTLIB digunakan. Untuk informasi selengkapnya, lihat/NODEFAULTLIB (Abaikan Pustaka) dan /Zl (Hilangkan Nama Pustaka Default).

Kunci loader masih dapat terjadi, tetapi sekarang terjadi secara konsisten dan terdeteksi. Jika DllMain berisi instruksi MSIL, pengkompilasi menghasilkan peringatan Compiler Warning (tingkat 1) C4747. Selain itu, CRT atau CLR akan mencoba mendeteksi dan melaporkan upaya untuk menjalankan MSIL di bawah kunci loader. Deteksi CRT menghasilkan diagnostik runtime C Run-Time Error R6033.

Sisa artikel ini menjelaskan skenario lain di mana MSIL dapat dijalankan di bawah kunci pemuatan. Ini menunjukkan cara mengatasi masalah di bawah setiap skenario tersebut, dan teknik debugging.

Skenario dan Solusi

Ada beberapa situasi yang berbeda-beda di mana kode pengguna dapat menjalankan MSIL saat pemuat terkunci. Pengembang harus memastikan bahwa implementasi kode pengguna tidak mencoba menjalankan instruksi MSIL dalam setiap keadaan ini. Subbagian berikut menjelaskan semua kemungkinan dengan diskusi tentang cara menyelesaikan masalah dalam kasus yang paling umum.

DllMain

Fungsi DllMain adalah titik masuk yang didefinisikan oleh pengguna untuk DLL. Kecuali pengguna menentukan sebaliknya, DllMain dipanggil setiap kali proses atau utas menempel pada atau terlepas dari DLL yang berisikan. Karena pemanggilan ini dapat terjadi saat kunci pemuat ditahan, tidak ada fungsi yang disediakan DllMain pengguna yang harus dikompilasi ke MSIL. Selain itu, tidak ada fungsi dalam pohon panggilan yang berakar di DllMain yang dapat dikompilasi ke MSIL. Untuk mengatasi masalah di sini, blok kode yang menentukan DllMain harus dimodifikasi dengan #pragma unmanaged. Hal yang sama harus dilakukan untuk setiap fungsi yang DllMain memanggil.

Dalam kasus di mana fungsi-fungsi ini harus memanggil fungsi yang memerlukan implementasi MSIL untuk konteks panggilan lain, Anda dapat menggunakan strategi duplikasi di mana .NET dan versi asli dari fungsi yang sama dibuat.

Alternatifnya, jika DllMain tidak diperlukan atau jika tidak perlu dijalankan di bawah kunci loader, Anda dapat menghapus implementasi DllMain yang disediakan pengguna, yang menghilangkan masalah.

Jika DllMain mencoba menjalankan MSIL secara langsung, Compiler Warning (level 1) C4747 akan menyebabkan. Namun, pengkompilasi tidak dapat mendeteksi kasus di mana DllMain memanggil fungsi di modul lain yang pada gilirannya mencoba menjalankan MSIL.

Untuk informasi selengkapnya tentang skenario ini, lihat Penghambat Diagnosis.

Menginisialisasi Objek Statis

Menginisialisasi objek statis dapat mengakibatkan kebuntuan jika inisialisasi dinamis diperlukan. Kasus sederhana (seperti ketika Anda menetapkan nilai yang diketahui pada waktu kompilasi ke variabel statis) tidak memerlukan inisialisasi dinamis, sehingga tidak ada risiko kebuntuan. Namun, beberapa variabel statis diinisialisasi oleh panggilan fungsi, pemanggilan konstruktor, atau ekspresi yang tidak dapat dievaluasi pada waktu kompilasi. Semua variabel ini memerlukan kode untuk dijalankan selama inisialisasi modul.

Kode di bawah ini menunjukkan contoh penginisialisasi statis yang memerlukan inisialisasi dinamis: panggilan fungsi, konstruksi objek, dan inisialisasi pointer. (Contoh-contoh ini tidak statis, tetapi diasumsikan memiliki definisi dalam cakupan global, yang memiliki efek yang sama.)

// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);
CObject* op = new CObject(arg1, arg2);

Risiko kebuntuan ini tergantung pada apakah modul yang berisi dikompilasi dengan /clr dan apakah MSIL akan dijalankan. Secara khusus, jika variabel statis dikompilasi tanpa /clr (atau berada dalam #pragma unmanaged blok), dan inisialisasi dinamis yang diperlukan untuk menginisialisasinya menghasilkan eksekusi instruksi MSIL, kebuntuan dapat terjadi. Pasalnya, untuk modul yang dikompilasi tanpa /clr, inisialisasi variabel statis dilakukan oleh DllMain. Sebaliknya, variabel statis yang dikompilasi dengan /clr diinisialisasi oleh .cctor, setelah tahap inisialisasi yang tidak dikelola selesai dan kunci pemuat telah dirilis.

Ada sejumlah solusi untuk kebuntuan yang disebabkan oleh inisialisasi dinamis variabel statis. Mereka diatur di sini kira-kira dalam urutan waktu yang diperlukan untuk memperbaiki masalah:

  • File sumber yang berisi variabel statis dapat dikompilasi dengan /clr.

  • Semua fungsi yang dipanggil oleh variabel statis dapat dikompilasi ke kode asli menggunakan direktif #pragma unmanaged .

  • Mengkloning secara manual kode yang variabel statisnya bergantung, dengan menyediakan versi .NET dan versi asli dengan nama yang berbeda. Pengembang kemudian dapat memanggil versi asli dari penginisialisasi statis asli dan memanggil versi .NET di tempat lain.

Fungsi yang Disediakan Pengguna Memengaruhi Startup

Ada beberapa fungsi yang disediakan oleh pengguna di mana pustaka mengandalkan untuk inisialisasi selama proses startup. Misalnya, ketika operator di C++ seperti new dan delete di-overload secara global, versi yang ditentukan pengguna digunakan di mana-mana, termasuk dalam inisialisasi dan penghancuran Pustaka Standar C++. Akibatnya, Pustaka Standar C++ dan penginisialisasi statis yang disediakan pengguna akan memanggil versi operator yang disediakan pengguna.

Jika versi yang disediakan pengguna dikompilasi ke MSIL, penginisialisasi ini akan mencoba menjalankan instruksi MSIL saat kunci pemuat ditahan. Objek malloc yang disediakan pengguna memiliki konsekuensi yang sama. Untuk mengatasi masalah ini, salah satu kelebihan beban atau definisi yang disediakan pengguna ini harus diimplementasikan sebagai kode asli menggunakan arahan #pragma unmanaged .

Untuk informasi selengkapnya tentang skenario ini, lihat Penghambat Diagnosis.

Lokal Kustom

Jika pengguna menyediakan lokal global kustom, lokal ini akan terbiasa untuk menginisialisasi semua aliran I/O di masa mendatang, termasuk aliran yang diinisialisasi secara statis. Jika objek lokal global ini dikompilasi ke MSIL, maka fungsi anggota objek lokal yang dikompilasi ke MSIL dapat dipanggil saat kunci loader ditahan.

Ada tiga opsi untuk memecahkan masalah ini:

File sumber yang berisi semua definisi aliran I/O global dapat dikompilasi menggunakan /clr opsi . Ini mencegah penginisialisasi statis mereka dieksekusi saat kunci pemuat aktif.

Definisi fungsi lokal kustom dapat dikompilasi ke kode asli dengan menggunakan direktif #pragma unmanaged .

Jangan menetapkan lokalisasi kustom sebagai lokalisasi global sampai setelah kunci pemuat dilepaskan. Kemudian secara eksplisit mengonfigurasi aliran I/O yang dibuat selama inisialisasi dengan lokal kustom.

Impedimen terhadap Diagnosis

Dalam beberapa kasus, sulit untuk mendeteksi sumber kebuntuan. Sub bagian berikut membahas skenario dan cara untuk mengatasi masalah ini.

Implementasi pada Header

Dalam kasus tertentu, implementasi fungsi di dalam file header dapat mempersulit diagnosis. Fungsi sebaris dan kode templat keduanya mengharuskan fungsi ditentukan dalam file header. Bahasa C++ menentukan Aturan Satu Definisi, yang memaksa semua implementasi fungsi dengan nama yang sama setara secara semantik. Akibatnya, linker C++ tidak perlu membuat pertimbangan khusus saat menggabungkan file objek yang memiliki implementasi duplikat dari fungsi tertentu.

Di versi Visual Studio sebelum Visual Studio 2005, linker hanya memilih definisi terbesar yang setara secara semantik ini. Ini dilakukan untuk mengakomodasi deklarasi penerusan dan skenario ketika opsi pengoptimalan yang berbeda digunakan untuk berkas sumber yang berbeda. Ini menciptakan masalah untuk DLL campuran asli dan .NET.

Karena header yang sama dapat disertakan oleh file C++ yang memiliki /clr diaktifkan maupun dinonaktifkan, atau jika #include dibungkus di dalam blok #pragma unmanaged, dimungkinkan untuk memiliki versi MSIL sekaligus versi asli dari fungsi yang menyediakan implementasi dalam header. MSIL dan implementasi asli memiliki semantik yang berbeda untuk inisialisasi di bawah kunci loader, yang secara efektif melanggar satu aturan definisi. Akibatnya, ketika linker memilih versi terbesar dari implementasi, ia dapat memilih versi MSIL dari suatu fungsi, bahkan jika fungsi tersebut secara eksplisit disusun ke kode asli di bagian lain menggunakan direktif #pragma unmanaged. Untuk memastikan bahwa versi MSIL dari templat atau fungsi sebaris tidak pernah dipanggil saat terjebak dalam loader lock, setiap definisi dari fungsi tersebut yang dipanggil dalam situasi ini harus dimodifikasi dengan direktif #pragma unmanaged. Jika file header berasal dari pihak ketiga, cara termudah untuk melakukan perubahan ini adalah dengan push dan pop arahan #pragma unmanaged di sekitar arahan #include untuk file header tersebut. (Lihat terkelola, tidak dikelola untuk contoh.) Namun, strategi ini tidak berfungsi untuk header yang berisi kode lain yang harus langsung memanggil API .NET.

Untuk memudahkan pengguna yang menangani penguncian pemuat, linker akan memilih implementasi asli daripada yang dikelola ketika keduanya disajikan. Pengaturan default ini menghindari masalah di atas. Namun, ada dua pengecualian untuk aturan ini dalam rilis ini karena dua masalah yang belum terselesaikan dengan kompilator:

  • Panggilan ke fungsi sebaris adalah melalui penunjuk fungsi statis global. Skenario ini tidak memungkinkan karena fungsi virtual dipanggil melalui penunjuk fungsi global. Contohnya,
#include "definesmyObject.h"
#include "definesclassC.h"

typedef void (*function_pointer_t)();

function_pointer_t myObject_p = &myObject;

#pragma unmanaged
void DuringLoaderlock(C & c)
{
    // Either of these calls could resolve to a managed implementation,
    // at link-time, even if a native implementation also exists.
    c.VirtualMember();
    myObject_p();
}

Mendiagnosis dalam Mode Debug

Semua diagnosis masalah penguncian loader harus dilakukan dengan build Debug. Build rilis tidak selalu menghasilkan pesan diagnostik. Dan, pengoptimalan yang dilakukan dalam mode Rilis dapat menutupi beberapa MSIL pada skenario penguncian pemuat.

Cara memecahkan masalah kunci pemuat

Diagnostik yang dihasilkan CLR ketika fungsi MSIL dipanggil menyebabkan CLR menghentikan sementara eksekusi. Sebagai akibatnya, debugger mode campuran Visual C++ ikut ditangguhkan saat menjalankan aplikasi debug secara in-process. Namun, saat melampirkan ke proses, tidak mungkin mendapatkan stack panggilan terkelola untuk debuggee menggunakan debugger campuran.

Untuk mengidentifikasi fungsi MSIL spesifik yang dipanggil saat loader lock aktif, pengembang harus mengikuti langkah-langkah berikut:

  1. Pastikan simbol untuk mscoree.dll dan mscorwks.dll tersedia.

    Anda dapat membuat simbol tersedia dengan dua cara. Pertama, PDB untuk mscoree.dll dan mscorwks.dll dapat ditambahkan ke jalur pencarian simbol. Untuk menambahkannya, buka dialog opsi jalur pencarian simbol. (Dari Menu alat , pilih Opsi. Di panel kiri kotak dialog Opsi , buka simpul Penelusuran Kesalahan dan pilih Simbol.) Tambahkan jalur ke file PDB mscoree.dll dan mscorwks.dll ke daftar pencarian. PDB ini diinstal ke simbol %VSINSTALLDIR%\SDK\v2.0\. Pilih OK.

    Kedua, PDB untuk mscoree.dll dan mscorwks.dll dapat diunduh dari Microsoft Symbol Server. Untuk mengonfigurasi Server Simbol, buka dialog opsi jalur pencarian simbol. (Dari Menu alat , pilih Opsi. Di panel kiri kotak dialog Opsi , buka simpul Penelusuran Kesalahan dan pilih Simbol.) Tambahkan jalur pencarian ini ke daftar pencarian: https://msdl.microsoft.com/download/symbols. Tambahkan direktori singgahan simbol ke kotak teks cache server simbol. Pilih OK.

  2. Atur mode debugger ke mode khusus asli.

    Buka grid Properti untuk proyek yang diinisiasi dalam solusi. Pilih Properti Konfigurasi>Debug. Atur properti Jenis Debugger ke Native-Only.

  3. Mulai debugger (F5).

  4. Saat /clr diagnostik dihasilkan, pilih Coba Lagi lalu pilih Berhenti.

  5. Buka jendela tumpukan panggilan. (Pada bilah menu, pilih Debug>Windows>Call Stack.) Elemen atau penginisialisasi statis yang menyinggung diidentifikasi dengan panah hijau. Jika fungsi yang menyinggung tidak diidentifikasi, langkah-langkah berikut harus diambil untuk menemukannya.

  6. Buka jendela Langsung (Pada bilah menu, pilih Debug>Windows>Immediate.)

  7. Masukkan .load sos.dll ke jendela Segera untuk memuat layanan penelusuran kesalahan SOS.

  8. Masukkan !dumpstack ke jendela Segera untuk mendapatkan daftar lengkap tumpukan internal /clr .

  9. Cari instans pertama (paling dekat dengan bagian bawah tumpukan) dari _CorDllMain (jika DllMain menyebabkan masalah) atau _VTableBootstrapThunkInitHelperStub atau GetTargetForVTableEntry (jika inisialisasi statis menyebabkan masalah). Entri tumpukan yang berada tepat di bawah panggilan ini merupakan pemanggilan fungsi yang diimplementasikan MSIL yang mencoba dijalankan di bawah "kunci pemuat".

  10. Buka file sumber dan nomor baris yang diidentifikasi pada langkah sebelumnya dan perbaik masalah menggunakan skenario dan solusi yang dijelaskan di bagian Skenario.

Contoh

Deskripsi

Sampel berikut menunjukkan cara menghindari penguncian pemuat dengan memindahkan kode dari DllMain ke dalam konstruktor objek global.

Dalam sampel ini, ada objek terkelola global yang konstruktornya berisi objek terkelola yang awalnya ada di DllMain. Bagian kedua dari sampel ini mereferensikan perakitan, membuat instans objek terkelola untuk memanggil konstruktor modul yang melakukan inisialisasi.

Kode

// initializing_mixed_assemblies.cpp
// compile with: /clr /LD
#pragma once
#include <stdio.h>
#include <windows.h>
struct __declspec(dllexport) A {
   A() {
      System::Console::WriteLine("Module ctor initializing based on global instance of class.\n");
   }

   void Test() {
      printf_s("Test called so linker doesn't throw away unused object.\n");
   }
};

#pragma unmanaged
// Global instance of object
A obj;

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
   // Remove all managed code from here and put it in constructor of A.
   return true;
}

Contoh ini menunjukkan masalah dalam inisialisasi rakitan campuran:

// initializing_mixed_assemblies_2.cpp
// compile with: /clr initializing_mixed_assemblies.lib
#include <windows.h>
using namespace System;
#include <stdio.h>
#using "initializing_mixed_assemblies.dll"
struct __declspec(dllimport) A {
   void Test();
};

int main() {
   A obj;
   obj.Test();
}

Kode ini menghasilkan output berikut:

Module ctor initializing based on global instance of class.

Test called so linker doesn't throw away unused object.

Lihat juga

Rakitan Campuran (Asli dan Terkelola)