Esto me tiene perplejo. Intenté optimizar algunas pruebas para Noda Time, donde tenemos alguna comprobación de inicializador de tipo. Pensé en averiguar si un tipo tiene un inicializador de tipo (constructor estático o variables estáticas con inicializadores) antes de cargar todo en un nuevo AppDomain
. Para mi sorpresa, arrojó una pequeña prueba de esto NullReferenceException
, a pesar de que no hay valores nulos en mi código. Que sólo se produce la excepción cuando se compila con ninguna información de depuración.
Aquí hay un programa corto pero completo para demostrar el problema:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
Console.WriteLine("Got initializer? {0}", cctor != null);
}
}
Y una transcripción de compilación y salida:
c:\Users\Jon\Test>csc Test.cs
Microsoft (R) Visual C# Compiler version 4.0.30319.17626
for Microsoft (R) .NET Framework 4.5
Copyright (C) Microsoft Corporation. All rights reserved.
c:\Users\Jon\Test>test
Unhandled Exception: System.NullReferenceException: Object reference not set to
an instance of an object.
at System.RuntimeType.GetConstructorImpl(BindingFlags bindingAttr, Binder bin
der, CallingConventions callConvention, Type[] types, ParameterModifier[] modifi
ers)
at Test.Main()
c:\Users\Jon\Test>csc /debug+ Test.cs
Microsoft (R) Visual C# Compiler version 4.0.30319.17626
for Microsoft (R) .NET Framework 4.5
Copyright (C) Microsoft Corporation. All rights reserved.
c:\Users\Jon\Test>test
Got initializer? True
Ahora notará que estoy usando .NET 4.5 (el candidato de lanzamiento), que puede ser relevante aquí. Es algo complicado para mí probarlo con los otros frameworks originales (en particular, "vanilla" .NET 4) pero si alguien más tiene fácil acceso a máquinas con otros frameworks, me interesarían los resultados.
Otros detalles:
- Estoy en una máquina x64, pero este problema ocurre con los ensambles x86 y x64
- Es la "depuración" del código de llamada lo que marca la diferencia, aunque en el caso de prueba anterior lo está probando en su propio ensamblado, cuando probé esto contra Noda Time no tuve que volver
NodaTime.dll
a compilar para ver las diferencias. soloTest.cs
que se refería a eso. - Ejecutar el ensamblaje "roto" en Mono 2.10.8 no arroja
¿Algunas ideas? Error de marco?
EDITAR: Más curioso y más curioso. Si saca la Console.WriteLine
llamada:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
}
}
Ahora solo falla cuando se compila con csc /o- /debug-
. Si activa las optimizaciones, ( /o+
) funciona. Pero si incluye la Console.WriteLine
llamada según el original, ambas versiones fallarán.
NullReferenceException
(que siempre debería indicar un error) realmente lo hace Mira poco fiable. Sospecho firmemente que si se trata de un error de .NET 4.5, he perdido la ventana para solucionarlo ...csc /o+ /debug- Test.cs
falla para mí, lo cual es extraño.Respuestas:
con
csc test.cs
:Intentando cargar desde
[rsi+8]
cuando@rsi
es NULL. Inspeccionemos la función:@rsi
se carga al principio desde,[rsp+20h]
por lo que debe ser pasado por la persona que llama. Veamos a la persona que llama:(Mi desmontaje se muestra
System.Console.get_In
porque agregué unConsole.GetLine()
test.cs para tener la oportunidad de romper el depurador. Validé que no cambia el comportamiento).Estamos en esta llamada:
000007fe8d45010c 41ff5228 call qword ptr [r10+28h]
(nuestra dirección de retiro de trama AV es la instrucción justo después de estocall
).Comparemos esto con lo que sucede cuando compilamos
csc /debug test.cs
. Podemos configurar unbp 000007fee5735360
, por suerte, el módulo se carga en la misma dirección. En la instrucción que carga@rsi
:Tenga en cuenta que
@rsi
es 00000000002de extensivamente Pasar a través de la función muestra que esta es la dirección que se desreferenciará más tarde en el lugar cuando las bombas ejecutables malas (es decir@rsi
, no cambien). La pila es muy interesante porque muestra un marco adicional :La llamada es la misma
call qword ptr [r10+28h]
que hemos visto antes, por lo que, en el caso negativo, esta función probablemente se incluyó en elMain()
, por lo que el hecho de que haya un marco adicional es una pista falsa. Si nos fijamos en la preparación de estecall qword ptr [r10+28h]
nos damos cuenta de esta instrucción:mov qword ptr [rsp+20h],rcx
. Esto es lo que carga la dirección que finalmente se desreferencia como@rsi
. En el buen caso, así es como@rcx
se carga:En el mal caso, se ve muy diferente:
Esto es muy diferente A diferencia del buen caso que llama a CORINFO_HELP_GETSHARED_GCSTATIC_BASE y lee lo que termina como el puntero crítico que causa que el AV de algún miembro se desplace
1F0
en una estructura de retorno, el código optimizado lo carga desde una dirección estática. Y, por supuesto, 12721220h contiene NULL:Lamentablemente, es demasiado tarde para profundizar en este momento, el desensamblaje
CORINFO_HELP_GETSHARED_GCSTATIC_BASE
está lejos de ser trivial. Estoy publicando esto con la esperanza de que alguien más conocedor de los aspectos internos de CLR pueda tener sentido (como puede ver, realmente consideré el problema solo de las instrucciones nativas POV e ignoré completamente IL).fuente
Como creo que he encontrado algunos hallazgos nuevos e interesantes sobre el problema, decidí agregarlos como respuesta, reconociendo al mismo tiempo que no están abordando el "por qué sucede" en la pregunta original. Quizás alguien que sepa más sobre el funcionamiento interno de los tipos involucrados podría publicar una respuesta edificante basada también en las observaciones que estoy publicando.
También he logrado reproducir el problema en mi máquina y he rastreado una conexión con la interfaz System.Runtime.InteropServices._Type , que implementa la
System.Type
clase.Inicialmente, he encontrado al menos 3 enfoques alternativos para solucionar el problema:
Simplemente echando el
Type
que_Type
en el interior delMain
método:O asegurándose de que el enfoque 1 se utilizó previamente dentro del método:
O agregando un campo estático a la
Test
clase e inicializándolo (con conversión a_Type
):Más tarde, descubrí que si no queremos involucrar a la
System.Runtime.InteropServices._Type
interfaz en las soluciones, el problema tampoco ocurre por:Agregar un campo estático a la
Test
clase e inicializarlo (sin convertirlo a_Type
):O inicializando la
cctor
variable en sí como un campo estático de la clase:Espero sus comentarios.
fuente