Isso me deixou perplexo. Eu estava tentando otimizar alguns testes para o Noda Time, onde temos algumas verificações de inicializador de tipo. Eu pensei em descobrir se um tipo tem um inicializador de tipo (construtor estático ou variáveis estáticas com inicializadores) antes de carregar tudo em um novo AppDomain
. Para minha surpresa, um pequeno teste disso foi lançado NullReferenceException
- apesar de não haver valores nulos no meu código. Ele só lança a exceção quando compilado sem informações de depuração.
Aqui está um programa curto, mas completo, para demonstrar o problema:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
Console.WriteLine("Got initializer? {0}", cctor != null);
}
}
E uma transcrição de compilação e saída:
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
Agora você notará que estou usando o .NET 4.5 (o candidato a lançamento) - que pode ser relevante aqui. É um pouco complicado para mim testá-lo com as várias outras estruturas originais (em particular o "vanilla" .NET 4), mas se alguém tiver acesso fácil a máquinas com outras estruturas, eu estaria interessado nos resultados.
Outros detalhes:
- Estou em uma máquina x64, mas esse problema ocorre com os conjuntos x86 e x64
- É a "debug-ness" do código de chamada que faz a diferença - embora no caso de teste acima o esteja testando em seu próprio assembly, quando tentei no Noda Time, não precisei recompilar
NodaTime.dll
para ver as diferenças - exatamente oTest.cs
que se refere a ele. - Executando o "quebrado" montagem em Mono 2.10.8 não jogar
Alguma ideia? Bug do framework?
EDIT: Mais curioso e mais curioso. Se você atender a Console.WriteLine
chamada:
using System;
class Test
{
static Test() {}
static void Main()
{
var cctor = typeof(Test).TypeInitializer;
}
}
Agora só falha quando compilado com csc /o- /debug-
. Se você ativar as otimizações, ( /o+
) funcionará. Mas se você incluir a Console.WriteLine
chamada conforme o original, ambas as versões falharão.
NullReferenceException
(que sempre deve indicar um bug) realmente parece desonesto. I fortemente suspeito se este é um .NET 4.5 bug, eu perdi a janela para começá-lo fixo ...csc /o+ /debug- Test.cs
falha para mim, o que é estranho.Respostas:
com
csc test.cs
:Tentando carregar a partir de
[rsi+8]
quando@rsi
é NULL. Vamos inspecionar a função:@rsi
é carregado no início a partir de[rsp+20h]
então deve ser passado pelo chamador. Vamos olhar para o chamador:(Minha desmontagem mostra
System.Console.get_In
porque eu adicionei umConsole.GetLine()
no test.cs para ter a oportunidade de interromper o depurador. Eu validei que isso não muda o comportamento).Estamos nesta chamada:
000007fe8d45010c 41ff5228 call qword ptr [r10+28h]
(nosso endereço de retificação de quadro AV é a instrução logo após issocall
).Vamos comparar isso com o que acontece quando compilamos
csc /debug test.cs
. Podemos configurar umbp 000007fee5735360
, felizmente o módulo carrega no mesmo endereço. Na instrução que carrega@rsi
:Observe que
@rsi
é 00000000002depg. Ao percorrer a função, é mostrado que esse é o endereço que será desreferenciado posteriormente no local em que o exe ruim bombardeia (ou seja@rsi
, não muda). A pilha é muito interessante porque mostra um quadro extra :A chamada é a mesma
call qword ptr [r10+28h]
que vimos antes; portanto, no caso ruim, essa função provavelmente foi incorporada noMain()
, portanto, o fato de haver um quadro extra é um arenque vermelho. Se olharmos para a preparação destecall qword ptr [r10+28h]
notamos esta instrução:mov qword ptr [rsp+20h],rcx
. É isso que carrega o endereço que é eventualmente desreferenciado como@rsi
. No bom caso, é assim que@rcx
é carregado:No caso ruim, parece muito diferente:
Isto é muito diferente. Diferentemente do bom caso que chama CORINFO_HELP_GETSHARED_GCSTATIC_BASE e lê o que acaba sendo o ponteiro crítico que causa o AV de algum membro em deslocamento
1F0
em uma estrutura de retorno, o código otimizado o carrega de um endereço estático. E é claro que 12721220h contém NULL:Infelizmente é tarde demais para eu aprofundar agora, a desmontagem
CORINFO_HELP_GETSHARED_GCSTATIC_BASE
está longe de ser trivial. Estou postando isso na esperança de que alguém mais experiente em assuntos internos do CLR possa fazer sentido (como você pode ver, eu realmente considerei o problema apenas nas instruções nativas POV e ignorei completamente a IL).fonte
Como acredito ter encontrado algumas novas descobertas interessantes sobre o problema, decidi adicioná-las como uma resposta, reconhecendo ao mesmo tempo que elas não abordavam o "por que isso acontece" na pergunta original. Talvez alguém que saiba mais sobre o funcionamento interno dos tipos envolvidos possa postar uma resposta edificante com base também nas observações que estou postando.
Também consegui reproduzir o problema na minha máquina e rastreei uma conexão com a Interface System.Runtime.InteropServices._Type , implementada pela
System.Type
classe.Inicialmente, encontrei pelo menos três abordagens alternativas para corrigir o problema:
Simplesmente lançando o
Type
para_Type
dentro doMain
método:Ou verifique se a abordagem 1 foi usada anteriormente dentro do método:
Ou adicionando um campo estático à
Test
classe e inicializando-o (com conversão para_Type
):Mais tarde, descobri que, se não queremos envolver a
System.Runtime.InteropServices._Type
interface nas soluções alternativas, o problema também não ocorre:Adicionando um campo estático à
Test
classe e inicializando-o (sem convertê-lo para_Type
):Ou inicializando a
cctor
própria variável como um campo estático da classe:Estou ansioso pelo seu feedback.
fonte