Fazendo a linguagem .NET pisar corretamente no depurador

135

Em primeiro lugar, peço desculpas pela extensão desta pergunta.

Eu sou o autor do IronScheme . Recentemente, tenho trabalhado duro na emissão de informações de depuração decentes, para poder usar o depurador .NET 'nativo'.

Embora isso tenha sido parcialmente bem-sucedido, estou com alguns problemas iniciais.

O primeiro problema está relacionado ao pisar.

Como o Scheme é uma linguagem de expressão, tudo tende a ser colocado entre parênteses, ao contrário das principais linguagens .NET que parecem ser baseadas em declarações (ou linhas).

O código original (Esquema) se parece com:

(define (baz x)
  (cond
    [(null? x) 
      x]
    [(pair? x) 
      (car x)]
    [else
      (assertion-violation #f "nooo" x)]))

De propósito, expus cada expressão em uma nova linha.

O código emitido se transforma em C # (via ILSpy) se parece com:

public static object ::baz(object x)
{
  if (x == null)
  {
    return x;
  }
  if (x is Cons)
  {
    return Builtins.Car(x);
  }
  return #.ironscheme.exceptions::assertion-violation+(
     RuntimeHelpers.False, "nooo", Builtins.List(x));
}

Como você pode ver, bem simples.

Nota: Se o código foi transformado em uma expressão condicional (? :) em C #, a coisa toda seria apenas uma etapa de depuração, lembre-se disso.

Aqui está a saída IL com os números de origem e linha:

  .method public static object  '::baz'(object x) cil managed
  {
    // Code size       56 (0x38)
    .maxstack  6
    .line 15,15 : 1,2 ''
//000014: 
//000015: (define (baz x)
    IL_0000:  nop
    .line 17,17 : 6,15 ''
//000016:   (cond
//000017:     [(null? x) 
    IL_0001:  ldarg.0
    IL_0002:  brtrue     IL_0009

    .line 18,18 : 7,8 ''
//000018:       x]
    IL_0007:  ldarg.0
    IL_0008:  ret

    .line 19,19 : 6,15 ''
//000019:     [(pair? x) 
    .line 19,19 : 6,15 ''
    IL_0009:  ldarg.0
    IL_000a:  isinst [IronScheme]IronScheme.Runtime.Cons
    IL_000f:  ldnull
    IL_0010:  cgt.un
    IL_0012:  brfalse    IL_0020

    IL_0017:  ldarg.0
    .line 20,20 : 7,14 ''
//000020:       (car x)]
    IL_0018:  tail.
    IL_001a:  call object [IronScheme]IronScheme.Runtime.Builtins::Car(object)
    IL_001f:  ret

    IL_0020:  ldsfld object 
         [Microsoft.Scripting]Microsoft.Scripting.RuntimeHelpers::False
    IL_0025:  ldstr      "nooo"
    IL_002a:  ldarg.0
    IL_002b:  call object [IronScheme]IronScheme.Runtime.Builtins::List(object)
    .line 22,22 : 7,40 ''
//000021:     [else
//000022:       (assertion-violation #f "nooo" x)]))
    IL_0030:  tail.
    IL_0032:  call object [ironscheme.boot]#::
       'ironscheme.exceptions::assertion-violation+'(object,object,object)
    IL_0037:  ret
  } // end of method 'eval-core(033)'::'::baz'

Nota: Para impedir que o depurador simplesmente destaque todo o método, eu faço o ponto de entrada do método ter apenas 1 coluna de largura.

Como você pode ver, cada expressão é mapeada corretamente para uma linha.

Agora, o problema com o passo (testado no VS2010, mas o mesmo problema / semelhante no VS2008):

Estes IgnoreSymbolStoreSequencePointsnão são aplicados.

  1. Chame baz com argumento nulo, ele funciona corretamente. (nulo? x) seguido por x.
  2. Chame baz com Contras, funciona corretamente. (nulo? x) então (par? x) e depois (carro x).
  3. Chame baz com outro argumento, ele falha. (null? x) then (pair? x) then (car x) then (violação da asserção ...).

Ao aplicar IgnoreSymbolStoreSequencePoints(conforme recomendado):

  1. Chame baz com argumento nulo, ele funciona corretamente. (nulo? x) seguido por x.
  2. Chame baz com Contras, ele falha. (nulo? x) e depois (par? x).
  3. Chame baz com outro argumento, ele falha. (null? x) then (pair? x) then (car x) then (violação da asserção ...).

Também acho neste modo que algumas linhas (não mostradas aqui) estão incorretamente destacadas, e são desativadas por 1.

Aqui estão algumas idéias sobre quais poderiam ser as causas:

  • Tailcalls confunde o depurador
  • Locais sobrepostos (não mostrados aqui) confundem o depurador (ele funciona muito bem ao definir um ponto de interrupção)
  • ????

O segundo problema, mas também sério, é o depurador que falha ao interromper / atingir pontos de interrupção em alguns casos.

O único local em que eu posso fazer com que o depurador seja quebrado corretamente (e consistentemente) é no ponto de entrada do método.

A situação melhora um pouco quando IgnoreSymbolStoreSequencePointsnão é aplicada.

Conclusão

Pode ser que o depurador do VS seja apenas um buggy :(

Referências:

  1. Tornando um idioma CLR / .NET debugável

Atualização 1:

Mdbg não funciona para assemblies de 64 bits. Então isso está fora. Não tenho mais máquinas de 32 bits para testá-lo. Atualização: Tenho certeza de que isso não é um grande problema, alguém tem uma correção? Edit: Sim, bobo, apenas inicie o mdbg no prompt de comando x64 :)

Atualização 2:

Eu criei um aplicativo C # e tentei dissecar as informações da linha.

Minhas descobertas:

  • Após qualquer brXXXinstrução, você precisa ter um ponto de sequência (se não for válido, também conhecido como '#line hidden', emita a nop).
  • Antes de qualquer brXXXinstrução, emita um '#line hidden' e a nop.

A aplicação disso, no entanto, não corrige os problemas (sozinho?).

Mas adicionando o seguinte, dá o resultado desejado :)

  • Depois ret, emita um '#line hidden' e a nop.

Isso está usando o modo em que IgnoreSymbolStoreSequencePointsnão é aplicado. Quando aplicadas, algumas etapas ainda são ignoradas :(

Aqui está a saída IL quando acima foi aplicada:

  .method public static object  '::baz'(object x) cil managed
  {
    // Code size       63 (0x3f)
    .maxstack  6
    .line 15,15 : 1,2 ''
    IL_0000:  nop
    .line 17,17 : 6,15 ''
    IL_0001:  ldarg.0
    .line 16707566,16707566 : 0,0 ''
    IL_0002:  nop
    IL_0003:  brtrue     IL_000c

    .line 16707566,16707566 : 0,0 ''
    IL_0008:  nop
    .line 18,18 : 7,8 ''
    IL_0009:  ldarg.0
    IL_000a:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_000b:  nop
    .line 19,19 : 6,15 ''
    .line 19,19 : 6,15 ''
    IL_000c:  ldarg.0
    IL_000d:  isinst     [IronScheme]IronScheme.Runtime.Cons
    IL_0012:  ldnull
    IL_0013:  cgt.un
    .line 16707566,16707566 : 0,0 ''
    IL_0015:  nop
    IL_0016:  brfalse    IL_0026

    .line 16707566,16707566 : 0,0 ''
    IL_001b:  nop
    IL_001c:  ldarg.0
    .line 20,20 : 7,14 ''
    IL_001d:  tail.
    IL_001f:  call object [IronScheme]IronScheme.Runtime.Builtins::Car(object)
    IL_0024:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_0025:  nop
    IL_0026:  ldsfld object 
      [Microsoft.Scripting]Microsoft.Scripting.RuntimeHelpers::False
    IL_002b:  ldstr      "nooo"
    IL_0030:  ldarg.0
    IL_0031:  call object [IronScheme]IronScheme.Runtime.Builtins::List(object)
    .line 22,22 : 7,40 ''
    IL_0036:  tail.
    IL_0038:  call object [ironscheme.boot]#::
      'ironscheme.exceptions::assertion-violation+'(object,object,object)
    IL_003d:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_003e:  nop
  } // end of method 'eval-core(033)'::'::baz'

Atualização 3:

Problema com o 'semi-reparo' acima. Peverify relata erros em todos os métodos devido ao nopdepois ret. Eu realmente não entendo o problema. Como pode uma nopverificação de interrupção após a ret. É como código morto (exceto que nem é código) ... Bem, a experimentação continua.

Atualização 4:

De volta a casa agora, removemos o código 'não verificável', rodando no VS2008 e as coisas são muito piores. Talvez a execução de código não verificável para a depuração adequada possa ser a resposta. No modo 'release', toda saída ainda seria verificável.

Atualização 5:

Decidi agora que a minha ideia acima é a única opção viável por enquanto. Embora o código gerado não seja verificável, ainda não encontrei nenhum VerificationException. Não sei qual será o impacto no usuário final com esse cenário.

Como um bônus, minha segunda questão também foi resolvida. :)

Aqui está um pequeno screencast do que eu acabei. Ele atinge pontos de interrupção, faz o passo adequado (entrada / saída / saída), etc. Em suma, o efeito desejado.

Eu, no entanto, ainda não estou aceitando isso como a maneira de fazê-lo. Parece demais para mim. Ter uma confirmação sobre o problema real seria bom.

Atualização 6:

Só tive a alteração para testar o código no VS2010, parece haver alguns problemas:

  1. A primeira chamada agora não é executada corretamente. (violação de asserção ...) for atingida. Outros casos funcionam bem. Algum código antigo emitia posições desnecessárias. Removido o código, funciona conforme o esperado. :)
  2. Mais seriamente, os pontos de interrupção falham na segunda chamada do programa (usando a compilação na memória, despejar o assembly no arquivo parece tornar os pontos de interrupção felizes novamente).

Ambos os casos funcionam corretamente no VS2008. A principal diferença é que, no VS2010, o aplicativo inteiro é compilado para o .NET 4 e no VS2008, compilado para o .NET 2. Ambos executando 64 bits.

Atualização 7:

Como mencionado, eu tenho o mdbg rodando em 64 bits. Infelizmente, ele também tem o problema do ponto de interrupção onde falha ao executar novamente o programa (isso implica que ele é recompilado, portanto, não usando o mesmo assembly, mas ainda usando a mesma fonte).

Atualização 8:

Eu registrei um bug no site do MS Connect referente ao problema do ponto de interrupção.

Update: Fixed

Atualização 9:

Após uma longa reflexão, a única maneira de fazer feliz o depurador parece estar executando o SSA, para que cada passo possa ser isolado e seqüencial. Ainda estou para provar essa noção. Mas parece lógico. Obviamente, limpar os temporários do SSA interromperá a depuração, mas isso é fácil de alternar e deixá-los não tem muita sobrecarga.

leppie
fonte
Quaisquer idéias bem-vindas, vou testá-las e anexar os resultados à pergunta. Primeiras 2 idéias, tente mdbg e escreva o mesmo código em C # e compare.
leppie
6
Qual é a questão?
George Stocker
9
Eu acho que a pergunta dele é do tipo "por que meu idioma não está avançando corretamente?"
5119 McKay
1
Se você não passar pelo peverify, não poderá executar em situações de confiança parcial, como em uma unidade mapeada ou em muitas configurações de hospedagem de plugins. Como você está gerando sua IL, reflection.Emit ou via text + ilasm? Em qualquer um dos casos, você sempre pode colocar o .line 16707566,16707566: 0,0 '' dessa maneira, assim você não precisa se preocupar com eles colocando um nop após o retorno.
Hippiehunter
@Hippiehunter: Obrigado. Infelizmente, sem os nops, o passo falhou (vou verificar isso novamente para ter certeza). É um sacrifício, acho que teria que fazer. Não é como se o VS pudesse rodar sem direitos de administrador :) Btw usando Reflection.Emit via DLR (uma ramificação inicial muito hackeada).
leppie

Respostas:

26

Sou engenheiro da equipe do Visual Studio Debugger.

Corrija-me se estiver errado, mas parece que o único problema que resta é que, ao mudar de PDBs para o formato de símbolo de compilação dinâmica do .NET 4, alguns pontos de interrupção estão sendo perdidos.

Provavelmente precisaríamos de uma reprodução para diagnosticar exatamente o problema. No entanto, aqui estão algumas notas que podem ajudar.

  1. O VS (2008+) pode ser executado como um não administrador
  2. Algum símbolo carrega na segunda vez? Você pode testar invadindo (por meio de exceção ou chamar System.Diagnostic.Debugger.Break ())
  3. Supondo que os símbolos carreguem, existe uma reprodução que você poderia nos enviar?
  4. A provável diferença é que o formato do símbolo para o código compilado dinâmico é 100% diferente entre o .NET 2 (fluxo PDB) e o .NET 4 (acho que eles chamam de IL DB?)
  5. O som do nop está certo. Consulte as regras para gerar pontos de sequência implícitos abaixo.
  6. Na verdade, você não precisa emitir coisas em linhas diferentes. Por padrão, o VS passo 'instruções de símbolo' onde, como escritor do compilador, você define o que 'instrução de símbolo' significa. Portanto, se você deseja que cada expressão seja uma coisa separada no arquivo de símbolos, isso funcionará perfeitamente.

O JIT cria um ponto de sequência implícito com base nas seguintes regras: 1. Instruções IL nop 2. IL empilham pontos vazios 3. A instrução IL imediatamente após uma instrução de chamada

Se precisarmos de uma reprodução para resolver seu problema, você pode registrar um bug de conexão e fazer o upload de arquivos com segurança por esse meio.

Atualizar:

Incentivamos outros usuários que enfrentam esse problema a experimentar a Visualização do desenvolvedor do Dev11 em http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=27543 e comentar com comentários. (Precisa segmentar 4.5)

Atualização 2:

Leppie verificou a correção para trabalhar com ele na versão Beta do Dev11 disponível em http://www.microsoft.com/visualstudio/11/en-us/downloads, conforme observado no bug de conexão https://connect.microsoft. com / VisualStudio / feedback / detalhes / 684089 / .

Obrigado,

Lucas

Luke Kim
fonte
Muito obrigado. Vou tentar fazer repro, se necessário.
leppie
Quanto à confirmação, sim, você está correto, mas eu ainda preferiria resolver os problemas de etapa sem quebrar a verificabilidade. Mas, como já foi dito, estou disposto a sacrificar que, se puder provar que isso nunca produzirá um tempo de execução VerificationException.
Leppie
Quanto ao ponto 6, simplesmente o fiz com base em linhas para esta pergunta. O depurador, na verdade, efetua corretamente a entrada / saída / sobre expressões como pretendo que funcione :) O único problema com a abordagem é definir pontos de interrupção nas expressões 'internas' se uma expressão 'externa' a cobrir; o depurador no VS tende a querer criar a expressão mais externa como o ponto de interrupção.
leppie
Eu acho que isolei a questão do ponto de interrupção. Durante a execução no CLR2, os pontos de interrupção parecem ser reavaliados ao executar o código novamente, embora novo código. No CLR4, os pontos de interrupção apenas 'aderem' ao código original. Eu fiz um pequeno screencast do comportamento do CLR4 @ screencast.com/t/eiSilNzL5Nr . Observe que o nome do tipo muda entre invocações.
leppie
Eu registrei um bug ao conectar. A repro é muito fácil :) connect.microsoft.com/VisualStudio/feedback/details/684089
leppie
3

Sou engenheiro da equipe SharpDevelop Debugger :-)

Você resolveu o problema?

Você tentou depurá-lo no SharpDevelop? Se houver um erro no .NET, pergunto-me se precisamos implementar alguma solução alternativa. Não estou ciente desse problema.

Você tentou depurar no ILSpy? Especialmente sem símbolos de depuração. Ele depuraria o código C #, mas informaria se as instruções de IL são bem depuráveis. (Lembre-se de que o depurador ILSpy é beta)

Notas rápidas sobre o código IL original:

  • .line 19,19: 6,15 '' ocorre duas vezes?
  • .line 20,20: 7,14 '' não inicia no ponto de sequência implícito (a pilha não está vazia). eu estou preocupado
  • .line 20,20: 7,14 '' inclui o código para "carro x" (bom) e "#f nooo x" (ruim?)
  • em relação ao nop após ret. E quanto a stloc, ldloc, ret? Eu acho que o C # usa esse truque para tornar ret um ponto de sequência distinto.

David

dsrbecky
fonte
+1 Obrigado pelo feedback, como o primeiro ponto, efeito colateral infeliz do gerador de código, mas parece inofensivo. Segundo ponto, é exatamente isso que eu preciso, mas entendo seu ponto de vista. Não tenho certeza do que você quer dizer com o último ponto.
Leppie
O terceiro ponto foi que a linha 22,22: 7,40 '' deve estar antes da IL_0020. Ou deve haver algo antes de IL_0020, caso contrário, o código ainda conta como .line 20,20: 7,14 ''. O quarto ponto é "ret, nop" pode ser substituído por "sloc, .line, ldloc, ret". Eu já vi o padrão antes, talvez fosse redundante, mas talvez tivesse um motivo.
precisa saber é o seguinte
Não posso fazer o stloc, ldloc antes de ret, pois perderei a chamada final. Ah, entendi, você estava se referindo à saída original?
Leppie