Assembly Base con NASM

Capitolo 14: Disassembling


Nel precedente capitolo, sono stati esposti numerosi concetti teorici, relativi al lavoro svolto dall'assembler, dal linker e dal SO, nel processo di conversione del codice sorgente Assembly in codice eseguibile; in questo capitolo, invece, questi stessi concetti teorici verranno verificati in pratica attraverso l'analisi di un programma eseguibile.
Questo tipo di analisi può essere effettuato in diversi modi e con diversi strumenti, dai più semplici ai più sofisticati; nel nostro caso, analizzeremo in modo sintetico alcune tecniche che ci permetteranno di conoscere numerosi dettagli interni di un programma eseguibile in formato EXE o COM.

14.1 Analisi di un file eseguibile con un editor binario

Un qualunque file, viene memorizzato su disco sotto forma di sequenza di byte, cioè sotto forma di valori binari da 8 bit ciascuno; proprio per questo motivo, i file memorizzati su disco vengono definiti genericamente, file binari.
I linguaggi di programmazione di alto livello, invece, tendono a distinguere tra file binari e file di testo; come si può facilmente intuire, questa distinzione è puramente formale. Consideriamo, ad esempio, la seguente sequenza di byte memorizzata in un file di testo:
41h, 73h, 73h, 65h, 6Dh, 62h, 6Ch, 79h, 0Dh, 0Ah
Un editor di testo, nella fase di apertura di questo file, cercherà di convertire ogni byte, nel simbolo associato al corrispondente codice ASCII; i simboli così ottenuti verranno poi visualizzati sullo schermo. Come si può facilmente constatare, i primi 8 byte codificano la stringa:
Assembly
Il penultimo byte (0Dh), rappresenta un codice che permette di simulare sullo schermo del computer il ritorno carrello (carriage return) della macchina da scrivere; un editor di testo, interpreta questo codice spostando il cursore lampeggiante, sul bordo sinistro dello schermo.
L'ultimo byte (0Ah), rappresenta un codice che permette di simulare sullo schermo del computer l'avanzamento riga (line feed) della macchina da scrivere; un editor di testo, interpreta questo codice spostando il cursore lampeggiante, sulla riga successiva dello schermo.

Appare evidente il fatto che questo file di testo, quando viene memorizzato su disco, risulta disposto sotto forma di sequenza di byte, come un qualsiasi file binario; in sostanza, i file di testo sono particolari file binari, che contengono una sequenza di codici ASCII. Se proviamo ad aprire un file eseguibile con un editor di testo, otteniamo sullo schermo una sequenza incomprensibile di simboli; l'editor di testo, infatti, cercherà di trattare come codici ASCII quelli che, in realtà, sono i codici macchina dei dati e delle istruzioni di un programma.
Se vogliamo analizzare il contenuto grezzo di un file binario, possiamo servirci di appositi editor chiamati editor binari; l'editor binario, visualizza i vari byte che formano un file binario, senza attribuire ad essi alcun significato.
Un esempio pratico di editor binario, è rappresentato dal programma QEDITOR.EXE che viene fornito con MASM32; questo editor, oltre a visualizzare i file di testo, dispone anche del menu:
File - Open Binary as Hex
In ambiente Linux esistono ottimi editor binari come GHex, KHexEdit e Okteta; la Figura 14.1 mostra, appunto, Okteta mentre visualizza la parte iniziale del file EXETEST.EXE che abbiamo ottenuto nel precedente capitolo. Questo particolare tipo di visualizzazione delle informazioni binarie, presenti in un qualsiasi supporto di memoria (disco, RAM, etc), viene anche definito memory dump.
Osserviamo subito che Okteta visualizza il dump sotto forma di righe, dove ogni riga contiene un gruppo di 16 byte (cioè 1 paragrafo); i vari byte vengono disposti da sinistra verso destra, per cui i dati di tipo WORD, DWORD, etc, appaiono disposti al contrario rispetto alla loro rappresentazione in notazione posizionale. Come impostazione predefinita Okteta mostra il dump in formato esadecimale; è anche possibile però selezionare il formato decimale, ottale, binario e testo.
Notiamo poi che Okteta mostra, nella parte sinistra, gli offset delle informazioni presenti nel file binario che abbiamo aperto; questi offset sono degli spiazzamenti calcolati, ovviamente, rispetto all'inizio del file EXETEST.EXE (nonostante le apparenze, si tratta di offset formati da 8 cifre esadecimali).
Nella parte destra Okteta mostra la traduzione del codice macchina in formato ASCII; si tratta cioè di ciò che apparirebbe se aprissimo EXETEST.EXE con un editor di testo (i caratteri non stampabili vengono mostrati sotto forma di un punto '.').

Ricordando quanto è stato detto nel precedente capitolo, possiamo subito intuire che le informazioni presenti in Figura 14.1 si riferiscono all'header di EXETEST.EXE; infatti, si nota subito che i primi due byte (4Dh e 5Ah), sono i codici ASCII della stringa 'MZ'. Proviamo allora ad "estrarre" dalla Figura 14.1, la struttura di controllo dell'header di EXETEST.EXE; il risultato che si ottiene, viene mostrato in Figura 14.2. Le informazioni di Figura 14.2 confermano l'esattezza di tutti i calcoli che avevamo raccolto nella Figura 13.9 del Capitolo 13; sempre dalla Figura 14.2 ricaviamo anche:
RelocationRecords = 0001h
e:
StartOfRelocationTable = 001Eh
Spostandoci, infatti, all'offset 0000:001Eh del blocco di Figura 14.1, troviamo le seguenti informazioni:
RelocationOffset = 0009h
e:
RelocationSeg = 0044h
Anche in questo caso, vengono confermati i calcoli riportati nella Figura 13.9 del Capitolo 13.

Proviamo ora a calcolare le dimensioni di EXETEST.EXE; in base ai dati di Figura 14.2, si ottiene:
((PageCount - 1) * 512) + LastPageSize = (3 * 512) + 018Fh = 1536 + 399 = 1935 byte
Infatti, nella cartella ASMBASE possiamo constatare che il file EXETEST.EXE occupa proprio 1935 byte.

Sempre dalla Figura 14.2 ricaviamo:
HeaderParagraphs = 0020h
Ciò significa che l'header assume la dimensione minima possibile, pari a:
0020h * 10h = 0200h = 512 byte
Di conseguenza, all'offset 0000:0200h di EXETEST.EXE, dovremmo trovare l'inizio di DATASEGM; a tale proposito, osserviamo il contenuto della Figura 14.3. Ci accorgiamo subito che le informazioni presenti in Figura 14.3, a partire dall'offset 0000:0200h, rappresentano proprio i dati visibili nel listing File della Figura 13.2 del Capitolo 13 (a partire dalla riga 54 del blocco DATASEGM).

Sempre dal listing file (o dal map file), rileviamo che DATASEGM occupa 0447h byte; inoltre, sappiamo che subito dopo la fine di DATASEGM, il linker ha inserito un buco di memoria da 1 byte per poter allineare CODESEGM alla DWORD. Di conseguenza, CODESEGM dovrebbe iniziare dall'offset:
0200h + 0447h + 1h = 0648h
del file EXETEST.EXE; a tale proposito, osserviamo il contenuto della Figura 14.4. Anche in questo caso, ci accorgiamo subito che le informazioni presenti in Figura 14.4, a partire dall'offset 0000:0648h, rappresentano proprio i codici macchina delle istruzioni presenti nel blocco CODESEGM; notiamo, inoltre, che il buco di memoria inserito dal linker all'offset 0000:0647h, è un byte di valore 00h.

Se ora ci portiamo alla fine del file EXETEST.EXE (vedere la Figura 14.4), troviamo gli ultimi due byte, che valgono CDh, 21h; ma questo, non è altro che il codice macchina dell'istruzione:
INT 21h
che termina il nostro programma!
Tutto ciò significa che nel modulo caricabile presente nel file EXETEST.EXE, il linker non ha inserito i 0400h byte del blocco STACKSEGM; come si spiega questa situazione?
Per rispondere a questa domanda, bisogna ricordare che il blocco STACKSEGM si trova proprio alla fine del nostro programma e contiene 0400h byte di dati non inizializzati; in un caso di questo genere, il linker elimina questo segmento di programma dal modulo caricabile e inizializza opportunamente il campo MinimumAllocation presente nella struttura di controllo dell'header. Osservando, infatti, la Figura 14.2, ci accorgiamo che:
MinimumAllocation = 0041h
Questo valore è espresso in paragrafi e rappresenta quindi:
0041h * 10h = 0410h = 1040 byte
di memoria aggiuntiva; tale memoria, viene appunto aggiunta dal DOS al program segment destinato a EXETEST.EXE.
Si può notare che il valore 1040 è leggermente superiore a quello che noi avevamo richiesto (1024 byte per lo stack); questo accade in quanto il linker tiene conto del fatto che, anche dopo la fine di CODESEGM, è previsto un buco di memoria da 1 byte che garantisce l'allineamento di STACKSEGM al paragrafo. Bisogna ricordare, inoltre, che i blocchi di memoria DOS hanno sempre una dimensione in byte multipla intera di 16; nel nostro caso, per poter contenere 1 byte di allineamento, più 1024 byte di stack, abbiamo bisogno, come minimo, di un blocco di memoria aggiuntiva formato appunto da 41h paragrafi (65 paragrafi), pari a:
65 * 16 = 1040 byte
Come si intuisce dal nome, la memoria aggiuntiva può essere posizionata solo dopo la fine del program segment; di conseguenza, se disponiamo STACKSEGM prima di CODESEGM o di DATASEGM, il linker provvede ad incorporare anche lo stack, direttamente all'interno del modulo caricabile.

Attraverso gli editor binari, è possibile analizzare anche alcune stranezze relative alle dimensioni dei file eseguibili in formato COM; consideriamo a tale proposito il file COMTEST.COM che abbiamo creato nel precedente capitolo.
In base al listing file prodotto dall'assembler, si rileva che il blocco COMSEGM occupa 06BCh byte (cioè 1724 byte) che comprendono anche i 256 byte del PSP; nella cartella ASMBASE notiamo però che il file COMTEST.COM (che contiene solo il modulo caricabile), occupa 1468 byte!
Se proviamo ad aprire il file COMTEST.COM con un editor binario, ci accorgiamo che mancano i 256 byte del PSP; come si nota, infatti, in Figura 14.5, il file inizia direttamente con i codici macchina delle varie istruzioni. Tutto ciò è una diretta conseguenza del procedimento standard utilizzato dal linker e dal SO per la gestione dei programmi in formato COM; il linker ha ricevuto (da noi) l'ordine di generare un file COM e quindi inserisce nel file stesso, solo il codice e i dati statici. Il linker si comporta in questo modo perché sa che il SO, non trovando la stringa 'MZ' all'inizio del file COMTEST.COM, lo tratterà come un eseguibile in formato COM e avvierà la procedura standard per il suo caricamento in memoria; questa procedura prevede anche la creazione, all'inizio del program segment, dello spazio necessario per il PSP, con conseguente slittamento dell'entry point, all'offset 0100h.

Con l'ausilio di un editor binario, si possono fare parecchie cose interessanti; ad esempio, un programmatore Assembly piuttosto esperto, potrebbe apportare delle modifiche ad un file eseguibile. Bisogna prestare molta attenzione al fatto che con QEDITOR, se si vuole salvare su disco il contenuto di un file binario, ci si deve servire del menu:
File - Save Hex As Binary
Questo menu, salva su disco solamente le informazioni binarie del file; tutte le altre informazioni (commenti e offset), vengono eliminate. Se proviamo a servirci del normale menu:
File - Save
otteniamo un file corrotto che, in fase di esecuzione, provoca un sicuro crash; ciò accade in quanto, con il normale menu Save, QEDITOR salva su disco, non solo le informazioni binarie del file, ma anche le altre informazioni aggiuntive visibili nella finestra dell'editor.
Con GHex, KHexEdit e Okteta questo problema non sussiste; trattandosi, infatti, di editor binari puri, viene salvato solamente il codice binario visualizzato nella finestra principale.

Facciamo un semplice esperimento che consiste nel creare, direttamente con Okteta (o qualunque altro editor binario), un file binario chiamato, ad esempio, TEST.COM; il programma che si ottiene, visualizza una stringa attraverso il servizio n. 09h (Display String) della INT 21h. Questo servizio presenta le caratteristiche illustrate in Figura 14.6. Il programma, in formato COM, contiene le istruzioni mostrate in Figura 14.7. Come si nota in Figura 14.6, DS:DX deve contenere l'indirizzo logico Seg:Offset della stringa da visualizzare; la componente Seg di questo indirizzo è COMSEGM, per cui DS non necessita di alcuna modifica (infatti, nel formato COM il DOS pone DS=COMSEGM).
Osservando la struttura del nostro programma COM (allineamento PARA di COMSEGM), abbiamo anche la certezza che l'offset 010Dh di messaggio, non subirà alcuna rilocazione; di conseguenza, possiamo caricare in DX direttamente il valore 010Dh.
Apriamo ora Okteta, clicchiamo su Nuovo e, nel riquadro di sinistra, inseriamo direttamente il codice macchina del nostro programma; si ottiene così la seguente situazione: Attraverso il menu File - Salva di Okteta, salviamo il nostro file da 19 byte, chiamandolo TEST.COM; possiamo subito notare che il file TEST.COM salvato su disco, è formato proprio da 19 byte. Eseguendo ora TEST.COM, possiamo constatare che il programma appena creato "a mano" è perfettamente funzionante; sullo schermo, infatti, sarà mostrata la stringa:
Prova
Gli editor binari vengono largamente utilizzati anche per analizzare la struttura interna di particolari formati di file; in questo modo, si possono scoprire parecchi segreti relativi, ad esempio, alla codifica dei file audio (WAV, MID, MP3, etc), dei file video (AVI, MPG, etc), dei file immagine (GIF, BMP, JPG, TIFF, etc) e di numerosissimi altri tipi di file.

Se vogliamo effettuare una analisi molto più avanzata di un file eseguibile, possiamo servirci di appositi strumenti capaci di mostrare sullo schermo, direttamente il codice Assembly dell'eseguibile stesso; questa categoria di strumenti comprende anche i cosiddetti debugger.

14.2 Analisi di un file eseguibile con un debugger

Come è stato spiegato in un precedente capitolo, la corrispondenza biunivoca che esiste tra codice macchina e codice Assembly, rende possibile l'esistenza di particolari strumenti chiamati disassembler (disassemblatori); il disassembler è un programma che legge il codice macchina presente in un file eseguibile e lo converte in codice Assembly.
Come si può facilmente intuire, uno strumento così potente può offrire un aiuto enorme ai programmatori che hanno la necessità di analizzare la struttura interna e il comportamento di un eseguibile; proprio grazie alle loro notevoli potenzialità, i disassembler vengono largamente usati anche dai pirati informatici, per finalità poco lecite.

Come è stato scritto in precedenza, nella categoria dei disassembler rientrano anche i debugger; lo scopo fondamentale dei debugger è quello di aiutare i programmatori nella complessa fase di ricerca delle cause che provocano il malfunzionamento dei propri programmi.
Un programmatore che vuole sfruttare al massimo le potenzialità dei debugger, deve creare dei programmi eseguibili che contengono al loro interno una serie di informazioni, chiamate appunto informazioni di debugging; grazie a tali informazioni, il debugger è in grado di fornire una enorme quantità di dettagli, relativi a tutto ciò che accade all'interno di un programma nella fase di esecuzione.

Gli utilizzatori degli strumenti di sviluppo della Microsoft, hanno a disposizione un debugger denominato CodeView; lo si può trovare, ad esempio, nel MASM 6.11 (file CV.EXE nella directory C:\MASM\BIN).
Chi utilizza gli strumenti di sviluppo della Borland, invece, ha a disposizione un debugger chiamato Turbo Debugger, rappresentato dal file TD.EXE (o TD32.EXE per i programmi a 32 bit per Windows); è possibile reperire il Turbo Debugger scaricando, ad esempio, il Borland Turbo Pascal, disponibile gratuitamente nel sito della Borland dedicato al "museo del software".

Analizziamo il caso di CodeView (CV); attraverso CV è possibile "debuggare" qualsiasi programma scritto con gli strumenti di sviluppo della Microsoft, della Borland e anche con NASM.
L'assembler NASM è in grado di creare informazioni di debugging destinate, sia al Turbo Debugger, sia a CodeView; a tale proposito, dobbiamo passare all'assembler l'opzione -g, la quale lavora in combinazione con il formato dell'object file specificato con l'altra opzione -f.
In riferimento, ad esempio, al file EXETEST.ASM presentato nel precedente capitolo, dalla directory c:\nasm\asmbase dobbiamo effettuare l'assemblaggio attraverso il seguente comando:
c:\nasm\nasm -g -f obj exetest.asm
L'opzione -g indica a NASM di inserire tutte le necessarie informazioni di debugging all'interno dell'object file EXETEST.OBJ.
La fase di linking con il linker del MASM 6.11 deve essere effettuata attraverso il comando:
c:\masm\bin\link /codeview exetest.obj
L'opzione /codeview indica a LINK di inserire tutte le necessarie informazioni di debugging all'interno del file eseguibile EXETEST.EXE.
A questo punto, possiamo lanciare il debugger con il comando:
c:\masm\bin\cv exetest.exe
Una volta entrati nel debugger, abbiamo a disposizione una enorme quantità di strumenti attraverso i quali possiamo esaminare, nel minimo dettaglio, tutta la fase di esecuzione di EXETEST.EXE; si può anche constatare che, grazie alle informazioni di debugging, CV è in grado di mostrare persino i nomi simbolici che abbiamo assegnato ai dati e alle etichette!
Chi volesse approfondire questo argomento, può consultare il manuale utente fornito insieme a CodeView.

Le informazioni di debugging influiscono negativamente sulla velocità di esecuzione di un programma e aumentano notevolmente le dimensioni del codice; proprio per questo motivo, una volta che la fase di debugging è terminata, è necessario ricreare l'eseguibile, disabilitando la generazione di tutte le informazioni destinate al debugger.

Vediamo un esempio riferito sempre al programma EXETEST.EXE, che abbiamo creato nel precedente capitolo; dopo aver assemblato il file senza l'opzione -g e dopo aver effettuato il linking senza l'opzione /codeview, proviamo ad eseguire il solito comando:
c:\masm\bin\cv exetest.exe
In assenza delle informazioni di debugging, TD mostra una finestra con un messaggio del tipo:
Warning: no CodeView information for 'EXETEST.EXE'
Questo messaggio ci informa sul fatto che, per EXETEST.EXE, non sono disponibili le informazioni di debugging e quindi sarà possibile effettuare solamente un debugging di tipo "grezzo".
La Figura 14.8 mostra una schermata relativa a CodeView; se si vuole visualizzare lo stato dei registri della CPU, è necessario selezionare (con Alt+W) il menu Windows - Register. Osserviamo subito che CV ci mostra il contenuto del blocco puntato inizialmente da CS (source1 CS:IP); la prima istruzione della lista è ovviamente quella presente all'entry point.
Come è stato specificato in precedenza, dal menu Windows - Register si può chiedere la visualizzazione della finestra dei registri della CPU; per avere i registri a 32 bit si deve andare sul menu Options - 32-Bit Registers. Per i flags vengono usate delle abbreviazioni illustrate in Figura 14.11.

Tenendo presente che queste informazioni possono variare da computer a computer, possiamo notare che:
DS = ES = 1CA1h
Ciò significa che EXETEST.EXE è stato caricato in memoria a partire dall'indirizzo logico 1CA1h:0000h; questo è anche l'indirizzo iniziale del program segment, per cui, possiamo dire che:
StartSeg = PSP = DS = ES = 1CA1h
All'indirizzo logico 1CA1h:0000h corrisponde l'indirizzo fisico 1CA10h; il PSP occupa 0100h byte, per cui il blocco DATASEGM parte dall'indirizzo fisico:
1CA10h + 0100h = 1CB10h
A questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 1CB1h:0000h.
Dal listing file della Figura 13.2 del precedente capitolo, ricaviamo che il blocco DATASEGM occupa 0447h byte; inoltre, il linker ha posto, alla fine di DATASEGM, un buco di memoria da 1 byte per allineare CODESEGM alla DWORD. Ciò significa che CODESEGM parte dall'indirizzo fisico:
1CB10h + 0447h + 1h = 1CF58h
A questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 1CF5h:0008h, che coincide anche con l'indirizzo dell'entry point del programma; infatti, in Figura 14.8 notiamo che appena EXETEST.EXE viene caricato in memoria, si ha:
CS = 1CF5h, IP = 0008h
Dal listing file della Figura 13.2 del precedente capitolo, ricaviamo che il blocco CODESEGM occupa 0147h byte; inoltre, il linker ha posto, alla fine di CODESEGM, un buco di memoria da 1 byte per allineare STACKSEGM al paragrafo. Ciò significa che STACKSEGM parte dall'indirizzo fisico:
1CF58h + 0147h + 1h = 1D0A0h
A questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 1D0Ah:0000h; inoltre, la dimensione iniziale dello stack è pari a 0400h byte. Infatti, in Figura 14.8 notiamo che appena EXETEST.EXE viene caricato in memoria, si ha:
SS = 1D0Ah, SP = 0400h
(notare che la WORD più significativa di ESP vale 0000h).

Inizialmente, DS punta al PSP; infatti, all'indirizzo logico DS:0000h del blocco dati di Figura 14.8 (selezionare il menu Windows - Memory 1), troviamo i due codici macchina CDh, 20h che, come sappiamo, si riferiscono all'istruzione:
INT 20h
per la terminazione in "vecchio stile" dei programmi DOS.

Se vogliamo trovare i dati statici del nostro programma, dobbiamo avanzare all'offset 0100h, in modo da scavalcare i 256 byte del PSP; infatti, andando all'indirizzo DS:0100h possiamo notare la presenza dei dati statici di EXETEST.EXE.

Per visualizzare il blocco stack, dobbiamo impostare la componente Seg pari a 1D0Ah (come calcolato in precedenza) nella finestra Memory 1; in questo modo possiamo notare che lo stack è pieno di dati il cui valore è diverso da 0. Si tratta di una diretta conseguenza del fatto che, per le variabili temporanee, abbiamo richiesto 0400h byte non inizializzati; di conseguenza, il blocco stack è inizialmente pieno di dati casuali che, in gergo, vengono definiti "spazzatura".

Nel blocco codice mostrato in Figura 14.8, la coppia CS:IP punta inizialmente all'entry point (1CF5h:0008h); la prima istruzione che viene eseguita è quindi:
mov ax, 1CB1h
Come abbiamo visto in precedenza, 1CB1h è la componente Seg dell'indirizzo logico iniziale (1CB1h:0000h) di DATASEGM; di conseguenza, la successiva istruzione:
mov ds, ax
carica 1CB1h in DS. Da questo momento, DS:0000h rappresenta l'indirizzo logico iniziale del blocco dati.

Consideriamo ora la terza istruzione:
mov word ptr [0029h], 0DACh
In base al listing file della Figura 13.2 del precedente capitolo, questa istruzione corrisponde a:
mov word [varRect+p1+x], 3500
Infatti, sempre dal listing file, ricaviamo che varRect.p1.x è un dato a 16 bit, che si trova all'offset 0029h del blocco DATASEGM.
In assenza delle informazioni di debugging, CV non è ovviamente in grado di visualizzare i nomi simbolici che abbiamo assegnato ai dati del nostro programma; di conseguenza, nel caso della precedente istruzione, viene visualizzato l'offset del dato (0029h) e la sua dimensione in bit (attraverso l'operatore WORD PTR di MASM).

14.2.1 Esecuzione a passo singolo di un programma

Una delle caratteristiche più interessanti dei debugger come CV e TD, è quella di poter eseguire, a richiesta, solamente la prossima istruzione di un programma; questo metodo di debugging prende il nome di single-step mode (modo di esecuzione a passo singolo). Nel caso di CV, si può ottenere l'esecuzione single-step, premendo ripetutamente il tasto funzione F8; in questo modo, è possibile seguire "al rallentatore", l'evoluzione del programma in esecuzione.
Generalmente, questa tecnica si basa sull'utilizzo del Trap Flag presente nel registro FLAGS; come abbiamo visto in un precedente capitolo, se poniamo TF=1, la CPU genera una INT 01h dopo aver elaborato ogni singola istruzione del programma in esecuzione. Come conseguenza di questa INT, viene chiamata la ISR il cui indirizzo Seg:Offset è memorizzato all'indice 01h del vettore delle interruzioni; i debuggers, non fanno altro che installare una loro ISR per la gestione della INT 01h.
Ogni volta che la CPU elabora una istruzione del programma in esecuzione, genera una INT 01h; questa INT viene intercettata dalla ISR del debugger, che può così visualizzare sullo schermo, tutti i dettagli relativi al programma stesso.
Come si può facilmente intuire, questa tecnica risulta essere particolarmente vulnerabile; un programma che non vuole farsi "debuggare", non deve fare altro che installare, a sua volta, una propria ISR per la gestione della INT 01h. Questa ISR, intercetta la INT 01h generata dalla CPU e, attraverso apposite istruzioni "maligne", provoca intenzionalmente un crash; si tratta di un semplice espediente, che viene utilizzato da molti programmi dotati di protezione "anti-debugger".

14.2.2 I breakpoint

Un'altra importante caratteristica dei debuggers, consiste nella possibilità di eseguire un programma a velocità normale, da una istruzione di partenza sino ad una istruzione di arrivo; quando l'esecuzione giunge alla istruzione di arrivo, il programma viene interrotto, dando così la possibilità al debugger di visualizzare il contenuto di tutti i registri della CPU e dei vari dati del programma stesso. Questa caratteristica risulta molto utile nel momento in cui il programmatore ritiene di aver individuato la "zona" di un programma nella quale si verifica un malfunzionamento; il punto (scelto dallo stesso programmatore) nel quale il programma viene interrotto, prende il nome di breakpoint (punto di stop).
Come accade per l'esecuzione in single-step mode, anche la gestione dei breakpoint avviene attraverso un apposito vettore di interruzione; in questo caso, si tratta della INT 03h. A differenza delle altre INT, che hanno un codice macchina da 2 byte, la INT 03h ha un codice macchina (CCh) formato da un solo byte; in questo modo, si semplifica notevolmente il lavoro che il debugger deve svolgere per attivare o disattivare i breakpoint in un programma.
Per gestire i breakpoint, il debugger installa una propria ISR per la INT 03h; in fase di esecuzione di un programma, quando la CPU incontra il codice macchina CCh, genera una INT 03h che viene intercettata dalla ISR del debugger, la quale può così procedere con le necessarie verifiche sul programma stesso.

Tutte le CPU della famiglia 80x86, a partire dalla 80386, sono state dotate di appositi registri interni, destinati esplicitamente ai debuggers; tutti questi argomenti, verranno trattati nelle sezioni Assembly Avanzato e Modalità Protetta.

14.3 Il debugger del DOS

In mancanza di un debugger professionale, può rivelarsi molto utile anche il "mini-debugger" fornito dal DOS; si tratta del celebre programma DEBUG presente nella cartella C:\DOS (nel caso di Windows 9x la cartella è C:\WINDOWS\COMMAND, mentre per Windows XP la cartella è C:\WINDOWS\SYSTEM32).
FreeDOS (e quindi anche DOSEmu per Linux) fornisce un ottimo clone di DEBUG; tra le caratteristiche più potenti di questo clone troviamo, in particolare, il supporto per le istruzioni a 32 bit!
In effetti, la principale limitazione di DEBUG per DOS/Windows è rappresentata dal fatto che si tratta di un debugger a 16 bit; di conseguenza, questo debugger lavora in modo corretto solo con programmi a 16 bit, contenenti esclusivamente istruzioni 8086 e 8087.

Il programma DEBUG può essere eseguito in due modalità distinte; la prima modalità, consiste nell'impartire dal prompt, il comando:
debug
Dopo aver premuto il tasto [Invio], ci ritroviamo all'interno del debugger; sul bordo sinistro dello schermo, viene mostrato un trattino, attraverso il quale DEBUG ci informa che è pronto a ricevere comandi.
Quando DEBUG viene lanciato in questo modo (cioè, senza alcun argomento), predispone automaticamente un program segment da 64 KiB che, nei primi 256 byte, contiene un generico PSP; i registri di segmento vengono inizializzati in questo modo:
CS = DS = ES = SS = PSP
I registri IP e SP vengono inizializzati in questo modo:
IP = 0100h, SP =  FFEEh
Se si eccettua quindi SP, che vale FFEEh, tutti gli altri registri vengono inizializzati secondo lo standard dei programmi in formato COM.
All'interno del program segment così predisposto, è possibile fare esperimenti di ogni genere; più avanti verranno illustrati degli esempi pratici.

Il secondo metodo per l'esecuzione di DEBUG, consiste nel passargli un file eseguibile come argomento; possiamo impartire, ad esempio, il comando:
debug exetest.exe
In un caso del genere, i vari registri vengono inizializzati secondo lo schema tipico degli eseguibili in formato EXE; nel nostro caso, per i registri di segmento DS e ES si ha:
DS = ES = PSP
La coppia CS:IP punta all'entry point da noi stabilito; la coppia SS:SP punta alla cima dello stack da noi predisposto.
Se, invece, impartiamo il comando:
debug comtest.com
i vari registri vengono inizializzati secondo lo schema tipico degli eseguibili in formato COM; nel nostro caso, per i registri di segmento si ha:
CS = DS = ES = SS = PSP
Per i registri IP e SP si ha:
IP = 0100h, SP =  FFFEh

14.3.1 I comandi di DEBUG

Una volta che abbiamo lanciato il debugger, proviamo ad impartire il comando:
-?
(il trattino rappresenta il prompt di debug)

Premendo ora il tasto [Invio], otteniamo una lista dei comandi che debug è in grado di accettare; esaminiamo il significato dei vari comandi, con particolare riferimento a quelli più interessanti. Nei vari esempi che verranno illustrati, si suppone che DEBUG sia stato lanciato senza alcun argomento; per ogni comando, gli eventuali argomenti specificati tra parentesi quadre sono facoltativi. Per maggiori dettagli, si consiglia di consultare i manuali del DOS. Questo comando ci permette di inserire direttamente in memoria, istruzioni Assembly appartenenti al set della CPU 8086 e della FPU 8087; se impartiamo questo comando senza argomenti, DEBUG ci fornisce l'indirizzo logico iniziale, rappresentato da CS:IP. Supponendo, ad esempio, che CS contenga il valore 1331h, e che IP contenga il valore 0100h, in seguito al comando:
-a
il debugger ci mostra l'indirizzo logico:
1331:0100
Sulla destra di questo indirizzo, possiamo inserire l'istruzione Assembly desiderata; si noti che tutti gli indirizzi logici e tutti i valori immediati contenuti nelle istruzioni, devono essere espressi implicitamente in esadecimale.
In alternativa, possiamo specificare anche un indirizzo, attraverso il comando:
-a 1331:02ca
oppure:
-a cs:02ca
In questo caso, il debugger risponde mostrando la riga:
1331:02ca
A questo punto, possiamo procedere con l'inserimento delle istruzioni Assembly; sono disponibili le direttive DB e DW, gli address size operators BYTE PTR e WORD PTR, i commenti su una sola linea (;), i segment override, etc.
Ogni volta che inseriamo una nuova istruzione, DEBUG incrementa l'offset nel blocco codice e ci mostra la prossima coppia libera CS:Offset; per terminare l'inserimento delle istruzioni, basta premere il tasto [Invio] in una riga vuota.

Vediamo un esempio pratico, che consiste nella visualizzazione di una stringa attraverso il servizio n. 09h (Display String) della INT 21h (che è stato già illustrato in precedenza); la sequenza delle istruzioni da inserire viene mostrata in Figura 14.9. Anche in questo caso, DS non necessita di alcuna modifica, in quanto contiene già la componente Seg dell'indirizzo logico della stringa; infatti, abbiamo appena visto che DEBUG, lanciato senza argomenti, crea un unico segmento di programma che parte da un indirizzo logico la cui componente Seg viene caricata in CS, DS, ES e SS.
In DX viene caricato il valore 0100h, che è l'offset della stringa da visualizzare; possiamo maneggiare tranquillamente questi indirizzi assoluti, in quanto stiamo inserendo le istruzioni direttamente in memoria (non esiste quindi alcuna rilocazione degli indirizzi).
Per eseguire questo programma, possiamo servirci del comando G (Go), che viene descritto più avanti; nel nostro caso, possiamo notare che il codice eseguibile inizia da CS:0111h. Il comando da impartire è quindi:
-g = 1331:0111
oppure:
-g = cs:0111
In questo modo, DEBUG esegue il programma che, dopo aver visualizzato la stringa, restituisce il controllo al DOS.

È importante che le varie istruzioni Assembly vengano inserite nel program segment predisposto da DEBUG; in caso contrario, si corre il rischio di sovrascrivere aree riservate della memoria, con conseguente crash del programma. Con questo comando, è possibile comparare due aree di memoria, in modo da evidenziare eventuali differenze nel loro contenuto; il debugger visualizza esclusivamente i byte che, nelle due aree da confrontare, occupano la stessa posizione e differiscono tra loro in valore. Il formato di visualizzazione è del tipo:
indirizzo1 byte1 byte2 indirizzo2
L'argomento intervallo è formato dall'indirizzo iniziale del primo blocco e dall'offset finale del blocco stesso; l'argomento indirizzo rappresenta l'indirizzo iniziale del secondo blocco. Consideriamo il seguente esempio:
-c ds:0f00 0f10 ss:0200
Questo comando richiede la comparazione tra i primi 11h byte (da DS:0F00h a DS:0F10h) del blocco di memoria che inizia da DS:0F00h, e i primi 11h byte del blocco di memoria che inizia da SS:0200h; se, ad esempio, il terzo byte del primo blocco vale 2Fh e il terzo byte del secondo blocco vale 1Ah, verrà mostrato l'output:
DS:0F02 2F 1A SS:0202
Con questo comando, è possibile visualizzare il contenuto di un determinato blocco di memoria (memory dump); in assenza di argomenti, vengono visualizzati i primi 128 byte del blocco di memoria che inizia dall'entry point.
In alternativa, è possibile specificare l'indirizzo iniziale e l'offset finale del blocco da visualizzare; se, ad esempio, vogliamo visualizzare i primi 21h byte del blocco di memoria che parte dall'indirizzo SS:FB00h, dobbiamo impartire il comando:
-d ss:fb00 fb20
Vediamo un esempio pratico che ci permette di effettuare il dump dell'area della RAM riservata ai vettori di interruzione; in base a quanto è stato esposto nei precedenti capitoli, sappiamo che quest'area parte dall'indirizzo logico 0000h:0000h e contiene 256 coppie Seg:Offset. La Figura 14.10, mostra il blocco dei primi 64 vettori di interruzione; questo blocco inizia quindi dall'indirizzo logico 0000h:0000h e termina all'offset 0100h-1h=00FFh (4*64=100h byte). Come si può notare, nella parte destra DEBUG visualizza anche i codici ASCII associati ai vari byte presenti in memoria (al posto dei codici minori di 20h e maggiori di 7Eh, viene visualizzato un punto); si tenga presente che le informazioni mostrate in Figura 14.10, possono variare da computer a computer e da versione a versione del DOS o di Windows.
Per interpretare correttamente le informazioni presenti in Figura 14.10, è necessario tenere presente che tutte le CPU della famiglia 80x86, seguono una importante convenzione per la memorizzazione degli indirizzi logici Seg:Offset; questa convenzione prevede che la componente Offset preceda in memoria la componente Seg.
È anche importante osservare che DEBUG, mostra la memoria con gli indirizzi crescenti da sinistra verso destra; come già sappiamo, in un caso del genere i dati di tipo WORD, DWORD, etc, appaiono disposti al contrario rispetto alla loro rappresentazione con il sistema posizionale.

Sulla base di tutte queste considerazioni, dalla Figura 14.10 possiamo ricavare gli indirizzi Seg:Offset da cui partono in memoria le ISR che gestiscono i vari vettori di interruzione; come è stato già spiegato in un precedente capitolo, queste ISR sono destinate esclusivamente ai programmi che lavorano in modalità reale. A titolo di curiosità, esaminiamo i primi 4 vettori.

Sul PC a cui si riferisce la Figura 14.10, la ISR predefinita per la gestione della INT 00h si trova in memoria all'indirizzo logico 0116h:108Ah; si tratta di una INT hardware che viene generata dalla CPU ogni volta che in un programma, si verifica una divisione per zero (overflow di divisione). In fase di avvio del computer, il DOS installa una propria ISR per la gestione di questa INT; a loro volta, anche i normali programmi possono installare una apposita ISR per la INT 00h.

Sul PC a cui si riferisce la Figura 14.10, la ISR predefinita per la gestione della INT 01h si trova in memoria all'indirizzo logico 0070h:06F4h; come è stato già spiegato in questo capitolo, se poniamo TF=1 nel registro FLAGS, la CPU genera una INT 01h (hardware) dopo l'elaborazione di ogni istruzione del programma in esecuzione. In questo modo, i debugger possono eseguire i programmi in single-step mode; anche i normali programmi possono installare una apposita ISR per la INT 01h.

Sul PC a cui si riferisce la Figura 14.10, la ISR predefinita per la gestione della INT 02h si trova in memoria all'indirizzo logico 056Dh:0016h; questa INT ha la massima priorità possibile, in quanto viene generata dall'hardware del computer per segnalare l'insorgere di un grave problema interno. Data la sua importanza, la INT 02h non può essere inibita (mascherata) attraverso l'istruzione CLI (clear interrupt enable flag); proprio per questo motivo, questa INT viene chiamata NMI o Non Maskable Interrupt (interruzione non mascherabile). La INT 02h arriva direttamente alla CPU attraverso un apposito piedino chiamato NMI; vedere a tale proposito la Figura 9.3 e la Figura 9.6 del Capitolo 9.

Sul PC a cui si riferisce la Figura 14.10, la ISR predefinita per la gestione della INT 03h, si trova in memoria all'indirizzo logico 0070h:06F4h; come è stato già spiegato in questo capitolo, la INT 03h viene generata dal codice macchina CCh (interruzione software), inserito dai debugger per la gestione dei breakpoint.

Tutti questi argomenti, verranno trattati in dettaglio nella sezione Assembly Avanzato. Questo comando, permette di inserire dei byte, a partire dall'indirizzo di memoria specificato dall'argomento indirizzo; è anche possibile specificare un elenco di byte da inserire in sequenza, a partire dalla posizione indirizzo.
Ad esempio, il seguente comando:
-e cs:0100 3a 2b 1f ff 22 4d 5e 6f
inserisce in memoria 8 byte, a partire dall'indirizzo logico CS:0100h. Questo comando, inserisce un elenco di byte all'interno del blocco di memoria specificato da intervallo; ad esempio, il seguente comando:
-f ds:00d0 00d3 a2 b4 cd f9
inserisce 4 byte, nel blocco di memoria compreso tra gli indirizzi logici DS:00D0h e DS:00D3h. Questo comando, permette di eseguire un programma che si trova in memoria; in assenza di argomenti, il comando Go avvia l'esecuzione a partire dall'entry point.
In alternativa, il programma può essere avviato anche a partire da =indirizzo; l'altro argomento indirizzi, permette di specificare uno o più breakpoint.
Ad esempio, nel caso del programma di Figura 14.9, il comando:
-g = cs:0111 cs:0118
provoca l'esecuzione delle prime tre istruzioni; all'indirizzo logico CS:0118h, il debugger salva il primo byte del codice macchina dell'istruzione:
mov ah, 4ch
e lo sostituisce con il codice macchina CCh, che provoca, come sappiamo, la generazione di una INT 03h. Subito dopo l'interruzione del programma, il debugger ripristina il vecchio codice macchina presente all'indirizzo CS:0118h e visualizza lo stato di tutti i registri della CPU, compreso FLAGS. Questo comando, permette di effettuare somme e sottrazioni tra valori esadecimali a 8 o 16 bit; ad esempio, il comando:
-h 4fff 003b
produce il seguente output:
503A 4FC4
Il primo valore, rappresenta la somma tra 4FFFh e 003Bh; il secondo valore, rappresenta la differenza tra 4FFFh e 003Bh. Questo comando, visualizza un byte letto da una porta hardware del PC; l'indirizzo della porta, viene specificato attraverso l'argomento porta.
Ad esempio, chi ha a disposizione un normale joystick a due assi e due pulsanti, può provare a leggere l'input dalla porta joystick standard 0201h; a tale proposito, bisogna impartire il comando:
-i 201
Tenendo premuti: nessuno, uno o entrambi i pulsanti, si possono vedere le variazioni del valore letto dalla porta joystick.

Si consiglia di non assegnare numeri a caso all'argomento porta; infatti, la lettura di particolari porte hardware, può provocare un blocco del PC!
Gli emulatori DOS forniti con Windows, presentano diverse limitazioni rispetto al DOS vero e proprio; queste limitazioni riguardano, in particolare, l'accesso diretto alle porte hardware da parte dei programmi DOS.
Tutto ciò che riguarda l'I/O con le porte hardware, verrà trattato nella sezione Assembly Avanzato. Questo comando, carica dal disco un file precedentemente passato come argomento a debug o un file precedentemente nominato con il comando Name (illustrato più avanti); l'argomento indirizzo permette di specificare l'indirizzo logico di memoria da cui inizierà il caricamento del programma. Gli altri parametri, fanno riferimento a settori logici del disco specificato da unità (0=A:, 1=B:, 2=C:, etc). Accedere ad un disco attraverso il metodo utilizzato dal comando Load, significa scavalcare il controllo che il SO ha sul file system; proprio per questo motivo, si raccomanda vivamente di usare con cautela il comando Load. Questo comando, permette di trasferire il contenuto di un blocco di memoria specificato da intervallo, in un'altra area della memoria che inizia da indirizzo; possiamo scrivere, ad esempio:
-m cs:0200 02ff cs:0400
In questo caso, i 100h byte compresi tra gli indirizzi CS:0200h e CS:02FFh, vengono trasferiti in un'area di memoria da 100h byte, che inizia dall'indirizzo CS:0400h; come al solito, si raccomanda vivamente di effettuare questo tipo di operazioni solo all'interno dell'area di lavoro predisposta da debug.
Il comando Move lavora correttamente anche quando il blocco sorgente e il blocco destinazione sono parzialmente sovrapposti; la parte del blocco sorgente, destinata ad essere sovrascritta dal blocco destinazione, viene trasferita per prima. Questo comando, permette di specificare uno o più file che possono essere caricati dall'interno di debug; il caricamento dei file avviene poi attraverso il comando Load (illustrato in precedenza). Questo comando, permette di scrivere un valore byte a 8 bit, nella porta hardware che si trova all'indirizzo porta; la scrittura di dati casuali in determinate porte hardware, può provocare un crash del PC!
Come è stato detto in precedenza, tutto ciò che riguarda le operazioni di I/O con le porte hardware, verrà trattato nella sezione Assembly Avanzato. Questo comando, termina l'esecuzione di debug e restituisce il controllo al DOS. Questo comando, visualizza il contenuto di uno o più registri della CPU; in assenza dell'argomento registro, viene visualizzato il contenuto di tutti i registri della CPU, compreso FLAGS.
Impartendo, ad esempio, il comando:
-r ax
viene visualizzato il contenuto del solo registro AX; subito dopo, debug attende che l'utente inserisca un nuovo valore da assegnare ad AX.
Gli unici nomi validi per i registri sono, AX, BX, CX, DX, SP, BP, SI, DI, CS, DS, ES, SS, IP (o PC), F (registro dei FLAGS); come si può notare, debug permette di modificare anche il contenuto dell'instruction pointer (o program counter).
Per modificare uno o più flags, bisogna quindi impartire il comando:
-r f
Il debugger risponde mostrando una linea del tipo:
NV UP EI PL NZ NA PO NC -
Il trattino finale indica che debug resta in attesa che l'utente inserisca i nuovi valori da assegnare a uno o più flags; le modifiche diventano effettive, subito dopo la pressione del tasto [Invio].
La sintassi utilizzata per i soli 8 flags disponibili, viene mostrata in Figura 14.11; si tratta delle stesse abbreviazioni usate da CodeView in Figura 14.8. In questa tabella, la colonna SET si riferisce ai flags a livello logico 1; la colonna CLEAR si riferisce ai flags a livello logico 0. Questo comando, permette di cercare in memoria una sequenza formata da uno o più byte (consecutivi e contigui); l'argomento intervallo indica il blocco di memoria nel quale effettuare la ricerca, mentre l'argomento elenco indica la sequenza da cercare.
Impartendo, ad esempio, il comando:
-s cs:0300 04ff 3d 2a b2
stiamo richiedendo la ricerca della sequenza 3Dh, 2Ah, B2h, in un blocco di memoria da 200h byte, compreso tra gli indirizzi CS:0300h e CS:04FFh; il debugger visualizza, eventualmente, tutti gli indirizzi di questo blocco, a partire dai quali è presente la sequenza cercata. Questo comando, permette di eseguire un programma in single-step mode; in assenza di parametri, viene eseguita per prima l'istruzione puntata da CS:IP.
È anche possibile richiedere l'esecuzione della sola istruzione che si trova in posizione indirizzo; l'argomento opzionale valore, indica il numero di istruzioni da eseguire.
Nel caso, ad esempio, del programma di Figura 14.9, possiamo eseguire la sola istruzione:
mov ah, 4ch
attraverso il comando:
-t = 1331:0118
Terminata l'esecuzione dell'istruzione, debug visualizza lo stato di tutti i registri della CPU. Questo comando, è formalmente simile a Dump; la differenza sostanziale sta nel fatto che Unassemble visualizza il contenuto della memoria sotto forma di istruzioni Assembly.
Impartendo, ad esempio, il comando:
-u cs:0200 020f
viene mostrato il disassemblaggio di un blocco di memoria da 10h byte, compreso tra gli indirizzi logici CS:0200h e CS:020Fh; il listato Assembly che si ottiene, potrebbe essere totalmente privo di senso!
Bisogna tenere presente che Unassemble, cerca di convertire in istruzioni Assembly tutto ciò che incontra nel blocco di memoria specificato; se questo blocco contiene, ad esempio, dati statici, si ottengono ovviamente istruzioni Assembly prive di significato.
È anche necessario ricordare che il debug del DOS, lavora correttamente solo in presenza di istruzioni appartenenti al set della CPU 8086 e della FPU 8087; in presenza di codici macchina a 32 bit, si ottengono istruzioni Assembly insensate. Il clone di debug fornito da FreeDOS, come è stato spiegato in precedenza, supporta anche i registri e le istruzioni a 32 bit. Questo comando (che è la controparte di Load), permette di salvare su disco il contenuto di un'area di memoria che parte da indirizzo; per accedere al disco, bisogna specificare, il numero di unità, il settore logico iniziale (primosettore) e il numero dei blocchi da scrivere. Come già sappiamo, il numero di unità è 0 per il disco A:, 1 per il disco B:, 2 per il disco C: e così via; nella sezione Assembly Avanzato vedremo un esempio pratico.
Come si può facilmente intuire, questo metodo di accesso al disco permette di scavalcare il controllo che il SO ha sul file system; a differenza, però, di quanto accade con il comando Load (lettura dal disco), il comando Write effettua una operazione di scrittura sul disco, che potrebbe rivelarsi dannosa per il file system stesso!

14.4 Il disassemblatore NDISASM

Il pacchetto NASM comprende anche un disassemblatore chiamato NDISASM.EXE; lo scopo di NDISASM è semplicemente quello di leggere il codice macchina di un file eseguibile e di tradurlo in istruzioni Assembly.

L'utilizzo di NDISASM richiede una certa esperienza da parte del programmatore; se si usa questo strumento in modo irrazionale, si ottiene un listato Assembly totalmente privo di senso.
Impartendo, ad esempio, il seguente comando dalla cartella ASMBASE:
c:\nasm\ndisasm exetest.exe
si perviene ad un listato Assembly incomprensibile; ciò è dovuto al fatto che NDISASM cerca di tradurre in codice Assembly anche l'intestazione e il blocco dati del file EXE.
Per evitare questo problema, dobbiamo indicare a NDISASM quali parti del file devono essere disassemblate e quali parti, invece, devono essere saltate; a tale proposito, le opzioni disponibili sono:
-e num
(salta i primi num byte del file)

e:
-k start,num
(salta num byte a partire dall'offset start).

Nel nostro caso, osserviamo che l'intestazione di EXETEST.EXE occupa 0200h byte, il blocco DATASEGM occupa 0447h byte ed è seguito da un buco di memoria da 1 byte, il blocco CODESEGM occupa 0147h byte ed è seguito da un buco di memoria da 1 byte, mentre il blocco STACKSEGM occupa 0400h byte; dobbiamo saltare quindi i primi:
0200h + 0447h + 0001h = 0648h byte
Non c'è bisogno di saltare i 0400h byte dello stack in quanto, come sappiamo, i dati non inizializzati che si trovano nella parte finale del programma non vengono inseriti nel file EXE; per disassemblare EXETEST.EXE dobbiamo quindi impartire il seguente comando:
c:\nasm\ndisasm -e 0x0648 exetest.exe
Il comportamento predefinito di NDISASM consiste nell'inviare il suo output direttamente allo schermo; se vogliamo redirigere l'output su un file chiamato, ad esempio, EXETEST.DIS, dobbiamo impartire il seguente comando:
c:\nasm\ndisasm -e 0x0648 exetest.exe > exetest.dis
Ripetendo due o più volte questa operazione, l'output di NDISASM viene aggiunto al vecchio file EXETEST.DIS; se vogliamo che tale file venga ricreato ogni volta da zero, dobbiamo impartire il seguente comando:
c:\nasm\ndisasm -e 0x0648 exetest.exe >> exetest.dis
I comandi come > e >> appartengono al DOS e rappresentano la cosiddetta redirezione dell'output.

Nel caso di COMTEST.COM, dobbiamo ricordare che i 0200h byte del PSP non vengono inseriti nel file; il programma inizia quindi direttamente con il codice.
Dal listing file si ricava che il programma contiene 0142h byte di codice, seguiti da 0452h byte di dati; per disassemblare COMTEST.COM dobbiamo quindi impartire il comando:
c:\nasm\ndisasm -k 0x0142,0x0452 comtest.com > comtest.dis
Gli esempi appena presentati sono molto semplici grazie al fatto che conosciamo tutta la struttura interna di EXETEST.EXE e COMTEST.COM; evidentemente, se vogliamo disassemblare file eseguibili di cui non sappiamo niente, dobbiamo procedere per tentativi sino ad individuare codice Assembly sensato.

14.5 Conclusioni

Sarebbe interessante a questo punto poter scrivere dei programmi che, prendendo spunto dai debugger, visualizzano sullo schermo, in tempo reale, tutte le informazioni relative al proprio "assetto" in memoria; in questo modo, si potrebbe ottenere una ulteriore verifica pratica, dei concetti teorici illustrati nel precedente capitolo.
Un programma del genere, dovrebbe essere in grado di leggere il contenuto dei registri della CPU e di mostrarlo sullo schermo; purtroppo però, a differenza di quanto accade con i linguaggi di alto livello, l'Assembly non dispone di alcuna procedura predefinita per la gestione dell'input e dell'output delle informazioni.
Come è stato spiegato in un precedente capitolo, l'Assembly più che un linguaggio di programmazione è un insieme di strumenti attraverso i quali si può fare di tutto; se vogliamo gestire l'input dalla tastiera o l'output sullo schermo, non dobbiamo fare altro che scrivere apposite procedure. La scrittura di queste procedure, richiede però la conoscenza dell'Assembly; si viene a creare quindi un problema in base al quale: "per conoscere l'Assembly dobbiamo scrivere dei programmi, ma per scrivere dei programmi, dobbiamo già conoscere l'Assembly!"
Per poter uscire da questo vicolo cieco, l'unica possibilità è quella di utilizzare, inizialmente, una libreria di procedure già scritte da altri programmatori; man mano che si impara a programmare in Assembly, si acquisiscono anche le conoscenze necessarie per scrivere in proprio queste procedure.
Nel prossimo capitolo, verrà utilizzata una di queste librerie, che ci permetterà di gestire in modo molto semplice, le operazioni di input dalla tastiera e di output sullo schermo.