Condividi tramite


WinDbg: Sequenze temporali

logo WinDbg con una lente di ingrandimento che controlla i bit.

Il debug TTD (Time Travel Debugging) consente agli utenti di registrare tracce, che sono registrazioni dell'esecuzione di un programma. Le sequenze temporali sono una rappresentazione visiva degli eventi che si verificano durante l'esecuzione. Questi eventi possono essere punti di interruzione, lettura/scrittura della memoria, chiamate e ritorni di funzione, ed eccezioni.

Screenshot delle sequenze temporali nel debugger che visualizza eccezioni, accesso alla memoria, punti di interruzione e chiamate di funzione.

Usare la finestra Sequenze temporali per visualizzare eventi importanti, comprendere la posizione relativa e passare facilmente alla posizione nel file di traccia TTD. Usare più sequenze temporali per esplorare visivamente gli eventi nella traccia di viaggio temporale e individuare la correlazione degli eventi.

La finestra Sequenze temporali viene visualizzata quando si apre un file di traccia TTD. Mostra gli eventi chiave senza dover creare manualmente query del modello di dati. Allo stesso tempo, tutti gli oggetti di spostamento temporale sono disponibili per consentire query di dati più complesse.

Per ulteriori informazioni su come creare e usare i file di traccia del viaggio nel tempo, vedere Time Travel Debugging: Overview.

Tipi di sequenze temporali

La finestra Sequenze temporali mostra gli eventi nelle sequenze temporali seguenti:

  • Eccezioni: è possibile filtrare in base a un codice di eccezione specifico.
  • Punti di interruzione: è possibile visualizzare quando i punti di interruzione raggiungono una sequenza temporale.
  • Accessi alla memoria: è possibile leggere, scrivere ed eseguire tra due indirizzi di memoria.
  • Chiamate di funzione: è possibile eseguire ricerche sotto forma di module!function.

Passare il puntatore del mouse su ogni evento per ottenere ulteriori informazioni tramite il tooltip. La selezione di un evento esegue la query per l'evento e visualizza altre informazioni. Facendo doppio clic su un evento si passa a tale posizione nel file di traccia TTD.

Eccezioni

Quando si carica un file di traccia e la sequenza temporale è attiva, vengono visualizzate automaticamente eventuali eccezioni nella registrazione.

Quando si passa il puntatore del mouse su un punto di interruzione, vengono visualizzate informazioni quali il tipo di eccezione e il codice eccezione.

Screenshot di una sequenza temporale nel debugger che mostra le eccezioni con informazioni su un codice di eccezione specifico.

È possibile filtrare ulteriormente in base a un codice di eccezione specifico usando il campo facoltativo Codice eccezione .

Screenshot di una finestra di dialogo di eccezione del debugger della sequenza temporale, con il Tipo della Sequenza Temporale impostato su Eccezioni e il Codice Eccezione impostato su 0xC0000004.

È anche possibile aggiungere una nuova sequenza temporale per un tipo di eccezione specifico.

Punti di interruzione

Dopo aver aggiunto un punto di interruzione, le posizioni nella sequenza temporale mostrano quando viene raggiunto il punto di interruzione. Ad esempio, puoi usare il comando bp Set Breakpoint. Quando si passa il puntatore del mouse su un punto di interruzione, vengono visualizzati l'indirizzo e il puntatore alle istruzioni associati al punto di interruzione.

Screenshot di una sequenza temporale nel debugger che mostra circa 30 indicatori di punti di interruzione.

Quando il punto di interruzione viene cancellato, la sequenza temporale del punto di interruzione associata viene rimossa automaticamente.

Chiamate di funzione

È possibile visualizzare le posizioni delle chiamate di funzione nella sequenza temporale. A tale scopo, specificare la ricerca sotto forma di module!function. Un esempio è TimelineTestCode!multiplyTwo. È anche possibile specificare caratteri jolly, ad esempio TimelineTestCode!m*.

Screenshot dell'aggiunta di una sequenza temporale nel debugger con un nome di chiamata di funzione immesso.

Quando si passa il puntatore del mouse su una chiamata di funzione, vengono visualizzati il nome della funzione, i parametri di input, i relativi valori e il valore restituito. Questo esempio mostra il buffer e le dimensioni perché sono i parametri per DisplayGreeting!GetCppConGreeting.

Screenshot di una sequenza temporale nel debugger che visualizza le chiamate di funzione e la finestra Registri.

Accesso alla memoria

Usare la sequenza temporale degli accessi alla memoria per verificare quando è stata letta o scritta un intervallo di memoria specifico o la posizione in cui è stata eseguita l'esecuzione del codice. Gli indirizzi di avvio e arresto vengono usati per definire un intervallo tra due indirizzi di memoria.

Screenshot dell'aggiunta di una finestra di dialogo Accesso alla memoria con l'opzione Scrittura selezionata.

Quando si passa il puntatore del mouse su un elemento di accesso alla memoria, viene visualizzato il valore e il puntatore all'istruzione.

Screenshot di una sequenza temporale nel debugger che mostra gli eventi di accesso alla memoria.

Lavorare con linee temporali

Una linea grigia verticale segue il cursore quando si passa il puntatore del mouse sulla sequenza temporale. La linea blu verticale indica la posizione corrente nella traccia.

Selezionare le icone della lente di ingrandimento per ingrandire e ridurre la sequenza temporale.

Nell'area di controllo della sequenza temporale superiore, usare il rettangolo per spostare la visualizzazione della sequenza temporale. Trascinare i delimitatori esterni del rettangolo per ridimensionare la visualizzazione della sequenza temporale corrente.

Screenshot di una sequenza temporale nel debugger che mostra l'area superiore usata per selezionare la visualizzazione attiva.

Movimenti del mouse

Per eseguire lo zoom avanti e indietro, selezionare CTRL e usare la rotellina di scorrimento.

Per scorrere da un lato all'altro, selezionare Maiusc e usare la rotellina di scorrimento.

Tecniche di debug della cronologia

Per illustrare le tecniche di cronologia di debug, la procedura dettagliata di Time Travel Debugging viene riutilizzata qui. Questa dimostrazione presuppone che i primi due passaggi siano stati completati per compilare il codice di esempio e aver creato la registrazione TTD usando i primi due passaggi descritti in questa sezione.

In questo scenario, il primo passaggio consiste nel trovare l'eccezione nella traccia di viaggio temporale. Fare doppio clic sull'unica eccezione visualizzata nella sequenza temporale.

Nella finestra Comando è possibile osservare che il comando seguente è stato eseguito quando è stata selezionata l'eccezione.

(2dcc.6600): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: CC:0
@$curprocess.TTD.Events.Where(t => t.Type == "Exception")[0x0].Position.SeekTo()

Selezionare Visualizza>registri per visualizzare i registri a questo punto nella sequenza temporale per avviare l'indagine.

Screenshot di una sequenza temporale nel debugger che mostra l'eccezione del laboratorio dimostrativo e la finestra dei Registri.

Nell'output del comando, lo stack (esp) e il puntatore di base (ebp) puntano verso indirizzi differenti. Questa discrepanza potrebbe indicare il danneggiamento dello stack. Probabilmente, una funzione ha restituito e quindi ha danneggiato lo stack. Per convalidare questo problema, tornare indietro prima che lo stato della CPU sia danneggiato e verificare se è possibile determinare quando si è verificato il danneggiamento dello stack.

Durante l'esecuzione di questo processo, esaminando i valori delle variabili locali e dello stack:

  • Selezionare Visualizza>variabili locali per visualizzare i valori locali.
  • Selezionare Visualizza>stack per visualizzare lo stack di esecuzione del codice.

Al momento dell'errore, nella traccia è comune terminare alcuni passaggi dopo la vera causa nel codice di gestione degli errori. Con il viaggio nel tempo, è possibile tornare indietro un'istruzione alla volta per individuare la vera causa radice.

Nella barra multifunzione Home, usare il comando Passo indietro per tornare indietro di tre istruzioni. Durante questa operazione, continuare a esaminare le finestre Stack, Variabili locali e Registri .

La finestra Comando mostra la posizione temporale e i registri mentre si torna indietro di tre passi nelle istruzioni.

0:000> t-
Time Travel Position: CB:41
eax=00000000 ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=00540020 esp=003cf7d0 ebp=00520055 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
00540020 ??              ???
0:000> t-
Time Travel Position: CB:40
eax=00000000 ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=00061767 esp=003cf7cc ebp=00520055 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
DisplayGreeting!main+0x57:
00061767 c3              ret
0:000> t-
Time Travel Position: CB:3A
eax=0000004c ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=0006175f esp=003cf718 ebp=003cf7c8 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
DisplayGreeting!main+0x4f:
0006175f 33c0            xor     eax,eax

A questo punto del tracciamento, lo stack e il puntatore di base hanno valori più comprensibili. Sembra che ci si avvicini al punto nel codice in cui si è verificato il danneggiamento.

esp=003cf718 ebp=003cf7c8

La finestra Variabili locali contiene i valori dell'app di destinazione. La finestra Codice sorgente evidenzia la riga di codice che è pronta per l'esecuzione nel codice sorgente a questo punto della traccia.

Per ulteriori indagini, aprire una finestra Memoria per visualizzare il contenuto vicino all'indirizzo di memoria del puntatore dello stack (esp). In questo esempio ha un valore pari a 003cf7c8. Selezionare Memoria>Testo>ASCII per visualizzare il testo ASCII memorizzato in tale indirizzo.

Screenshot del debugger che mostra le finestre Registri, Stack e Memoria.

Sequenza temporale di accesso alla memoria

Dopo aver identificato una posizione di memoria di interesse, usare tale valore per aggiungere una sequenza temporale di accesso alla memoria. Selezionare + Aggiungi sequenza temporale e compilare l'indirizzo iniziale. Esaminare 4 byte in modo che, quando li si aggiunge al valore dell'indirizzo iniziale di 003cf7c8, si ottenga un valore dell'indirizzo finale pari a 003cf7cb. L'impostazione predefinita consiste nell'esaminare tutte le scritture di memoria, ma è anche possibile esaminare solo operazioni di scrittura o esecuzione del codice in tale indirizzo.

Screenshot dell'aggiunta di una finestra di dialogo per l'accesso alla memoria nella sequenza temporale con l'opzione di Scrittura selezionata e il valore iniziale 003cf7c8.

Ora è possibile percorrere la sequenza temporale al contrario per esaminare a quale punto in questa traccia di viaggio nel tempo questa locazione di memoria è stata scritta per scoprire cosa si può trovare. Quando si seleziona questa posizione nella sequenza temporale, è possibile notare che i valori locali hanno diversi valori per la stringa che viene copiata. Il valore di destinazione sembra incompleto, come se la lunghezza della stringa non sia corretta.

Screenshot della sequenza temporale di accesso alla memoria e della finestra Variabili locali che mostra i valori delle variabili locali con valori di origine e di destinazione diversi.

Sequenza temporale del punto di interruzione

L'uso dei punti di interruzione è un approccio comune per sospendere l'esecuzione del codice a un certo evento di interesse. Con TTD è possibile impostare un punto di interruzione e tornare indietro nel tempo fino a quando tale punto di interruzione non viene raggiunto dopo la registrazione della traccia. La possibilità di esaminare lo stato del processo dopo che si verifica un problema, per determinare la posizione migliore per un punto di interruzione, consente un maggior numero di flussi di lavoro di debug univoci per TTD.

Per esplorare una tecnica alternativa di debug del timeline, selezionare l'eccezione nel timeline e tornare indietro di tre passaggi usando il comando Passa all'indietro nella barra multifunzione Home.

In questo piccolo esempio è facile esaminare il codice. Se sono presenti centinaia di righe di codice e decine di subroutine, usare le tecniche descritte qui per ridurre il tempo necessario per individuare il problema.

Come accennato in precedenza, il puntatore di base (esp) punta al testo del messaggio anziché puntare a un'istruzione.

Usare il ba comando per impostare un punto di interruzione sull'accesso alla memoria. Hai impostato un w - punto di interruzione di scrittura per vedere quando questa area di memoria viene scritta.

0:000> ba w4 003cf7c8

Anche se si usa un semplice punto di interruzione dell'accesso alla memoria, è possibile costruire punti di interruzione per essere istruzioni condizionali più complesse. Per altre informazioni, vedere bp, bu, bm (Set Breakpoint).

Nel menu Home selezionare Torna indietro per tornare indietro nel tempo fino a quando non viene raggiunto il punto di interruzione.

A questo punto, è possibile esaminare lo stack di programmi per vedere il codice attivo.

Screenshot di una sequenza temporale nel debugger che mostra le sequenze temporali di accesso alla memoria e le finestre Stack.

Poiché è improbabile che la funzione fornita da wscpy_s() Microsoft abbia un bug di codice simile al seguente, esaminare ulteriormente lo stack. Lo stack mostra che Greeting!main chiama Greeting!GetCppConGreeting. Nell'esempio di codice di piccole dimensioni è possibile aprire il codice a questo punto e probabilmente trovare facilmente l'errore. Tuttavia, per illustrare le tecniche che è possibile usare con un programma più grande e più complesso, si aggiunge una sequenza temporale delle chiamate di funzione.

Sequenza temporale delle chiamate di funzione

Selezionare + Aggiungi sequenza temporale e compilare DisplayGreeting!GetCppConGreeting la stringa di ricerca della funzione.

Le caselle di controllo Posizione iniziale e Posizione finale indicano l'inizio e la fine di una chiamata di funzione nella traccia.

È possibile usare il comando dx per visualizzare l'oggetto di chiamata di funzione e per vedere i campi associati TimeStart e TimeEnd che corrispondono alla posizione iniziale e alla posizione finale della chiamata di funzione.

dx @$cursession.TTD.Calls("DisplayGreeting!GetCppConGreeting")[0x0]
    EventType        : 0x0
    ThreadId         : 0x6600
    UniqueThreadId   : 0x2
    TimeStart        : 6D:BD [Time Travel]
    SystemTimeStart  : Thursday, October 31, 2019 23:36:05
    TimeEnd          : 6D:742 [Time Travel]
    SystemTimeEnd    : Thursday, October 31, 2019 23:36:05
    Function         : DisplayGreeting!GetCppConGreeting
    FunctionAddress  : 0x615a0
    ReturnAddress    : 0x61746
    Parameters  

È necessario selezionare una delle caselle di controllo Posizione iniziale o Posizione finale o entrambe le caselle di controllo Posizione iniziale e Posizione finale .

Screenshot della finestra di dialogo Aggiungi nuova cronologia che mostra l'aggiunta di una cronologia delle chiamate di funzione con una stringa di ricerca di funzione DisplayGreeting!GetCppConGreeting.

Il codice non è ricorsivo o rientrante, quindi è facile individuare la sequenza temporale quando viene chiamato il GetCppConGreeting metodo . La chiamata a GetCppConGreeting viene eseguita anche contemporaneamente al punto di interruzione e all'evento di accesso alla memoria definito. Quindi sembra che tu abbia identificato un'area di codice da esaminare attentamente per individuare la causa principale del crash dell'applicazione.

Screenshot di una timeline nel debugger che mostra la sequenza temporale di accesso alla memoria e la finestra delle Variabili locali con un messaggio e un buffer contenente valori di stringa diversi.

Esplorare l'esecuzione del codice visualizzando più sequenze temporali

Anche se l'esempio di codice è ridotto, la tecnica di uso di più sequenze temporali consente l'esplorazione visiva di una traccia di viaggio temporale. È possibile esaminare il file di traccia per porre domande, ad esempio "Quando si accede a un'area di memoria prima che venga raggiunto un punto di interruzione?".

Screenshot di una sequenza temporale nel debugger che mostra una sequenza temporale degli accessi alla memoria e la finestra Locali.

La possibilità di visualizzare più correlazioni e trovare elementi imprevisti differenzia lo strumento della sequenza temporale dall'interazione con la traccia di spostamento temporale usando i comandi della riga di comando.

Segnalibri della sequenza temporale

Aggiungere un segnalibro alle posizioni di spostamento del tempo importanti in WinDbg invece di copiare e incollare manualmente la posizione nel Blocco note. I segnalibri semplificano la visualizzazione a colpo d'occhio di posizioni diverse nella traccia rispetto ad altri eventi e per annotarle.

È possibile specificare un nome descrittivo per i segnalibri.

Screenshot della finestra di dialogo di Nuovo segnalibro che mostra un nome di esempio per la prima chiamata API nell'app Visualizza messaggio di saluto.

Selezionare Visualizza>sequenza temporale per aprire la finestra Sequenze temporali in modo da poter accedere alla sequenza temporale segnalibri . Quando si passa il puntatore del mouse su un segnalibro, viene visualizzato il nome del segnalibro.

Screenshot della sequenza temporale che mostra tre segnalibri con il cursore che passa sopra di uno per visualizzare il nome del segnalibro.

Fare clic con il pulsante destro del mouse sul segnalibro per spostarsi nella posizione del segnalibro, rinominare il segnalibro o eliminarlo.

Screenshot del menu a comparsa Segnalibri che mostra le opzioni per spostarsi in posizione, modificare e rimuovere.

Annotazioni

La funzionalità segnalibro non è disponibile nella versione 1.2402.24001.0 di WinDbg.