c# - specification - common language runtime detectó un programa no válido
¿Cuál es la causa de este FatalExecutionEngineError en.NET 4.5 beta? (3)
El código de ejemplo siguiente se produjo de forma natural. De repente, mi código es una excepción FatalExecutionEngineError
muy desagradable. Pasé unos buenos 30 minutos tratando de aislar y minimizar la muestra culpable. Compila esto usando Visual Studio 2012 como una aplicación de consola:
class A<T>
{
static A() { }
public A() { string.Format("{0}", string.Empty); }
}
class B
{
static void Main() { new A<object>(); }
}
Debería producir este error en .NET framework 4 y 4.5:
¿Es este un error conocido, cuál es la causa y qué puedo hacer para mitigarlo? Mi trabajo actual es no usar string.Empty
. string.Empty
, pero ¿estoy ladrando en el árbol equivocado? Cambiar cualquier cosa sobre ese código lo hace funcionar como era de esperar, por ejemplo, eliminando el constructor estático vacío de A
, o cambiando el parámetro de tipo de object
a int
.
Intenté este código en mi computadora portátil y no me quejé. Sin embargo, probé mi aplicación principal y también se colgó en la computadora portátil. Debo haber destruido algo al reducir el problema, voy a ver si puedo averiguar qué fue eso.
Mi computadora portátil se colgó con el mismo código que el anterior, con el framework 4.0, pero los principales bloqueos incluso con 4.5. Ambos sistemas están usando VS''12 con las últimas actualizaciones (¿julio?).
Más información :
- Código IL (depuración compilada / cualquier CPU / 4.0 / VS2010 (¿no debería importar ese IDE?)): http://codepad.org/boZDd98E
- No se ve VS 2010 con 4.0. No falla con / sin optimizaciones, CPU objetivo diferente, depurador conectado / no conectado, etc. - Tim Medora
- Bloqueos en 2010 si uso AnyCPU, está bien en x86. Se bloquea en Visual Studio 2010 SP1, utilizando Platform Target = AnyCPU, pero está bien con Platform Target = x86. Esta máquina también tiene VS2012RC instalado, por lo que 4.5 posiblemente realice un reemplazo in situ. Use AnyCPU y TargetPlatform = 3.5 y luego no se cuelga, por lo que parece una regresión en el Framework.- colinsmith
- No se puede reproducir en x86, x64 o AnyCPU en VS2010 con 4.0. - Fuji
- Solo sucede para x64, (2012rc, Fx4.5) - Henk Holterman
- VS2012 RC en Win8 RP. Inicialmente no se ve esta MDA cuando se orienta a .NET 4.5. Cuando se cambió a la orientación de .NET 4.0 apareció la MDA. Luego, después de volver a .NET 4.5, la MDA permanece. - Wayne
Sospecho fuertemente que esto es causado por esta optimización (relacionada con BeforeFieldInit
) en .NET 4.0.
Si recuerdo correctamente:
Cuando declara explícitamente un constructor estático, se emite beforefieldinit
, indicando al tiempo de ejecución que el constructor estático debe ejecutarse antes de que cualquier miembro estático acceda .
Mi conjetura:
Supongo que de alguna manera arruinaron este hecho en x64 JITer, de modo que cuando se accede a un miembro estático de un tipo diferente desde una clase cuyo propio constructor estático ya se ha ejecutado, de alguna manera se salta la ejecución (o ejecuta en el orden incorrecto) del constructor estático - y por lo tanto causa un bloqueo. (No obtiene una excepción de puntero nulo, probablemente porque no tiene inicialización nula).
No he ejecutado su código, así que esta parte puede estar equivocada, pero si tuviera que hacer otra suposición, diría que podría ser algo de string.Format
. El string.Format
(o Console.WriteLine
, que es similar) necesita acceder internamente. causando el bloqueo, tal como una clase relacionada con el entorno local que necesita una construcción estática explícita.
Una vez más, no lo he probado, pero es mi mejor estimación de los datos.
Siéntase libre de probar mi hipótesis y dejarme saber cómo va.
Una observación, pero DotPeek muestra la cadena decompilada. Vacío así:
/// <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()
{
}
}
Si declaro mi propio Empty
la misma manera, excepto sin el atributo, ya no obtengo el MDA:
class A<T>
{
static readonly string Empty;
static A() { }
public A()
{
string.Format("{0}", Empty);
}
}
Esta tampoco es una respuesta completa, pero tengo algunas ideas.
Creo que he encontrado una buena explicación que encontraremos sin que alguien del equipo .NET JIT responda.
ACTUALIZAR
Miré un poco más profundo, y creo que he encontrado la fuente del problema. Parece ser causado por una combinación de un error en la lógica de inicialización de tipo JIT y un cambio en el compilador C # que se basa en la suposición de que el JIT funciona como se esperaba. Creo que el error JIT existía en .NET 4.0, pero fue descubierto por el cambio en el compilador de .NET 4.5.
No creo que beforefieldinit
sea el único problema aquí. Creo que es más simple que eso.
El tipo System.String
en mscorlib.dll de .NET 4.0 contiene un constructor estático:
.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
En la versión .NET 4.5 de mscorlib.dll, String.cctor
(el constructor estático) brilla por su ausencia:
..... Ningún constructor estático :( .....
En ambas versiones, el tipo String
está adornado con beforefieldinit
:
.class public auto ansi serializable sealed beforefieldinit System.String
Traté de crear un tipo que compilara IL de manera similar (para que tenga campos estáticos pero no constructor estático .cctor
), pero no pude hacerlo. Todos estos tipos tienen un método .cctor
en 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(); }
}
Mi suposición es que dos cosas cambiaron entre .NET 4.0 y 4.5:
Primero: El EE fue cambiado para que automáticamente inicialice String.Empty
del código no administrado. Este cambio probablemente se realizó para .NET 4.0.
Segundo: el compilador cambió para que no emitiera un constructor estático para la cadena, sabiendo que String.Empty
se asignaría desde el lado no administrado. Este cambio parece haberse realizado para .NET 4.5.
Parece que EE no asigna String.Empty
suficientemente pronto a lo largo de algunas rutas de optimización. El cambio realizado en el compilador (o lo que sea que haya cambiado para hacer que String.cctor
desaparezca) esperaba que EE realizara esta asignación antes de que se ejecute cualquier código de usuario, pero parece que EE no hace esta asignación antes de que String.Empty
se use en métodos de referencia escriba las clases genéricas reificadas.
Por último, creo que el error es indicativo de un problema más profundo en la lógica de inicialización de tipo JIT. Parece que el cambio en el compilador es un caso especial para System.String
, pero dudo que el JIT haya creado un caso especial aquí para System.String
.
Original
Antes que nada, WOW La gente de BCL se volvió muy creativa con algunas optimizaciones de rendimiento. Muchos de los métodos String
ahora se realizan utilizando un objeto StringBuilder
caché estático en caché.
Seguí esa pista por un tiempo, pero StringBuilder
no se usa en la ruta del código Trim
, así que decidí que no podía ser un problema estático de subprocesos.
Creo que encontré una extraña manifestación del mismo error.
Este código falla con una violación de acceso:
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);
}
}
Sin embargo, si descomenta //new A<int>(out s);
en Main
, el código funciona bien. De hecho, si A
se reifica con cualquier tipo de referencia, el programa falla, pero si A
se reifica con cualquier tipo de valor, entonces el código no falla. Además, si comenta el constructor estático de A, el código nunca falla. Después de profundizar en Trim
and Format
, está claro que el problema es que Length
está siendo inline, y que en estas muestras arriba, el tipo String
no se ha inicializado. En particular, dentro del cuerpo del constructor de A, string.Empty
no se asigna correctamente, aunque dentro del cuerpo de Main
, string.Empty
se asigna correctamente.
Es sorprendente para mí que la inicialización de tipos de String
alguna manera dependa de si A
está reificado con un tipo de valor. Mi única teoría es que hay alguna ruta de optimización del código JIT para la inicialización genérica de tipos que se comparte entre todos los tipos, y que esa ruta hace suposiciones sobre los tipos de referencia BCL (¿tipos especiales?) Y su estado. Una mirada rápida a través de otras clases BCL con campos public static
muestra que básicamente todos ellos implementan un constructor estático (incluso aquellos con constructores vacíos y sin datos, como System.DBNull
y System.Empty
. Los tipos de valores BCL con campos public static
no parecen para implementar un constructor estático ( System.IntPtr
, por ejemplo). Esto parece indicar que el JIT hace algunas suposiciones sobre la inicialización del tipo de referencia BCL.
FYI Aquí está el código JITed para las dos versiones:
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
}
El resto del código ( Main
) es idéntico entre las dos versiones.
EDITAR
Además, el IL de las dos versiones es idéntico, excepto por la llamada a A.ctor
en B.Main()
, donde el IL para la primera versión contiene:
newobj instance void class A`1<object>::.ctor(string&)
versus
... A`1<int32>...
en el segundo.
Otra cosa a tener en cuenta es que el código JITed para A<int>.ctor(out string)
: es el mismo que en la versión no genérica.