Rendere corretto il linguaggio .NET nel debugger


135

In primo luogo, mi scuso per la lunghezza di questa domanda.

Sono l'autore di IronScheme . Recentemente ho lavorato duramente sull'emissione di informazioni di debug decenti, in modo da poter utilizzare il debugger .NET "nativo".

Anche se questo ha avuto in parte successo, mi imbatto in alcuni problemi di dentizione.

Il primo problema è legato allo stepping.

Poiché Scheme è un linguaggio di espressione, tutto tende ad essere racchiuso tra parentesi, a differenza dei principali linguaggi .NET che sembrano essere basati su istruzioni (o linee).

Il codice originale (Schema) è simile a:

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

Ho espressamente disposto ogni espressione su una nuova riga.

Il codice emesso si trasforma in C # (tramite ILSpy) è simile a:

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));
}

Come puoi vedere, abbastanza semplice.

Nota: se il codice fosse trasformato in un'espressione condizionale (? :) in C #, l'intera cosa sarebbe solo un passo di debug, tienilo a mente.

Ecco l'output IL con i numeri di sorgente e di riga:

  .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: per impedire al debugger di evidenziare semplicemente l'intero metodo, faccio in modo che il punto di ingresso del metodo sia largo solo 1 colonna.

Come puoi vedere, ogni espressione è mappata correttamente su una linea.

Ora il problema con stepping (testato su VS2010, ma problema uguale / simile su VS2008):

Questi IgnoreSymbolStoreSequencePointsnon sono applicati.

  1. Chiama baz con null arg, funziona correttamente. (null? x) seguito da x.
  2. Chiama baz con contro arg, funziona correttamente. (null? x) quindi (pair? x) quindi (car x).
  3. Chiama baz con altri arg, fallisce. (null? x) quindi (pair? x) quindi (car x) quindi (asserzione-violazione ...).

Quando si applica IgnoreSymbolStoreSequencePoints(come raccomandato):

  1. Chiama baz con null arg, funziona correttamente. (null? x) seguito da x.
  2. Chiama baz con contro arg, non riesce. (null? x) quindi (coppia? x).
  3. Chiama baz con altri arg, fallisce. (null? x) quindi (pair? x) quindi (car x) quindi (asserzione-violazione ...).

Trovo anche in questa modalità che alcune linee (non mostrate qui) sono evidenziate in modo errato, sono disattivate di 1.

Ecco alcune idee su quali potrebbero essere le cause:

  • Tailcalls confonde il debugger
  • Posizioni sovrapposte (non mostrate qui) confondono il debugger (lo fa molto bene quando si imposta un breakpoint)
  • ????

Il secondo, ma anche grave, problema è che il debugger non riesce a rompere / colpire i punti di interruzione in alcuni casi.

L'unico posto in cui posso fare in modo che il debugger si interrompa correttamente (e coerentemente), è il punto di ingresso del metodo.

La situazione migliora leggermente quando IgnoreSymbolStoreSequencePointsnon viene applicata.

Conclusione

È possibile che il debugger VS sia semplicemente buggy :(

Riferimenti:

  1. Rendere debuggabile un linguaggio CLR / .NET

Aggiornamento 1:

Mdbg non funziona per gli assembly a 64 bit. Quindi è fuori. Non ho più macchine a 32 bit su cui testarlo. Aggiornamento: sono sicuro che questo non è un grosso problema, qualcuno ha una soluzione? Modifica: Sì, sciocco me, basta avviare mdbg al prompt dei comandi x64 :)

Aggiornamento 2:

Ho creato un'app C # e ho provato a sezionare le informazioni sulla linea.

Le mie scoperte:

  • Dopo ogni brXXXistruzione è necessario disporre di un punto sequenza (se non valido anche noto come '#line hidden', emettere a nop).
  • Prima di qualsiasi brXXXistruzione, emetti un '#line nascosto' e un nop.

L'applicazione di questo, tuttavia, non risolve i problemi (da solo?).

Ma aggiungendo quanto segue, si ottiene il risultato desiderato :)

  • Dopo ret, emetti un '#line nascosto' e un nop.

Questo utilizza la modalità in cui IgnoreSymbolStoreSequencePointsnon viene applicato. Se applicati, alcuni passaggi vengono comunque ignorati :(

Ecco l'output IL quando è stato applicato sopra:

  .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'

Aggiornamento 3:

Problema con "semi-fix" sopra. Peverify riporta errori su tutti i metodi a causa del nopdopo ret. Non capisco davvero il problema. Come può una nopverifica interruzione dopo a ret. È come un codice morto (tranne per il fatto che NON è nemmeno un codice) ... Vabbè, la sperimentazione continua.

Aggiornamento 4:

Tornato a casa ora, ho rimosso il codice "non verificabile", in esecuzione su VS2008 e le cose vanno molto peggio. Forse l'esecuzione di codice non verificabile per il corretto debugging potrebbe essere la risposta. In modalità 'rilascio', tutto l'output sarebbe comunque verificabile.

Aggiornamento 5:

Ora ho deciso che la mia idea di cui sopra è l'unica opzione praticabile per ora. Sebbene il codice generato non sia verificabile, devo ancora trovarne uno VerificationException. Non so quale sarà l'impatto sull'utente finale con questo scenario.

Come bonus, anche il mio secondo problema è stato risolto. :)

Ecco un piccolo screencast di quello che ho finito. Colpisce i punti di interruzione, fa il passo corretto (dentro / fuori / sopra), ecc. Tutto sommato, l'effetto desiderato.

Tuttavia, non sto ancora accettando questo come il modo di farlo. Mi sembra eccessivamente confuso. Avere una conferma sul vero problema sarebbe bello.

Aggiornamento 6:

Ho appena avuto la modifica per testare il codice su VS2010, sembra che ci siano alcuni problemi:

  1. La prima chiamata ora non passa correttamente. (asserzione-violazione ...) viene colpito. Altri casi funziona bene. Alcuni vecchi codici emettevano posizioni non necessarie. Rimosso il codice, funziona come previsto. :)
  2. Più seriamente, i punti di interruzione falliscono nella seconda invocazione del programma (usando la compilazione in memoria, il dumping dell'assembly su file sembra rendere di nuovo felici i punti di interruzione).

Entrambi questi casi funzionano correttamente con VS2008. La differenza principale è che in VS2010, l'intera applicazione viene compilata per .NET 4 e in VS2008, viene compilata in .NET 2. Entrambi in esecuzione a 64 bit.

Aggiornamento 7:

Come accennato, ho mdbg in esecuzione a 64 bit. Sfortunatamente, ha anche il problema del punto di interruzione in cui non riesce a rompersi se riesco a rieseguire il programma (questo implica che viene ricompilato, quindi non usando lo stesso assembly, ma usando sempre la stessa fonte).

Aggiornamento 8:

Ho segnalato un bug nel sito di MS Connect in merito al problema del punto di interruzione.

Aggiornamento: risolto

Aggiornamento 9:

Dopo una lunga riflessione, l'unico modo per rendere felice il debugger sembra essere SSA, quindi ogni passaggio può essere isolato e sequenziale. Devo ancora dimostrare questa nozione però. Ma sembra logico. Ovviamente, ripulire le temp da SSA interromperà il debug, ma è facile da attivare e lasciarle non ha molto sovraccarico.


Tutte le idee sono benvenute, le proverò e aggiungerò i risultati alla domanda. Prime 2 idee, prova mdbg e scrivi lo stesso codice in C # e confronta.
leppie,

6
Quale è la domanda?
George Stocker,

9
Penso che la sua domanda sia sulla falsariga di "perché la mia lingua non si muove correttamente?"
McKay,

1
Se non passi peverify non sarai in grado di eseguire in situazioni di attendibilità parziale, come da un'unità mappata o in molte configurazioni di hosting di plug-in. Come stai generando il tuo IL, reflection.Emit o tramite text + ilasm? In entrambi i casi puoi sempre inserire la .line 16707566,16707566: 0,0 '' in quel modo non devi preoccuparti che mettano un nop dopo il ritorno.
Hippiehunter il

@Hippiehunter: grazie. Sfortunatamente senza la nops, il passo fallisce (lo verificherò di nuovo per essere sicuro). È un sacrificio immagino che dovrei fare. Non è come se VS potesse funzionare anche senza i diritti di amministratore :) A proposito usando Reflection.Emit tramite il DLR (molto precocemente ramificato).
leppie,

Risposte:


26

Sono un ingegnere del team Debugger di Visual Studio.

Correggimi se sbaglio, ma sembra che l'unico problema rimasto sia che quando si passa dai PDB al formato del simbolo di compilazione dinamica .NET 4 mancano alcuni punti di interruzione.

Probabilmente avremmo bisogno di una riprogrammazione per diagnosticare esattamente il problema, tuttavia ecco alcune note che potrebbero aiutare.

  1. VS (2008+) può essere eseguito come non amministratore
  2. Qualche simbolo viene caricato per la seconda volta? È possibile eseguire il test inserendo (tramite eccezione o chiamando System.Diagnostic.Debugger.Break ())
  3. Supponendo che i simboli vengano caricati, c'è una riproduzione che potresti inviarci?
  4. La probabile differenza è che il formato del simbolo per il codice compilato in modo dinamico è diverso al 100% tra .NET 2 (flusso PDB) e .NET 4 (IL DB, secondo me lo hanno chiamato?)
  5. Il suono del 'nop è giusto. Vedi le regole per generare punti di sequenza impliciti di seguito.
  6. In realtà non è necessario emettere cose su linee diverse. Per impostazione predefinita, VS eseguirà le 'istruzioni-simbolo' dove, come autore del compilatore, puoi definire cosa significa 'istruzione-simbolo'. Quindi, se vuoi che ogni espressione sia una cosa separata nel file dei simboli, funzionerà perfettamente.

La JIT crea un punto di sequenza implicito basato sulle seguenti regole: 1. Istruzioni IL nop 2. Punti stack IL vuoti 3. L'istruzione IL immediatamente dopo un'istruzione call

Se risulta che abbiamo bisogno di una riprogrammazione per risolvere il tuo problema, puoi presentare un bug di connessione e caricare i file in modo sicuro attraverso quel supporto.

Aggiornare:

Stiamo incoraggiando altri utenti a riscontrare questo problema a provare l'anteprima per sviluppatori di Dev11 da http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=27543 e commentare con qualsiasi feedback. (Deve target 4.5)

Aggiornamento 2:

Leppie ha verificato la correzione per funzionare con lui sulla versione Beta di Dev11 disponibile all'indirizzo http://www.microsoft.com/visualstudio/11/en-us/downloads come indicato nel bug di connessione https://connect.microsoft. com / VisualStudio / feedback / dettagli / 684089 / .

Grazie,

Luca


Molte grazie. Proverò a fare repro, se necessario.
leppie,

Per quanto riguarda la conferma, sì, hai ragione, ma preferirei comunque risolvere i problemi di stepping senza interrompere la verificabilità. Ma, come detto, sono disposto a sacrificare che se potessi provare che non lancerà mai un runtime VerificationException.
leppie,

Per quanto riguarda il punto 6, ho semplicemente reso "basato sulla linea" per questa domanda. Il debugger infatti esegue correttamente le espressioni in / out / over come intendo che funzioni :) L'unico problema con l'approccio è impostare punti di interruzione sulle espressioni "interne" se un'espressione "esterna" la copre; il debugger in VS tende a voler rendere l'espressione più esterna come il punto di interruzione.
leppie,

Penso di aver isolato il problema del punto di interruzione. Durante l'esecuzione in CLR2 i punti di interruzione sembrano essere rivalutati quando si esegue nuovamente il codice, anche se nuovo codice. Su CLR4, i punti di interruzione si "attaccano" solo al codice originale. Ho realizzato un piccolo screencast sul comportamento CLR4 @ screencast.com/t/eiSilNzL5Nr . Si noti che il nome del tipo cambia tra le invocazioni.
leppie,

Ho segnalato un bug su connect. La riproduzione è abbastanza semplice :) connect.microsoft.com/VisualStudio/feedback/details/684089
leppie

3

Sono un ingegnere del team Debugger di SharpDevelop :-)

Hai risolto il problema?

Hai provato a eseguire il debug in SharpDevelop? Se c'è un bug in .NET, mi chiedo se dobbiamo implementare qualche soluzione alternativa. Non sono a conoscenza di questo problema.

Hai provato a eseguire il debug in ILSpy? Soprattutto senza simboli di debug. Eseguirà il debug del codice C #, ma ci direbbe se le istruzioni di IL sono correttamente eseguibili. (Ricorda che il debugger di ILSpy è beta però)

Note rapide sul codice IL originale:

  • .line 19,19: 6,15 '' si verifica due volte?
  • .line 20,20: 7,14 '' non si avvia sul punto di sequenza implicita (la pila non è vuota). sono preoccupato
  • .line 20,20: 7,14 '' include il codice per "car x" (buono) e "#f nooo x" (cattivo?)
  • per quanto riguarda il nop dopo ret. Che dire di stloc, ldloc, ret? Penso che C # usi questo trucco per rendere ret un punto di sequenza distinto.

David


+1 Grazie per il feedback, come primo punto, sfortunato effetto collaterale del generatore di codice, ma sembra innocuo. Secondo punto, questo è esattamente ciò di cui ho bisogno, ma vedo il tuo punto, lo esaminerò. Non sono sicuro di cosa intendi per ultimo punto.
leppie,

Il terzo punto è che .line 22,22: 7,40 '' dovrebbe essere prima di IL_0020. Oppure dovrebbe esserci qualcosa prima di IL_0020, altrimenti il ​​codice conta comunque come .line 20,20: 7,14 ''. Il quarto punto è "ret, nop" potrebbe essere sostituito con "sloc, .line, ldloc, ret". Ho già visto lo schema prima, forse era ridondante, ma forse aveva un motivo.
dsrbecky,

Non posso fare lo stloc, ldloc prima di ret, poiché perderò la chiamata in coda. Ah, ho capito, ti riferivi all'output originale?
Leppie,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.