Come funziona un debugger?


170

Continuo a chiedermi come funziona un debugger? In particolare quello che può essere "collegato" all'eseguibile già in esecuzione. Capisco che il compilatore traduca il codice in linguaggio macchina, ma allora come fa il debugger a "sapere" a cosa viene associato?


5
L'articolo di Eli è stato spostato su eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1
Oktalist

@Oktalist Questo articolo è interessante ma parla solo dell'astrazione a livello di API per il debug su Linux. Immagino che OP voglia saperne di più su under the hood.
smwikipedia,

Risposte:


96

I dettagli su come funziona un debugger dipenderanno dal debug e dal sistema operativo. Per il debug nativo su Windows è possibile trovare alcuni dettagli su MSDN: API di debug Win32 .

L'utente comunica al debugger a quale processo collegarsi, per nome o per ID processo. Se è un nome, il debugger cercherà l'ID del processo e avvierà la sessione di debug tramite una chiamata di sistema; sotto Windows questo sarebbe DebugActiveProcess .

Una volta collegato, il debugger entrerà in un loop di eventi simile a qualsiasi interfaccia utente, ma invece di eventi provenienti dal sistema a finestre, il sistema operativo genererà eventi in base a ciò che accade nel processo in fase di debug, ad esempio si verifica un'eccezione. Vedi WaitForDebugEvent .

Il debugger è in grado di leggere e scrivere la memoria virtuale del processo di destinazione e persino di regolare i suoi valori di registro tramite le API fornite dal sistema operativo. Vedi l'elenco delle funzioni di debug per Windows.

Il debugger è in grado di utilizzare le informazioni dai file dei simboli per tradurre da indirizzi a nomi di variabili e posizioni nel codice sorgente. Le informazioni sul file dei simboli sono un insieme separato di API e non sono una parte fondamentale del sistema operativo in quanto tale. Su Windows questo avviene tramite Debug Interface Access SDK .

Se si sta eseguendo il debug di un ambiente gestito (.NET, Java, ecc.) Il processo apparirà in genere simile, ma i dettagli sono diversi, poiché l'ambiente della macchina virtuale fornisce l'API di debug anziché il sistema operativo sottostante.


5
Questa domanda può sembrare stupida, ma come tiene traccia del sistema operativo se viene raggiunto un indirizzo specifico all'interno del programma. Ad esempio un breackpoint è impostato sull'indirizzo 0x7710cafe. Man mano che il puntatore dell'istruzione cambia, il sistema operativo (o forse la CPU) dovrà confrontare il puntatore dell'istruzione con tutti gli indirizzi del punto di interruzione, o sbaglio? Come funziona ..?
displayname

3
@StefanFalk Ho scritto una risposta che affronta alcuni dei dettagli di livello inferiore (su x86).
Jonathon Reinhart,

Potresti spiegare in che modo esattamente i nomi delle variabili sono associati agli indirizzi? L'applicazione utilizza gli stessi indirizzi di memoria per le variabili ogni volta che viene eseguita? Ho sempre pensato che fosse appena stato mappato dalla memoria disponibile, ma non avevo mai pensato davvero se i byte sarebbero stati mappati direttamente allo stesso punto nello spazio di memoria dell'app. Sembra che sarebbe un grosso problema di sicurezza.
James Joshua Street,

@JamesJoshuaStreet Immagino che sia un dettaglio specifico del debugger.
moonman239

Questa risposta rivela qualcosa. Ma penso che op sia più interessato ad alcuni dettagli di basso livello piuttosto che ad alcune astrazioni dell'API.
smwikipedia,

63

A quanto ho capito:

Per i punti di interruzione del software su x86, il debugger sostituisce il primo byte dell'istruzione con CC( int3). Questo viene fatto con WriteProcessMemorysu Windows. Quando la CPU arriva a quell'istruzione ed esegue il int3, questo fa sì che la CPU generi un'eccezione di debug. Il sistema operativo riceve questo interrupt, si rende conto che il processo è in fase di debug e notifica al processo di debug che è stato raggiunto il punto di interruzione.

Dopo che il punto di interruzione viene raggiunto e il processo viene arrestato, il debugger cerca nel suo elenco di punti di interruzione e sostituisce CCil byte con il byte originariamente presente. Il debugger imposta TF, il Flag trap in EFLAGS(modificando il CONTEXT) e continua il processo. Il Flag trap fa sì che la CPU generi automaticamente un'eccezione a passo singolo ( INT 1) sull'istruzione successiva.

Quando il processo in fase di debug si interrompe la volta successiva, il debugger sostituisce nuovamente il primo byte dell'istruzione breakpoint CCe il processo continua.

Non sono sicuro se questo è esattamente come è implementato da tutti i debugger, ma ho scritto un programma Win32 che riesce a eseguire il debug stesso usando questo meccanismo. Completamente inutile, ma educativo.


25

In Linux, il debug di un processo inizia con la chiamata di sistema ptrace (2) . Questo articolo ha un ottimo tutorial su come utilizzare ptraceper implementare alcuni semplici costrutti di debug.


1
Condivide la (2)dirci qualcosa di più (o meno) che "ptrace è una chiamata di sistema"?
Lazer,

5
@eSKay, no non proprio. Il (2)è il numero della sezione manuale. Vedi en.wikipedia.org/wiki/Man_page#Manual_sections per una descrizione delle sezioni del manuale.
Adam Rosenfield,

2
@AdamRosenfield Ad eccezione del fatto che la sezione 2 è specificamente "Chiamate di sistema". Quindi indirettamente, sì, ci dice che ptraceè una chiamata di sistema.
Jonathon Reinhart,

1
Più praticamente, (2)ci dice che possiamo digitare man 2 ptracee ottenere la manpage giusta - non è importante qui perché non c'è altro ptraceda chiarire, ma per confrontarlo man printfcon man 3 printfLinux.
slim

9

Se utilizzi un sistema operativo Windows, una grande risorsa per questo sarebbe "Debug delle applicazioni per Microsoft .NET e Microsoft Windows" di John Robbins:

(o anche l'edizione precedente: "Debugging Applications" )

Il libro ha un capitolo su come funziona un debugger che include il codice per un paio di debugger semplici (ma funzionanti).

Dal momento che non ho familiarità con i dettagli del debug Unix / Linux, questa roba potrebbe non essere applicabile ad altri sistemi operativi. Ma immagino che come introduzione a un argomento molto complesso i concetti - se non i dettagli e le API - dovrebbero "port" su quasi tutti i sistemi operativi.


3

Un'altra fonte preziosa per comprendere il debug è il manuale della CPU Intel (Manuale dello sviluppatore del software per architetture Intel® 64 e IA-32). Nel volume 3A, capitolo 16, è stato introdotto il supporto hardware del debug, come eccezioni speciali e registri di debug dell'hardware. Segue quel capitolo:

Flag T (trap), TSS: genera un'eccezione di debug (#DB) quando si tenta di passare a un'attività con il flag T impostato nel relativo TSS.

Non sono sicuro che Window o Linux utilizzino o meno questo flag, ma è molto interessante leggere quel capitolo.

Spero che questo aiuti qualcuno.


2

Penso che ci siano due domande principali a cui rispondere qui:

1. In che modo il debugger sa che si è verificata un'eccezione?

Quando si verifica un'eccezione in un processo in fase di debug, il debugger viene avvisato dal sistema operativo prima che qualsiasi gestore di eccezioni utente definito nel processo di destinazione abbia la possibilità di rispondere all'eccezione. Se il debugger sceglie di non gestire questa notifica (prima possibilità) dell'eccezione, la sequenza di dispacciamento dell'eccezione procede ulteriormente e al thread di destinazione viene quindi data la possibilità di gestire l'eccezione se lo desidera. Se l'eccezione SEH non viene gestita dal processo di destinazione, al debugger viene quindi inviato un altro evento di debug, chiamato notifica di seconda possibilità, per informarlo che si è verificata un'eccezione non gestita nel processo di destinazione. fonte

inserisci qui la descrizione dell'immagine


2. Come il debugger sa come fermarsi su un breakpoint?

La risposta semplificata è: quando si inserisce un punto di interruzione nel programma, il debugger sostituisce il codice in quel punto con un'istruzione int3 che è un interruzione del software . Di conseguenza, il programma viene sospeso e viene chiamato il debugger.


1

La mia comprensione è che quando compili un'applicazione o un file DLL, qualunque cosa compili contiene simboli che rappresentano le funzioni e le variabili.

Quando si dispone di una build di debug, questi simboli sono molto più dettagliati rispetto a quando si tratta di una build di rilascio, consentendo così al debugger di fornire maggiori informazioni. Quando si collega il debugger a un processo, guarda a quali funzioni si sta accedendo e risolve tutti i simboli di debug disponibili da qui (poiché sa come appaiono gli interni del file compilato, si può verificare quale potrebbe essere nella memoria , con contenuti di ints, float, stringhe, ecc.). Come ha detto il primo poster, queste informazioni e il modo in cui funzionano questi simboli dipendono fortemente dall'ambiente e dalla lingua.


2
Si tratta solo di simboli. C'è molto, molto di più nel debug dei simboli.
Jonathon Reinhart,
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.