Bagikan melalui


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 tidak boleh melakukan panggilan ke fungsi terkelola, secara langsung atau tidak langsung; tidak ada kode terkelola yang DllMain harus dideklarasikan atau diimplementasikan di DllMain; dan tidak ada pengumpulan sampah atau pemuatan pustaka otomatis yang harus 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. Pusat masalah pemuatan DLL campuran di sekitar 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 DLL secara berlebihan saat sebagian diinisialisasi. 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 jenis dalam rakitan 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 loader 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 diperlukan atau dependensi native/.NET campurannya mungkin telah 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. Tetapi karena DLL campuran menjalankan rutinitas inisialisasi asli, DLL memerlukan pemuat Windows asli, dan akibatnya kunci pemuat. 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 dikelola 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 .cctor NET. 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 reproduksi, 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 yang tersisa yang dapat dijalankan MSIL di bawah kunci pemuat. Ini menunjukkan cara mengatasi masalah di bawah setiap skenario tersebut, dan teknik penelusuran kesalahan.

Skenario dan Solusi

Ada beberapa situasi berbeda di mana kode pengguna dapat menjalankan MSIL di bawah kunci pemuat. 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 ini DllMain adalah titik masuk yang ditentukan pengguna untuk DLL. Kecuali pengguna menentukan sebaliknya, DllMain dipanggil setiap kali proses atau utas dilampirkan atau terlepas dari DLL yang berisi. 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 dapat dikompilasi DllMain 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.

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

Jika DllMain upaya untuk menjalankan MSIL secara langsung, Compiler Warning (level 1) C4747 akan menghasilkan. 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 .

  • Kloning kode yang bergantung pada variabel statis secara manual, menyediakan .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 pengguna di mana pustaka bergantung pada inisialisasi selama startup. Misalnya, ketika operator kelebihan beban global di C++ seperti new operator dan delete , versi yang disediakan 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. Pengguna yang disediakan malloc 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 dijalankan di bawah kunci loader.

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

Menahan diri dari pengaturan lokal kustom sebagai lokal global hingga 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 di 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 file sumber yang berbeda. Ini menciptakan masalah untuk DLL asli campuran dan .NET.

Karena header yang sama dapat disertakan baik oleh file C++ dengan /clr diaktifkan dan dinonaktifkan, atau #include dapat dibungkus di dalam #pragma unmanaged blok, dimungkinkan untuk memiliki MSIL dan versi asli 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 implementasi terbesar, ia dapat memilih versi MSIL dari fungsi, bahkan jika secara eksplisit dikompilasi ke kode asli di tempat lain menggunakan #pragma unmanaged direktif. Untuk memastikan bahwa versi MSIL dari templat atau fungsi sebaris tidak pernah dipanggil di bawah kunci pemuat, setiap definisi dari setiap fungsi tersebut yang disebut di bawah kunci pemuat harus dimodifikasi dengan direktif #pragma unmanaged . Jika file header berasal dari pihak ketiga, cara term mudah untuk melakukan perubahan ini adalah dengan mendorong dan memunculkan #pragma unmanaged arahan di sekitar arahan #include untuk file header yang menyinggung. (Lihat terkelola, tidak dikelola untuk contoh.) Namun, strategi ini tidak berfungsi untuk header yang berisi kode lain yang harus langsung memanggil API .NET.

Sebagai kenyamanan bagi pengguna yang berurusan dengan kunci loader, linker akan memilih implementasi asli daripada yang dikelola ketika disajikan dengan keduanya. 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 bukan tabel 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 mungkin tidak menghasilkan diagnostik. Dan, pengoptimalan yang dibuat dalam mode Rilis dapat menutupi beberapa MSIL di bawah skenario penguncian pemuat.

Cara men-debug masalah kunci loader

Diagnostik yang dihasilkan CLR ketika fungsi MSIL dipanggil menyebabkan CLR menangguhkan eksekusi. Itu pada gilirannya menyebabkan debugger mode campuran Visual C++ ditangguhkan juga saat menjalankan debuggee dalam proses. Namun, saat melampirkan ke proses, tidak mungkin untuk mendapatkan tumpukan panggilan terkelola untuk debuggee menggunakan debugger campuran.

Untuk mengidentifikasi fungsi MSIL tertentu yang dipanggil di bawah kunci loader, pengembang harus menyelesaikan langkah-langkah berikut:

  1. Pastikan bahwa 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 mscoree.dll dan mscorwks.dll PDB 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 kisi Properti untuk proyek startup dalam solusi. Pilih Properti Konfigurasi>Penelusuran Kesalahan. Atur properti Jenis Debugger ke Native-Only.

  3. Mulai debugger (F5).

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

  5. Buka jendela tumpukan panggilan. (Pada bilah menu, pilih Men-debug Tumpukan Panggilan Windows>.)> Penginisialisasi yang menyinggung DllMain atau statis 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>Segera.)

  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 tepat di bawah panggilan ini adalah 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.

Baca juga

Rakitan Campuran (Asli dan Terkelola)