Apa penyebab FatalExecutionEngineError ini dalam .NET 4.5 beta? [Tutup]

150

Kode sampel di bawah ini terjadi secara alami. Tiba-tiba kode saya menghasilkan FatalExecutionEngineErrorpengecualian yang sangat buruk . Saya menghabiskan waktu 30 menit untuk mengisolasi dan meminimalkan sampel pelakunya. Kompilasi ini menggunakan Visual Studio 2012 sebagai aplikasi konsol:

class A<T>
{
    static A() { }

    public A() { string.Format("{0}", string.Empty); }
}

class B
{
    static void Main() { new A<object>(); }
}

Seharusnya menghasilkan kesalahan ini pada .NET framework 4 dan 4.5:

Tangkapan layar FatalExecutionException

Apakah ini bug yang diketahui, apa penyebabnya, dan apa yang bisa saya lakukan untuk menguranginya? Pekerjaan saya saat ini adalah tidak menggunakan string.Empty, tetapi apakah saya menggonggong pohon yang salah? Mengubah apa pun tentang kode itu membuatnya berfungsi seperti yang Anda harapkan - misalnya menghapus konstruktor statis kosong A, atau mengubah tipe paramter dari objectmenjadi int.

Saya mencoba kode ini di laptop saya dan tidak mengeluh. Namun, saya memang mencoba aplikasi utama saya dan jatuh di laptop juga. Saya pasti telah menghancurkan sesuatu saat mengurangi masalah, saya akan melihat apakah saya bisa mengetahui apa itu.

Laptop saya macet dengan kode yang sama seperti di atas, dengan framework 4.0, tetapi crash utama bahkan dengan 4,5. Kedua sistem menggunakan VS'12 dengan pembaruan terbaru (Juli?).

Informasi lebih lanjut :

  • Kode IL (kompilasi Debug / Setiap CPU / 4.0 / VS2010 (bukankah IDE itu penting?)): Http://codepad.org/boZDd98E
  • Tidak terlihat VS 2010 dengan 4.0. Tidak mogok dengan / tanpa optimisasi, CPU target berbeda, debugger terpasang / tidak terpasang, dll. - Tim Medora
  • Gangguan pada 2010 jika saya menggunakan AnyCPU, baik-baik saja di x86. Gangguan di Visual Studio 2010 SP1, menggunakan Platform Target = AnyCPU, tetapi baik-baik saja dengan Platform Target = x86. Mesin ini memiliki VS2012RC diinstal juga sehingga 4,5 mungkin melakukan penggantian di tempat. Gunakan AnyCPU dan TargetPlatform = 3.5 maka itu tidak crash sehingga terlihat seperti regresi dalam Framework.- colinsmith
  • Tidak dapat mereproduksi pada x86, x64 atau AnyCPU di VS2010 dengan 4.0. - Fuji
  • Hanya terjadi untuk x64, (2012rc, Fx4.5) - Henk Holterman
  • VS2012 RC pada Win8 RP. Awalnya Tidak melihat MDA ini saat menargetkan .NET 4.5. Saat beralih ke penargetan .NET 4.0, MDA muncul. Kemudian setelah beralih kembali ke .NET 4.5 MDA tetap. - Wayne
Gleno
sumber
Saya tidak pernah tahu Anda bisa membuat konstruktor statis bersama dengan yang umum. Heck, aku tidak pernah tahu konstruktor statis ada.
Cole Johnson
Saya punya ide: karena Anda mengubah B dari yang agak menjadi kelas statis menjadi hanya kelas dengan Main statis?
Cole Johnson
@ ChrisSinclair, kurasa tidak. Maksud saya, saya menguji kode ini di laptop saya, dan mendapatkan hasil yang sama.
Gleno
@ColeJohnson Ya IL cocok di semua kecuali satu tempat yang jelas. Tampaknya tidak ada bug di sini di c # compiler.
Michael Graczyk
14
Terima kasih kepada poster asli yang melaporkannya di sini, dan juga kepada Michael untuk analisisnya yang luar biasa. Rekan saya di CLR mencoba mereproduksi bug di sini dan menemukan bahwa itu mereproduksi pada versi "Release Candidate" dari 64 bit CLR, tetapi tidak pada versi final "Released To Manufacturing", yang memiliki sejumlah perbaikan bug setelahnya. RC. (Versi RTM akan tersedia untuk umum pada 15 Agustus 2012.) Karena itu mereka percaya ini menjadi masalah yang sama dengan yang dilaporkan di sini: connect.microsoft.com/VisualStudio/feedback/details/737108/…
Eric Lippert

Jawaban:

114

Ini juga bukan jawaban yang lengkap, tetapi saya punya beberapa ide.

Saya percaya saya telah menemukan penjelasan yang baik karena kita akan menemukan tanpa seseorang dari tim NET JIT yang menjawab.

MEMPERBARUI

Saya melihat sedikit lebih dalam, dan saya yakin saya telah menemukan sumber masalahnya. Tampaknya disebabkan oleh kombinasi bug dalam logika inisialisasi tipe JIT, dan perubahan dalam kompiler C # yang bergantung pada asumsi bahwa JIT berfungsi sebagaimana dimaksud. Saya pikir bug JIT ada di. NET 4.0, tetapi ditemukan oleh perubahan dalam kompiler untuk .NET 4.5.

Saya kira itu beforefieldinitbukan satu-satunya masalah di sini. Saya pikir ini lebih sederhana dari itu.

Jenis System.Stringmscorlib.dll dari .NET 4.0 berisi konstruktor statis:

.method private hidebysig specialname rtspecialname static 
    void  .cctor() cil managed
{
  // Code size       11 (0xb)
  .maxstack  8
  IL_0000:  ldstr      ""
  IL_0005:  stsfld     string System.String::Empty
  IL_000a:  ret
} // end of method String::.cctor

Dalam versi .NET 4.5 dari mscorlib.dll, String.cctor(konstruktor statis) jelas tidak ada:

..... Tidak ada konstruktor statis :( .....

Dalam kedua versi Stringjenisnya dihiasi dengan beforefieldinit:

.class public auto ansi serializable sealed beforefieldinit System.String

Saya mencoba membuat jenis yang akan dikompilasi ke IL sama (sehingga memiliki bidang statis tetapi tidak ada konstruktor statis .cctor), tetapi saya tidak bisa melakukannya. Semua tipe ini memiliki .cctormetode dalam IL:

public class MyString1 {
    public static MyString1 Empty = new MyString1();        
}

public class MyString2 {
    public static MyString2 Empty = new MyString2();

    static MyString2() {}   
}

public class MyString3 {
    public static MyString3 Empty;

    static MyString3() { Empty = new MyString3(); } 
}

Dugaan saya adalah bahwa dua hal berubah antara .NET 4.0 dan 4.5:

Pertama: EE diubah sehingga secara otomatis akan diinisialisasi String.Emptydari kode yang tidak dikelola. Perubahan ini mungkin dibuat untuk .NET 4.0.

Kedua: Kompiler berubah sehingga tidak memancarkan konstruktor statis untuk string, mengetahui bahwa String.Emptyakan ditugaskan dari sisi yang tidak dikelola. Perubahan ini tampaknya telah dibuat untuk .NET 4.5.

Tampaknya EE tidakString.Empty segera menetapkan beberapa jalur optimasi. Perubahan yang dilakukan ke kompiler (atau apa pun yang diubah untuk String.cctormenghilangkan) mengharapkan EE membuat penugasan ini sebelum kode pengguna dijalankan, tetapi tampaknya EE tidak membuat penugasan ini sebelum String.Emptydigunakan dalam metode tipe referensi kelas generik reified.

Terakhir, saya percaya bahwa bug tersebut mengindikasikan masalah yang lebih dalam pada logika inisialisasi tipe JIT. Tampaknya perubahan pada kompiler adalah kasus khusus untuk System.String, tetapi saya ragu bahwa JIT telah membuat kasus khusus untuk ini System.String.

Asli

Pertama-tama, WOW Orang-orang BCL menjadi sangat kreatif dengan beberapa optimasi kinerja. Banyak dari Stringmetode yang sekarang dilakukan dengan menggunakan Thread statis cache StringBuilderobjek.

Saya mengikuti petunjuk itu untuk sementara waktu, tetapi StringBuildertidak digunakan pada Trimjalur kode, jadi saya memutuskan itu tidak bisa menjadi masalah Thread statis.

Saya pikir saya menemukan manifestasi aneh dari bug yang sama.

Kode ini gagal karena pelanggaran akses:

class A<T>
{
    static A() { }

    public A(out string s) {
        s = string.Empty;
    }
}

class B
{
    static void Main() { 
        string s;
        new A<object>(out s);
        //new A<int>(out s);
        System.Console.WriteLine(s.Length);
    }
}

Namun, jika Anda tanda komentar //new A<int>(out s);di Mainkemudian kode bekerja dengan baik. Bahkan, jika Adiverifikasi dengan jenis referensi apa pun, program gagal, tetapi jika Adiverifikasi dengan jenis nilai apa pun maka kode tersebut tidak gagal. Juga jika Anda berkomentar keluar Akonstruktor statis, kode tidak pernah gagal. Setelah menggali ke dalam Trimdan Format, jelas bahwa masalahnya adalah yang Lengthsedang digarisbawahi, dan bahwa dalam sampel di atas Stringjenis belum diinisialisasi. Secara khusus, di dalam tubuh A's konstruktor, string.Emptytidak benar ditetapkan, meskipun di dalam tubuh Main, string.Emptyyang ditugaskan dengan benar.

Sungguh menakjubkan bagi saya bahwa inisialisasi tipe Stringentah bagaimana tergantung pada apakah atau tidak Adiverifikasi dengan tipe nilai. Satu-satunya teori saya adalah bahwa ada beberapa jalur kode JIT yang dioptimalkan untuk inisialisasi tipe generik yang dibagikan di antara semua jenis, dan bahwa jalur tersebut membuat asumsi tentang tipe referensi BCL ("tipe khusus?") Dan statusnya. Pandangan cepat meskipun kelas BCL lainnya dengan public staticbidang menunjukkan bahwa pada dasarnya semuanya menerapkan konstruktor statis (bahkan yang dengan konstruktor kosong dan tanpa data, seperti System.DBNulldan System.Empty. Tipe nilai BCL dengan public staticbidang tampaknya tidak menerapkan konstruktor statis ( System.IntPtr, misalnya) Ini sepertinya mengindikasikan bahwa JIT membuat beberapa asumsi tentang inisialisasi tipe referensi BCL.

FYI Ini adalah kode JITed untuk dua versi:

A<object>.ctor(out string):

    public A(out string s) {
00000000  push        rbx 
00000001  sub         rsp,20h 
00000005  mov         rbx,rdx 
00000008  lea         rdx,[FFEE38D0h] 
0000000f  mov         rcx,qword ptr [rcx] 
00000012  call        000000005F7AB4A0 
            s = string.Empty;
00000017  mov         rdx,qword ptr [FFEE38D0h] 
0000001e  mov         rcx,rbx 
00000021  call        000000005F661180 
00000026  nop 
00000027  add         rsp,20h 
0000002b  pop         rbx 
0000002c  ret 
    }

A<int32>.ctor(out string):

    public A(out string s) {
00000000  sub         rsp,28h 
00000004  mov         rax,rdx 
            s = string.Empty;
00000007  mov         rdx,12353250h 
00000011  mov         rdx,qword ptr [rdx] 
00000014  mov         rcx,rax 
00000017  call        000000005F691160 
0000001c  nop 
0000001d  add         rsp,28h 
00000021  ret 
    }

Sisa kode ( Main) identik antara dua versi.

EDIT

Selain itu, IL dari dua versi identik kecuali untuk panggilan A.ctormasuk B.Main(), di mana IL untuk versi pertama berisi:

newobj     instance void class A`1<object>::.ctor(string&)

melawan

... A`1<int32>...

di yang kedua.

Satu hal yang perlu diperhatikan adalah bahwa kode JITed untuk A<int>.ctor(out string): sama dengan di versi non-generik.

Michael Graczyk
sumber
3
Saya telah mencari jawaban di sepanjang jalan yang sangat mirip, tetapi sepertinya tidak mengarah ke mana pun. Ini tampaknya menjadi masalah kelas string dan mudah-mudahan bukan masalah yang lebih umum. Jadi sekarang saya sedang menunggu seseorang (Eric) dengan kode sumber untuk datang dan menjelaskan apa yang salah, dan jika ada hal lain yang dilakukan. Sebagai manfaat kecil diskusi ini sudah menyelesaikan perdebatan apakah seseorang harus menggunakan string.Emptyatau ""... :)
Gleno
Apakah IL di antara mereka sama?
Cole Johnson
49
Analisis yang bagus! Saya akan meneruskannya ke tim BCL. Terima kasih!
Eric Lippert
2
@EricLippert dan lain-lain: Saya menemukan kode suka typeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty);memberikan hasil yang berbeda pada .NET 4.0 versus .NET 4.5. Apakah perubahan ini terkait dengan perubahan yang dijelaskan di atas? Bagaimana .NET 4.5 secara teknis mengabaikan saya mengubah nilai bidang? Mungkin saya harus mengajukan pertanyaan baru tentang ini?
Jeppe Stig Nielsen
4
@JeppeStigNielsen: Jawaban atas pertanyaan Anda adalah: "mungkin,", "agaknya mudah," dan "ini adalah situs tanya-jawab, jadi ya, itu ide yang bagus jika Anda ingin jawaban atas pertanyaan Anda lebih baik dari 'mungkin' ".
Eric Lippert
3

Saya sangat curiga ini disebabkan oleh optimasi ini (terkait dengan BeforeFieldInit) di .NET 4.0.

Jika saya ingat dengan benar:

Ketika Anda mendeklarasikan konstruktor statis secara eksplisit, beforefieldinitdipancarkan, memberi tahu runtime bahwa konstruktor statis harus dijalankan sebelum akses anggota statis .

Tebakanku:

Aku menduga bahwa mereka entah bagaimana kacau fakta ini pada JITer x64, sehingga ketika seorang yang berbeda jenis ini anggota statis diakses dari kelas yang sendiri statis konstruktor sudah berjalan, entah bagaimana melompat berjalan (atau mengeksekusi dalam urutan yang salah) yang konstruktor statis - dan karena itu menyebabkan kerusakan. (Anda tidak mendapatkan pengecualian pointer nol, mungkin karena itu bukan inisialisasi nol.)

Saya belum menjalankan kode Anda, jadi bagian ini mungkin salah - tetapi jika saya harus menebak, saya akan mengatakan itu mungkin sesuatu string.Format(atau Console.WriteLine, yang serupa) perlu diakses secara internal yang menyebabkan kerusakan, seperti mungkin kelas terkait- lokal yang membutuhkan konstruksi statis eksplisit.

Sekali lagi, saya belum mengujinya, tapi tebakan terbaik saya pada data.

Jangan ragu untuk menguji hipotesis saya dan biarkan saya tahu bagaimana hasilnya.

pengguna541686
sumber
Bug masih terjadi ketika Btidak memiliki konstruktor statis, dan itu tidak terjadi ketika Adiverifikasi dengan tipe nilai. Saya pikir ini sedikit lebih rumit.
Michael Graczyk
@MichaelGraczyk: Saya pikir saya bisa menjelaskannya (sekali lagi, dengan tebakan). Bmemiliki konstruktor statis tidak masalah. Karena Amemiliki ctor statis, runtime mengacaukan urutan menjalankannya dibandingkan dengan beberapa kelas terkait lokal di beberapa namespace lainnya. Sehingga bidang itu belum diinisialisasi. Namun, jika Anda instantiate Adengan tipe nilai, maka itu mungkin runtime yang kedua melalui instantiating A(CLR mungkin sudah pre-instantiated dengan tipe referensi, sebagai optimisasi) sehingga order berfungsi ketika dijalankan kedua kalinya .
user541686
@MichaelGraczyk: Meskipun ini bukan penjelasannya - saya pikir saya cukup yakin bahwa beforefieldinitoptimasi yang diberikan adalah akar penyebabnya. Mungkin beberapa penjelasan yang sebenarnya berbeda dari apa yang saya sebutkan, tetapi akar penyebabnya kemungkinan adalah hal yang sama.
user541686
Saya melihat lebih dalam IL, dan saya pikir Anda ke sesuatu. Saya tidak berpikir bahwa ide pass kedua akan relevan di sini, karena kode masih gagal jika saya melakukan banyak panggilan secara sewenang-wenang A<object>.ctor().
Michael Graczyk
@MichaelGraczyk: Senang mendengar, dan terima kasih untuk tes itu. Sayangnya, saya tidak dapat mereproduksinya di laptop saya sendiri. (2010 4.0 x64) Bisakah Anda memeriksa untuk melihat apakah memang terkait dengan pemformatan string (yaitu terkait-lokal)? Apa yang terjadi jika Anda menghapus bagian itu?
user541686
1

Pengamatan, tetapi DotPeek menunjukkan string yang didekompilasi.

/// <summary>
/// Represents the empty string. This field is read-only.
/// </summary>
/// <filterpriority>1</filterpriority>
[__DynamicallyInvokable]
public static readonly string Empty;

internal sealed class __DynamicallyInvokableAttribute : Attribute
{
  [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
  public __DynamicallyInvokableAttribute()
  {
  }
}

Jika saya menyatakan milik saya Emptydengan cara yang sama kecuali tanpa atribut, saya tidak lagi mendapatkan MDA:

class A<T>
{
    static readonly string Empty;

    static A() { }

    public A()
    {
        string.Format("{0}", Empty);
    }
}
lesscode
sumber
Dan dengan atribut itu? Kami sudah mapan ""memecahkannya.
Henk Holterman
Atribut "Performa kritis ..." mempengaruhi Atribut konstruktor itu sendiri, bukan metode yang menghiasi atribut.
Michael Graczyk
Itu internal. Ketika saya mendefinisikan atribut identik saya sendiri, itu masih tidak menyebabkan MDA. Bukannya saya harapkan - jika JITter mencari atribut spesifik itu, ia tidak akan menemukan milik saya.
lesscode