|
Dr. Carlo Pescio Soft-ICE/W, un debugger a basso livello per Windows |
Pubblicato su Computer Programming No. 43
Introduzione
Soft-ICE/W, prodotto da Nu-Mega Technologies, è un debugger
a basso livello per Windows, operante in modalità 386 enhanced.
Cosa si intende per debugger a basso livello sarà chiarito
meglio in seguito: in breve, si tratta di un debugger a livello
di sistema, che viene caricato prima di Windows stesso, e che
consente pertanto il debugging di programmi, DLL, device drivers,
VxD, programmi DOS nelle loro macchine virtuali (inclusi TSR e
drivers), nonché del codice di Windows stesso. Soft-ICE/W
è attivabile in ogni momento tramite la pressione di un
hot-key, consente il debugging in formato sorgente se è
disponibile una tavola dei simboli, ma permette anche di eseguire
il codice di un VxD di cui non si abbia il sorgente, ovviamente
presentando sul video il codice assembler.
Soft-ICE/W è stato progettato per fornire sofisticate possibilità
a livello di breakpoint: è possibile richiedere un breakpoint
sull'esecuzione di una istruzione, sull'accesso ad una locazione
di memoria, ad una porta di I/O, all'arrivo di un interrupt; I
breakpoint vengono automaticamente aggiornati quando Windows esegue
lo swapping ed il reloading della memoria.
Per le sue caratteristiche di debugger a basso livello, in grado
di tracciare passo passo anche il codice del kernel di Windows,
Soft-ICE/W non può allocare dinamicamente alcuna risorsa
di sistema, come il disco o la memoria; tutte le risorse necessarie
vengono pertanto acquisite al momento dello startup del programma,
tranne il codice sorgente del programma sotto debugging, che può
essere "iniettato" in seguito tramite un programma ausiliario.
Installazione
Soft-ICE/W è contenuto in un unico dischetto, e richiede
poco più di un megabyte su disco: quasi l'eccezione tra
gli strumenti di sviluppo, che ormai richiedono quasi sempre diversi
megabyte. La configurazione minima in termini di memoria è
di 256 KB liberi oltre a quelli utilizzati da Windows: tuttavia,
poiché tutta la memoria usata per i simboli del programma
sotto debugging deve essere allocata al momento dello startup,
la configurazione richiesta è in funzione della lunghezza
dei programmi da debuggare a livello di sorgente. Come anticipato,
Soft-ICE/W richiede una macchina in grado di eseguire Windows
in modalità 386 enhanced, quindi un 386 o superiori.
L'installazione si riduce alla copia dei file dal dischetto ad
una directory che sia nel path; è poi necessario lanciare
il programma ICONS.EXE per creare il relativo gruppo di icone
nel program manager. Questa filosofia spartana si propaga anche
alla documentazione: con il programma viene fornito un manuale
ad anelli di circa 300 pagine, ma nonostante questo molto scarno
ed essenziale. Il manuale ha funzione di reference per i numerosi
comandi, e non contiene una sezione tutorial sull'uso degli stessi.
È comunque fornito a corredo un libretto aggiuntivo, "Debugging
Windows with Soft-ICE/W" di Martin Heller, che copre in parte
le lacune del manuale dal punto di vista dell'introduzione all'uso
dello strumento.
Utilizzo dello strumento
Soft-ICE/W si carica lanciando WINICE.EXE al prompt del DOS: questo
provvederà automaticamente a lanciare Windows, allocando
preventivamente le risorse specificate nel file WINICE.DAT in
termini di memoria, file di simboli da pre-caricare, e così
via.
Il debugger lavora necessariamente in pagina testo, in quanto
non può in alcun modo sfruttare le risorse del sistema;
lo switching tra modalità grafica e testo è gestito
da un apposito driver, che tuttavia viene fornito solo per alcune
schede grafiche (a dire il vero ben poche). È inoltre possibile
utilizzare un secondo monitor collegato ad una scheda monocromatica
(recuperabile su un vecchio computer) o un secondo computer, utilizzato
in pratica come terminale connesso alla porta seriale; quest'ultima
opzione è pressoché obbligatoria quando la scheda
non è supportata direttamente e non può convivere
con una scheda monocromatica, come ad esempio la Matrox MGA II
che utilizzo normalmente. Va detto che la modalità di uso
preferita è proprio quella con secondo monitor, in quanto
consente la massima velocità nel refresh delle informazioni
a video, minimizzando l'interferenza del debugger con i programmi
attivi.
L'interfaccia è molto essenziale: come si può vedere
in figura 1, la pagina è suddivisa in più finestre
(registri, watch, dati, codice, comandi), analogamente a quanto
avviene in molti debugger simbolici. L'unica differenza sensibile
sta proprio nella finestra comandi, in quanto Soft-ICE/W non supporta
menu a tendina, mouse, o altre forme di interfaccia user-friendly:
tutta l'interazione avviene tramite comandi, o con combinazioni
di tasti che emulano un comando. Pertanto, per posizionare un
breakpoint in esecuzione occorre ad esempio utilizzare il comando
BPX seguito dall'indirizzo desiderato. Ovviamente, l'uso di hot
key è pressoché indispensabile per i comandi più
utilizzati, come il single step; tuttavia siamo abbastanza lontani
dall'immediatezza di uso di strumenti come il Turbo Debugger di
Borland o altri ambienti che, pur in pagina testo, sono basati
su una interfaccia più intuitiva.
Analogamente, essendo Soft-ICE/W indipendente dal linguaggio (purché
venga generata una mappa dei simboli), mancano molte delle caratteristiche
più comode di ambienti di debugging specializzati, come
la possibilità di esaminare oggetti C++ complessi tramite
un browser: in Soft-ICE/W, si ritorna a dover esaminare dei semplici
dump di zone di memoria; per contro, Soft-ICE/W è in grado
di svolgere compiti per i quali i debugger di più alto
livello non sono indicati, come vedremo tra breve.
Soft-ICE/W può essere attivato in ogni istante premendo
Ctrl-D (combinazione comunque riconfigurabile), ma l'utilizzo
più comune prevede il caricamento del programma sotto
debugging attraverso un'utility separata chiamata WLDR (Windows
Loader, vedi figura 2). Attraverso il WLDR è possibile
pre-caricare i soli simboli di un EXE o DLL, oppure lanciare il
programma caricando anche i simboli. In questo caso, immediatamente
prima dello startup del programma, il debugger si attiverà
automaticamente per permettere di posizionare breakpoints e così
via. Anche in questo caso, se il codice è suddiviso in
più file è necessario ricorrere ad un apposito
comando (FILE) per caricare il file sorgente desiderato nella
finestra del codice. WLDR può essere lanciato anche all'interno
di una shell DOS, nel qual caso consente di caricare un programma
DOS da debuggare all'interno della macchina virtuale; questa possibilità
è molto interessante per chiunque debba sottoporre a debugging
programmi DOS che cooperino con programmi Windows, sia a livello
di device driver che a livello di applicazione.
La sequenza di caricamento è invece diversa per i VxD o
i device driver, che vanno pre-caricati includendone il nome nel
file WINICE.DAT, attraverso il comando Load=<filename>;
maggiori dettagli sul debugging dei VxD verranno dati più
avanti.
In ogni istante, è possibile lanciare il programma Windows
WLOG, che scrive su file un log degli ultimi comandi utilizzati;
si tratta di uno strumento utile ma troppo limitato, in quanto
nel log non compare alcun tipo di informazione aggiuntiva, come
i valori dei registri, lo stack o la parte di codice attiva. Può
quindi essere utile per mantenere una traccia del lavoro svolto
e poterlo così replicare, ma richiederebbe un consistente
miglioramento per essere realmente utile.
10 casi reali di utilizzo
Soft-ICE/W è uno strumento di eccezionale flessibilità,
nonostante la sua interfaccia utente spartana lo renda inizialmente
un pò ostico da utilizzare; l'utilizzatore si trova di
fronte ad un considerevole numero di comandi molto potenti, e
non è immediatamente chiaro come questi possano essere
sfruttati nelle diverse occasioni.
Proprio per questo, ho ritenuto interessante dimostrare come Soft-ICE/W
possa tornare utile nei casi più disparati, riassumendo
dieci casi reali tratti dalla mia esperienza personale con il
tool durante gli ultimi anni.
1) Uscire da un ciclo infinito
Consideriamo il seguente programma:
#include < windows.h >
int PASCAL WinMain( HINSTANCE,
HINSTANCE,
LPSTR,
int )
{
for( ;; )
;
return( 0 ) ;
}
poiché il multitasking di Windows 3.x è non-preemptive,
queste poche linee bastano a bloccare il sistema; possiamo naturalmente
premere Ctrl-Alt-Del ed uccidere il task bloccato.
Ovviamente, in una applicazione reale non scriveremo codice simile,
tuttavia è tutt'altro che raro trovarsi di fronte a cicli
infiniti, dovuti normalmente ad una errata espressione booleana;
per quanto sia comunque possibile terminare il task, sarebbe molto
più utile poter determinare la linea esatta in cui il
task è bloccato, ed eventualmente poter riprendere l'esecuzione
bypassando il ciclo infinito.
Se è attivo Soft-ICE/W, questa operazione è spesso
molto semplice: premendo il tasto di attivazione Ctrl-D passiamo
all'interno del debugger; se siamo fortunati, ci troveremo all'interno
della nostra applicazione (come sicuramente avviene per il semplice
esempio di cui sopra), mentre in altri casi ci troveremo all'interno
del codice di Windows stesso o di altre librerie, e sarà
necessario premere un certo numero di volte F8 fino a ritrovarsi
all'interno del proprio codice: alcuni debugger ad alto livello
possono talvolta bloccarsi durante queste operazioni, ma per Soft-ICE/W
si tratta di ordinaria amministrazione. A questo punto, possiamo
determinare la causa del blocco, ed in molti casi passare oltre,
sia alterando opportunamente il valore delle variabili in gioco
che imponendo direttamente un nuovo instruction pointer. Nel caso
in esame, con il comando g=.11 possiamo passare direttamente all'undicesima
linea del codice (la numerazione è visibile nella finestra
codice di Soft-ICE/W), ovvero allo statement di return; nel caso
di una applicazione reale, bypassare il ciclo infinito potrebbe
comportare la perdita di alcune risorse, ma di norma sarà
un'opzione di gran lunga migliore rispetto alla terminazione del
task.
Ricordiamo che, essendo Soft-ICE/W sempre attivo, possiamo interrompere
una applicazione bloccata anche se questa non è stata caricata
con il WLoader, a patto che si riesca a districarsi all'interno
del codice assembly relativo. Ciò può talvolta salvare
in extremis da una applicazione bloccata che vogliamo "resuscitare"
ad ogni costo. Acquisita una certa confidenza, è possibile
rimediare anche a situazioni piuttosto drastiche, come un errore
nel kernel stesso di Windows.
2) Verificare l'overflow dello stack
Per quanto esistano strumenti appositi (come il BoundsChecker,
sempre di NuMega), è possibile utilizzare Soft-ICE/W per
interrompere il programma non appena sta per verificarsi uno stack
overflow.
In figura 3 possiamo vedere com'é organizzato il DGROUP
di una applicazione Windows; in questo caso, se avviene un accesso
in scrittura alla locazione puntata da pStackBottom, siamo in
presenza di un overflow dello stack. Possiamo vedere operativamente
come verificare questa situazione, usando il seguente programma
in C; come si vedrà, la tecnica è indipendente dal
linguaggio usato per scrivere il programma.
#include < windows.h >
void f( int x )
{
f( x+1 ) ;
}
int PASCAL WinMain( HINSTANCE,
HINSTANCE,
LPSTR,
int )
{
f( 0 ) ;
return( 0 ) ;
}
Una volta caricato il programma con il WLoader, mettiamo un breakpoint
sulla prima istruzione eseguita (ovvero f(0)), posizionandoci
con il cursore sul codice e usando poi il comando BPX (breakpoint
in esecuzione). Lanciamo poi il programma con il comando G, e
quando il debugger riprende il controllo a seguito del breakpoint,
richiediamo il dump del segmento dati DS con il comando DW DS:0.
È ora necessaria una certa conoscenza del funzionamento
di Windows: la word ad indirizzo 0A all'interno del DGROUP è
il puntatore pStackTop della figura 3 (questa ed altre informazioni
sono reperibili in [1]). Nel caso specifico, supponiamo che il
valore di tale word sia 0A20; possiamo allora posizionare un breakpoint
in scrittura su tale locazione con il comando BPMW DS:0A20 e lanciare
nuovamente il programma con G.
Prima di uno stack overflow, il debugger prenderà nuovamente
il controllo, indicando la linea che causa l'overflow stesso;
a questo punto, passando alla visualizzazione del codice assembler
(comando SRC) siamo in grado di evitare facilmente l'overflow
dello stack saltando le istruzioni che realizzano la ricorsione;
in casi più complessi, possiamo semplicemente prendere
nota della riga per una successiva fase di correzione del codice.
3) Scoprire un memory overwrite
La stessa istruzione BPM[W] vista al punto precedente può
essere utile in molte altre occasioni, in quanto più in
generale può essere utilizzata per bloccare il programma
quando si verifica un accesso (in lettura, scrittura, lettura/scrittura
od esecuzione) ad un range di locazioni di memoria. Se vi è
mai capitato di trovarvi con il valore di una variabile misteriosamente
cambiato, con un puntatore deallocato senza che logicamente ciò
debba avvenire, se insomma, come avviene talvolta in programmi
complessi, per qualche ragione una parte di codice sovrascrive
un'area di memoria ma non riuscite a trovare la linea incriminata,
BPM è il comando che vi serve: trovate l'indirizzo del
simbolo che viene sovrascritto (comando SYM) e definite un opportuno
breakpoint sul pattern di accesso che più si addice alla
vostra situazione.
4) Debug di routine interrupt
Windows consente di gestire routine attivate da interrupt sia
all'interno di una DLL che di un VxD; sfortunatamente, è
impossibile usare un normale debugger per tracciare l'esecuzione
del codice all'interno di queste routine, anche limitandosi al
caso di DLL. È possibile posizionarvi dei breakpoint, ma
il debugger bloccherà il sistema quando il breakpoint viene
attivato. Ciò non avviene con Soft-ICE/W, che non accede
ad alcuna risorsa di sistema (memoria, disco, eccetera) una volta
che il programma sia stato caricato con il loader: ciò
consente di debuggare con facilità le funzioni attivate
da interrupt. Non solo, tramite il comando BPINT possiamo porre
un breakpoint sull'occorrenza dell'interrupt stesso, non sulla
routine; ciò è molto utile quando sembra che la
nostra routine di gestione non venga attivata correttamente, per
scoprire se la causa è un interrupt che non viene generato,
o che non viene correttamente indirizzato alla nostra routine,
o un bug nella routine stessa.
5) Debug di una funzione WEP
Ogni DLL dovrebbe contenere una funzione WEP, che viene chiamata
prima dell'unloading della DLL stessa; purtroppo, con un debugger
"tradizionale" è estremamente arduo (anche se
possibile utilizzando un "task fantasma") debuggare
la routine WEP. Ciò avviene a causa del supporto che Windows
fornisce ai debugger tradizionali, sotto forma di notifiche dal
Kernel e delle routine del ToolHelp, sulle quali si appoggiano
pressoché tutti i debugger interattivi in Windows. Purtroppo,
il debugger riceve la notifica NFY_EXITTASK prima che le DLL vengano
scaricate; a questo punto, elimina ogni breakpoint e segnala la
terminazione del task, rendendo molto problematico il debugging
delle DLL. Ciò non avviene per un debugger di sistema come
Soft-ICE/W, che è sempre in esecuzione, indipendentemente
dal task attivo; se caricate una DLL, potete selezionare un breakpoint
all'interno della sua WEP, sicuri che verrà eseguito senza
problemi quando la DLL verrà scaricata.
6) Debug di un VxD
Windows, quando eseguito in modalità 386 enhanced, permette
la virtualizzazione delle periferiche tramite i cosiddetti device
driver virtuali, chiamati comunemente VxD; in effetti, un
VxD non ha necessariamente il compito di virtualizzare una periferica,
ma può semplicemente fornire una serie di servizi non ottenibili
attraverso un normale programma Windows. Infatti, un VxD viene
eseguito a ring 0 (ovvero al più basso livello di protezione)
mentre i normali programmi Windows vengono eseguiti a ring 3;
ciò consente ai VxD lo stesso grado di libertà che
nel DOS veniva consentito ad ogni programma, in pratica la visibilità
diretta dell'hardware, la possibilità di manipolare la
memoria fisica, eccetera. Un VxD viene normalmente sviluppato
in assembly a 32 bit, per quanto esistano dei tool che consentono
di scriverli in C o in C++ (ad esempio, VToolsD di Vireo Software).
Il debugging di un VxD richiede necessariamente un debugger di
sistema; nel caso di Soft-ICE/W, è necessario generare
un file .SYM ed includere il nome del VxD sotto debugging nella
lista dei programmi da caricare (con lo statement "Load=..."
all'interno di WINICE.DAT). Il problema maggiore è come
posizionare i breakpoint iniziali, in quanto a differenza dei
normali eseguibili, il caricamento di un VxD non comporta l'attivazione
del debugger; la soluzione migliore è quella di introdurre
una istruzione aggiuntiva nel codice del VxD, che attivi il debugger
e consenta di posizionare altri breakpoint dove necessario. Una
buona candidata è l'istruzione INT 1, nel qual caso è
necessario aggiungere la linea "INIT=I1HERE on" all'interno
di WINICE.DAT.
7) I/O Trapping
Anche in questo caso la "vocazione al breakpoint" di
Soft-ICE/W si dimostra immediatamente: tramite il comando BPIO
è infatti possibile selezionare un breakpoint sull'accesso
ad una porta di I/O (in lettura, scrittura, o lettura/scrittura).
Questa possibilità si rivela di estrema utilità
ogni qualvolta si debba debuggare un programma che gestisce una
periferica, ma anche per analizzare il meccanismo utilizzato da
programmi esistenti per dialogare con la periferica stessa.
Il comando BPIO utilizza la possibilità del 386 di porre
una trap sull'accesso alle porte di I/O; ciò purtroppo
ha anche un risvolto negativo: non è possibile utilizzarlo
all'interno di un VxD, in quanto un altro VxD potrebbe avere già
virtualizzato (con la stessa tecnica di trapping) la porta stessa.
In questo caso (ed in altri come vedremo al punto successivo)
è possibile usare il comando TSS, che esegue un dump del
Task State Segment; questo include un elenco delle porte di I/O
sulle quali esiste una trap, e l'indirizzo del codice eseguito
al momento del trapping. In tal modo, se stiamo debuggando un
VxD e vogliamo porre un breakpoint su una porta virtualizzata,
dobbiamo in realtà mettere un breakpoint in esecuzione
all'indirizzo della trap associata alla porta.
8) Supporto al debugging "creativo"
Neppure Soft-ICE/W è in grado di rispondere a problematiche
di temporizzazione, che richiedono tipicamente un supporto hardware;
nella mia esperienza, ho dovuto spesso affrontare problemi di
questo tipo durante lo sviluppo di applicazioni di acquisizione
e processing dei dati in real-time. Verificare la stabilità
della frequenza di un interrupt, o misurare il tempo di latenza
di un interrupt all'interno di un VxD, sono compiti al di là
delle possibilità di Soft-ICE/W, e richiedono apparecchiature
molto costose. Tuttavia, con un pò di creatività
è spesso possibile utilizzare strumenti più abbordabili
anche in questo caso: ad esempio, introducendo nel codice del
VxD delle istruzioni che modifichino lo stato di una porta di
output (ad esempio una delle linee di output di una porta parallela)
e connettendovi un più economico oscilloscopio e un frequenzimetro.
In tal modo, pur con il minimo ritardo introdotto dalle istruzioni
di OUT, è possibile verificare molte proprietà di
timing, specialmente con uno strumento multitraccia.
Esiste però un problema: se la porta di I/O è virtualizzata
da un altro device, il "minimo ritardo" di cui sopra
diventa molto consistente, e può vanificare l'intera misura.
Anche in questo caso, la possibilità di Soft-ICE/W di esplorare
il TSS ci consente di determinare rapidamente se le porte utilizzate
siano virtualizzate o meno.
9) Esplorare Windows
Come detto in precedenza, Soft-ICE/W è un debugger di sistema:
è possibile attivarlo in ogni momento e seguire passo passo
l'esecuzione del codice; inoltre, vengono fornite con il programma
anche le tavole dei simboli per USER, KERNEL e GDI, rendendo possibile
l'esplorazione dei dettagli interni del sistema; naturalmente,
si tratta comunque di esplorare codice assembly, per cui è
necessaria una buona conoscenza del linguaggio.
In quali occasioni può essere necessario esplorare il sistema?
In tutti quei casi in cui è necessario approfondire un
aspetto poco o mal documentato; ad esempio, sia [1] che [2] sono
frutto di una simile esplorazione di Windows, eseguita in larga
misura proprio con Soft-ICE/W; nel mio caso, è stato necessario
investigare il funzionamento di Windows durante la stesura di
un'utility in grado di eseguire un dump delle risorse allocate
e non rilasciate da una applicazione (ed opzionalmente di rilasciarle),
a causa di alcuni problemi legati all'uso fatto internamente a
Windows delle region. Ovviamente non si tratta di un compito
ordinario, ma Soft-ICE/W è uno dei pochi tool in grado
di aiutarvi nel caso doveste intraprendere questa attività.
10) Debug di un debugger
Sicuramente ben pochi tra i lettori si avventureranno nello sviluppo
di un debugger per Windows; in ogni caso, ha senso chiedersi quale
strumento si possa utilizzare per debuggare un debugger. Normalmente
non il debugger stesso (magari in una precedente versione), poiché
i debugger tendono a trattare alcune delle risorse di sistema
come esclusive, impedendo a due versioni di girare allo stesso
tempo.
Un debugger di sistema come Soft-ICE/W è invece in grado
di debuggare anche il codice di un debugger: io stesso ne ho fatto
uso, al momento del rilascio del compilatore Borland 4.0, per
investigare un bug legato all'uso di server DDE lanciati dalla
stessa applicazione sotto debugging, scoprendo un errato utilizzo
della funzione DirectedYield, comunicata in seguito alla Borland
(il bug è stato corretto nella versione 4.20).
Conclusioni
Soft-ICE/W non è un debugger "per i deboli di cuore":
per essere sfruttato a fondo, richiede una buona conoscenza del
sistema, anche a livello di linguaggio assembly e di informazioni
dettagliate sulla struttura interna di Windows (per cui si consigliano
vivamente [1] e [2]). Ciò nonostante, può essere
utile in una varietà estrema di situazioni, solo parzialmente
coperte dagli esempi riportati; con l'esclusione dei problemi
di temporizzazione, se le vostre esigenze di debugging non sono
soddisfatte dai normali debugger interattivi, Soft-ICE/W è
probabilmente la risposta migliore ad un costo abbordabile.
Bibliografia
[1] Schulman, Maxey, Pietrek: "Undocumented Windows",
Addison-Wesley, 1992
[2] Matt Pietrek: "Windows Internals", Addison-Wesley,
1993.
Biografia
Carlo Pescio svolge attività di consulenza in ambito internazionale
sulle metodologie di sviluppo Object Oriented e di programmazione
in ambiente Windows. È laureato in Scienze dell'Informazione,
membro dell'Institute of Electrical and Electronics Engineers
Computer Society, dei Comitati Tecnici IEEE su Linguaggi Formali,
Software Engineering e Medicina Computazionale, dell'ACM e dell'Academy
of Sciences di New York. Può essere contattato tramite
la redazione o su Internet come
pescio@acm.org