Assembly Base con MASM
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 nel 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
QEDITOR (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 QEDITOR e inseriamo direttamente il codice macchina
del nostro programma; si ottiene così la seguente situazione:
Attraverso il menu Save Hex as Binary di QEDITOR, 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 MASM è in grado di creare informazioni di debugging
destinate, sia al Turbo Debugger, sia a CodeView; a tale
proposito, dobbiamo passare all'assembler l'opzione /Zi.
In riferimento, ad esempio, al file EXETEST.ASM presentato nel precedente
capitolo, dalla directory c:\masm\asmbase dobbiamo effettuare
l'assemblaggio attraverso il seguente comando:
c:\masm\bin\ml /Zi exetest.asm
L'opzione /Zi indica a MASM 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
/Zi 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 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.
- Comando Assemble; sintassi: A [indirizzo]
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.
- Comando Compare; sintassi: C intervallo indirizzo
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
- Comando Dump; sintassi: D [intervallo]
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.
- Comando Enter; sintassi: E indirizzo [elenco]
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.
- Comando Fill; sintassi: F intervallo elenco
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.
- Comando Go; sintassi: G [=indirizzo] [indirizzi]
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.
- Comando Hex; sintassi: H valore1 valore2
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.
- Comando Input; sintassi: I porta
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.
- Comando Load; sintassi: L [indirizzo] [unità]
[primosettore] [numero]
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.
- Comando Move; sintassi: M intervallo indirizzo
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.
- Comando Name; sintassi: N [nomepercorso] [elencoargomenti]
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).
- Comando Output; sintassi: O porta byte
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.
- Comando Quit; sintassi: Q
Questo comando, termina l'esecuzione di debug e restituisce il
controllo al DOS.
- Comando Register; sintassi: R [registro]
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.
- Comando Search; sintassi: S intervallo elenco
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.
- Comando Trace; sintassi: T [=indirizzo] [valore]
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.
- Comando Unassemble; sintassi: U [intervallo]
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.
- Comando Write; sintassi: W [indirizzo] [unità]
[primosettore] [numero]
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 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.