Dr. Carlo Pescio
Soft-ICE/W, un debugger a basso livello per Windows

Pubblicato su Computer Programming No. 43


Vi sono situazioni in cui un debugger simbolico tradizionale non è sufficiente: in questi casi, è necessario utilizzare un debugger a livello di sistema. Soft-ICE/W è in grado di tracciare dal programma in C++ sino al VxD in assembly a 32 bit, e di arrivare sino al kernel del sistema stesso.

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.

Reader's Map
Molti visitatori che hanno letto
questo articolo hanno letto anche:

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