DEBUG / ASSEMBLY TUTORIAL | NEXT chapters |
Debug Tutorial by Fran Golden
I comandi di debug.exe non sono stati pubblicati in ordine alfabetico, ma in un ordine logico, sequenziale, in modo da facilitarne la comprensione anche a chi è a digiuno di questo genere di argomenti. Ricorda che l'utilizzo delle informazioni contenute in questo articolo è sotto la tua responsabilità e a tuo rischio. Debug è un comando DOS, MS-DOS, OS/2 e Microsoft Windows ( solo le versioni x86, no x64 ) che lancia il programma debug.exe ( o DEBUG.COM nelle vecchie versioni di DOS ). Questo è un tutorial su Debug per assistere gli studenti che devono comprendere il funzionamento interno dei computer con tecnologia Intel. L'obiettivo di questo manuale è istruire lo studente su come osservare il contenuto del microprocessore e di tutte le locazioni di memoria che il processore può raggiungere. Dopo aver familiarizzato con Debug ed aver compreso come Debug vede gli indirizzi di memoria, sarete introdotti alla programmazione a livello macchina, utilizzando Debug come un assembler. Al termine di questo tutorial ( 11 lezioni ) dovreste aver raggiunto una buona comprensione dei sistemi IBM e della programmazione di basso livello, essendo in grado di: guardare nell'area dati di DOS allocata nella memoria e determinare che tipo di periferiche sono installate nella scheda madre; guardare il funzionamento interno di DOS, in particolare a cose del tipo: il buffer della tastiera, il clock real time, la tabella degli interrupt, il chip rom bios e i chip della ram video; recuperare file cancellati da un floppy o un hard disk; recuperare da un hard disk dati illeggibili per DOS che, in caso contrario, andrebbero perduti; testare l'hardware, come la scheda video, stampanti, dischi e chip; formattare a basso livello un hard disk; infine, assemblare piccoli programmi NOTA: Il programma DEBUG incluso nelle versioni Windows® NT/2000/XP/2003 sembra funzionare come su DOS 5.0, ma con due fondamentali differenze: DEBUG non ha più l'accesso all'hard disk, ma solo a file esistenti o ai dischi esterni A:\ o B:\ ( comandi L e W ), ma solo se Windows è in grado di riconoscere i relativi filesystem! Inoltre, i comandi di Input e Output non hanno alcuna utilità, visto che l'interfaccia con il resto del sistema è solo una emulazione, non prevedendo alcun accesso diretto all'hardware ( A Guide to DEBUG, di Daniel B. Sedory ). FINE NOTA Il debugger viene fornito in tutte le versioni di Windows, con il file:
Per lanciarlo, è sufficiente aprire una finestra DOS. Al prompt di DOS scrivere:
Debug risponderà con un trattino al prompt:
Da questo momento, Debug resta in attesa di uno dei suoi tanti comandi composti da una sola lettera. Al prompt, è possibile anche specificare un file da caricare:
In questo caso, debug carica in memoria sè stesso e il file specificato, immettendo il primo byte del file nella locazione di memoria posta all'indirzzo ( offset ) 100H dell'area di lavoro. Debug si riserva un'area di lavoro di 65.536 locazioni di memoria ad un byte. Le prime 256 locazioni ( 000 - 100 in esadecimale ) di quest'area di lavoro sono riservate al Program Segment Prefix ( PSP ) del programma stesso ( debug ) e non devono essere modificate in alcun modo. Qualsiasi cosa dovessimo caricare con debug, deve essere allocata in locazioni con offset superiore a 100H. NOTA - Debug accetta solo locazioni espresse in notazione esadecimale. Quindi, in tutto l'articolo, verranno utilizzate le notazioni numeriche esadecimali, facendo seguire il numero dalla lettera H ( esadecimale ): negli esempi pratici di riga di comando, la lettera H non apparirà, per evitare di generare confusione nel lettore, ma resta inteso che i numeri saranno sempre espressi in notazione esadecimale. Inoltre, ritengo utile ricordare cosa si intende con i termini: locazione di memoria, segmento, offset. Per segmento, si intende un'area di memoria ( RAM ) di 65536 ( 64k ) locazioni consecutive, utilizzata dal sistema per allocare i programmi. Una locazione di memoria ( RAM ) è una sequenza di bit ( generalmente 8 ) che può essere utilizzata per conservare un dato o una istruzione. Un altra coppia di concetti chiave è quella di indirizzo logico e indirizzo fisico. Il problema è sorto fin dall'inizio: i progettisti dell'8086 si sono trovati a gestire un bus d'indirizzi di 20 linee con registri CPU a 16 bit; in pratica era evidente la necessità di porre un indirizzo valido sul rispettivo bus ma non era possibile farlo perchè gli strumenti a disposizione ( registri ) erano troppo piccoli. Da questa esigenza è nata la tecnica della segmentazione della memoria, un abile sotterfugio per poter puntare ogni indirizzo con 2 registri a 16 bit: il primo ( detto appunto registro di segmento ) individua un'area di 64kBytes consecutivi, dentro lo spazio indirizzabile del processore. Il secondo ( detto registro di offset ) è in grado di scorrere ciascuna delle 65536 locazioni contenute nel segmento, in virtù dei diversi possibili valori che può assumere. Dunque con un indirizzo logico, cioè con una coppia di registri di tipo:
è possibile ottenere un indirizzo fisico. L'indirizzo fisico ( cioè il numero binario a 20 bit che sarà posto sul bus address ) si ottiene sommando il valore di un registro di segmento moltiplicato per 16 ( moltiplicare per 16 significa aggiungere a destra del valore esadecimale del registro di segmento 4 bit, un nibble, di valore nullo, uguale a 0 ) con il valore di un registro di offset. Se:
moltiplicando 1234H x 10H abbiamo:
Sommando, infine, 12340H e 0100H, abbiamo:
che è l'indirizzo fisico. Quante locazioni di memoria ( indirizzi fisici ) è possibile raggiungere con il metodo degli indirizzi logici? L'ultimo indirizzo raggiungibile è esprimibile nella coppia segmento/offset seguente:
L'ultimo indirizzo raggiungibile, quindi, è un indirizzo a 21 bit. Al tempo in cui il bus degli indirizzi era di soli 20 bit, di fronte ad un indirizzo composto da più di 20 bit, la CPU troncava i bit più significativi. Il nostro indirizzo fisico 10FFEF sarebbe, quindi, diventato FFEF. Un motivo di confusione nell'utilizzo del formato Segmento:Offset è che uno stesso indirizzo fisico può essere rappresentato da più coppie di valori: ci possono essere fino a 4096 coppie di valori che esprimono lo stesso indirizzo fisico, o indirizzo assoluto.
In realtà, il primo segmento di memoria contiene solo i primi 16 byte ( o paragrafi: sequenza di 16 byte consecutivi di memoria ) unici. C'è solo una coppia di valori che esprime ciascuno di quei sedici indirizzi: segmento 0000 e uno dei sedici valori che può assumere l'offset: 0000 fino a 000F.
Per ogni paragrafo ( 16 byte ) superiore della memoria, il numero di segmenti sovrapponibili cresce di uno, fino ad arrivare al termine del primo segmento. A questo punto, ogni paragrafo successivo di memoria avrà un numero di segmenti sovrapponibili pari a 4,096!
Come è possibile verificare, il segmento 9000 è l'ultimo segmento ( 64kb ) interamente compreso nei limiti di quell'area di memoria chiamata "Memoria Convenzionale" ( i primi 640kb o 655.360 byte). Il primo paragrafo del segmento A000 è l'inizio di quell'area di memoria chiamata Upper Memory Area ( UMA ), che contiene un totale di 384kb o 393.216 byte. Il segmento F000 è l'ultimo segmento dell'area UMA.
Gli indirizzi compresi nel segmento FFFF venivano, una volta, mappati nel segmento 0000. Più tardi, la memoria superiore al MB ( 1.048.576 byte ) che ancora poteva essere raggiunta utilizzando le coppie di valori segment:offset venne chiamata High Memory Area ( HMA ). ( A Guide to DEBUG ) FINE NOTA MS-DOS permette solo due tipi di programmi da verificare con il debugger: i programmi con estensione EXE ed i programmi con estensione COM. La differenza tra le due estensioni è nel modo in cui DOS gestisce le porzioni del programma, porzioni chiamate spesso Program Segment Prefix ( PSP ): si tratta di un blocco di memoria di 256 byte riservato al programma ( debug.exe ) e necessario a DOS per riconsegnare il controllo al sistema operativo una volta che il programma ( debug.exe ) termina. Senza addentrarci troppo nei dettagli, almeno in questo momento, vediamo quali sono le principali differenze tra le estensioni EXE e COM. Estensione COM:
Estensione EXE:
Dalla comparazione, è evidente che è molto più difficoltoso assemblare programmi E' importante ora comprendere che:
Q ( quit ). Chiude debug.exe e restituisce il controllo a DOS. Non accetta parametri. Assicurarsi di aver scritto su disco il programma o i dati prima di lanciare Q, altrimenti verranno persi. D ( dump ). Stampa a video il contenuto delle locazioni di memoria, suddiviso in due schermate: una in esadecimale e l'altra in ASCII.
Il parametro range specifica la locazione di partenza e l'ultima locazione da leggere oppure la lunghezza della sequenza da leggere ( in esadecimale ), preceduta dalla lettera L.
Se le locazioni vengono espresse con il solo offset, debug.exe mostrerà le locazioni dello stesso segmento in cui si trova. Per ricevere, invece, il contenuto di un segmento diverso da quello in cui è allocato debug.exe, occorre che il primo valore sia espresso con la coppia:
Se non viene specificato alcun range ( intervallo ) debug.exe stamperà 128 byte a partire dall'ultimo indirizzo stampato. Per esempio:
Ripetendo il comando D, avremo:
E ( enter ). Inserisce dati a partire dalla locazione di memoria specificata. I dati possono essere inseriti sia in esadecimale, sia in formato ASCII. Per inserire una stringa ( ASCII ), è necessario racchiuderla tra virgolette ( singole o doppie ). Il dato presente alla locazione specificata verrà perduto e sarà sostituito dal nuovo dato.
dove NOTA
In questo esempio, prima eseguiamo un dump ( stampa ) dei valori delle locazioni di memoria con offset compresi tra 200H e 205H ( quindi, all'interno dello stesso segmento in cui si trova debug.exe ). Il debugger ci dice che in quelle sei locazioni di memoria si trovano i valori esadecimali 20 69 20 66 69 6C, corrispondenti alla stringa ASCII ( spazio )i fil. A questo punto, eseguiamo un enter, inserendo i valori esadecimali 43,49, 41,4f, a partire dalla locazione di memoria con offset 200H. Visto che debug.exe non ci informa se l'inserimento è andato a buon fine ( in realtà, ci avvertirebbe se qualcosa fosse andato storto ), rieseguiamo un dump, in modo da verificare lo stato delle stesse locazioni di memoria stampate la prima volta. Il valore delle prime quattro locazioni di memoria è cambiato. Come per il comando D ( dump ), l'indirizzo della prima locazione di memoria ( address ) può essere espresso sia con il solo valore di offset
( in questo caso, la modifica verrà applicata a partire dalla locazione di memoria con l'offset indicato all'interno dello stesso segmento in cui è allocato debug.exe ), oppure con la coppia
FINE NOTA Se viene specificato solo il valore dell'indirizzo della prima locazione di memoria ( address ), ma non i valori da scrivere ( list ), debug.exe mostrerà l'indirizzo di partenza ed il suo contenuto, mettendosi in attesa del nuovo dato da inserire in sostituzione dell'attuale.
Per modificare questo valore, è sufficiente scrivere il nuovo valore:
In questo esempio, il valore 41H presente alla locazione di memoria con offset 100H è stato modificato in 35H. E' possibile inserire valori di byte consecutivi con un solo comando E. Invece di premere il tasto Enter ( Invio ) dopo aver cambiato un valore, è sufficiente premere la barra spaziatrice e debug.exe mostrerà la locazione di memoria successiva, nella quale potrete inserire un nuovo valore. Ripetete questo processo quante volte volete. Premendo la barra spaziatrice tre volte, debug.exe mostrerà le tre locazioni di memoria successive.
Per ritornare al byte precedente, premete il tasto - ( trattino ). Ogni volta che viene premuto il tasto trattino ( - ), debug.exe torna indietro di un byte. Per uscire dal comando E, è sufficiente premere il tasto Enter ( Invio ). Per inserire dati utilizzando una stringa di testo, è necessario racchiudere la stringa di testo tra le virgolette:
Questo comando inserirà la stringa di caratteri ASCII in 25 locazioni di memoria consecutive ( una locazione per ciascun carattere e spazio ), a partire dalla locazione con offset 100H. F ( fill ). Riempie le locazioni di memoria specificate con il valore specificato. Il valore può essere espresso sia in esadecimale, sia in ASCII.
dove
Se C ( compare ). Compara due blocchi di memoria,
dove
compara il blocco di memoria compreso tra l'offset 100H e l'offset 220H del segmento in cui si trova debug.exe con il blocco di memoria che inizia alla locazione con offset 500H.
compara il blocco di memoria che inzia alla locazione con offset 100H ed ha una lunghezza di 121 ( in notazione esadecimale ) byte del segmento in cui si trova debug.exe con il blocco di memoria che inizia alla locazione con offset 500H. R ( register ). Stampa a video o modifica i contenuti di uno o più registri del microprocessore. Il comando lanciato senza alcun parametro, stampa a video tutti i registri.
E' possibile specificare il registro da stampare a video o modificare.
Se si specifica un registro ( AX ), debug.exe stampa a video il valore a 16 bit di quel registro in notazione esadecimale, seguito da due punti. Se si desidera MODIFICARNE il valore, basta scrivere il nuovo valore e premere INVIO. Se, invece, non si desidera modificare il valore di quel registro, è sufficiente premere INVIO. Se al posto del nome del registro, si inserisce la lettera F, debug.exe stampa a video lo stato corrente del registro delle FLAG.
A questo punto, debug.exe attende che inseriate, dopo il trattino, il codice a due lettere della FLAG sulla quale desiderate agire. Ciascuna FLAG ha un codice a due lettere che esprime il suo stato attuale:
Se si desidera MODIFICARE il valore di una o più FLAG, basta scrivere il o i nuovo/i valore/i e premere INVIO. Se, invece, non si desidera modificare il valore di alcuna FLAG, è sufficiente premere INVIO.
Quando si apre debug.exe, i registri di segmento sono puntati alla locazione di memoria libera più bassa ( 0D28 nel nostro esempio ), il registro IP punta sempre alla locazione di memoria 100H, tutte le FLAG sono azzerate e tutti gli altri registri sono azzerati eccetto il registro SP ( stack ), che è impostato a FFFE. A ( assemble ). Assembla istruzioni mnemoniche Assembly in memoria. Il comando è utilizzato per creare codice macchina eseguibile da istruzioni assembly. Tutti i valori numerici devono essere espressi in notazione esadecimale e possono essere composti al massimo da 4 cifre.
dove NOTA. Per verificare il funzionamento dell'istruzione A, riempiamo di 0 i primi 128 ( 80 in notazione esadecimale ) byte del segmento di default:
Ora, lanciamo il comando A in modalità interattiva, senza alcun parametro. In questo modo, Debug.exe resterà in attesa delle istruzioni CPU da allocare: ogni istruzione inserita e confermata con il tasto Invio verrà allocata nella locazione di memoria successiva a quella appena utilizzata, partendo dalla locazione con offset 100H ( la locazione puntata dai registri
Nelle locazioni di memoria 100H, 101H e 102H troviamo: l'istruzione CPU MOV AX ( B8 ) ed il valore da spostare ( 01H ). Come visto in precedenza, l'istruzione B8 ( 10111000 ) nei processori della famiglia 8088 significa proprio: "sposta i prossimi due byte in AX". I due byte che esprimono il valore da spostare sono memorizzati con la parte bassa ( 01 ) prima e la parte alta ( 00 ) dopo. Inseriamo una seconda istruzione:
Nelle locazioni di memoria 103H, 104H e 105H troviamo: l'istruzione CPU MOV BX ( BB ) ed il valore da spostare ( 1234H ). I due byte che esprimono il valore da spostare sono memorizzati con la parte bassa ( 34 ) prima e la parte alta ( 12 ) dopo. Inseriamo una terza istruzione:
Nelle locazioni di memoria 106H e 107H troviamo l'istruzione CPU ADD BX, AX e metti il risultato in BX ( 01C3 ). Con queste semplici operazioni, abbiamo inserito le istruzioni CPU in memoria. Tali istruzioni, naturalmente, non sono state ancora eseguite. Tanto è vero che se chiediamo a debug.exe di stampare a video i valori dei registri, troveremo AX e BX ancora puliti ( con valore zero ).
Per eseguire le istruzioni memorizzate, occorrerà lanciare un comando di esecuzione, quale il comando G ( go ). La sola utilità del comando A risiede proprio nella sua capacità di assemblare le istruzioni inserite, segnalandoci eventuali errori. FINE NOTA G ( go ). Esegue il programma in memoria. Il comando G utilizza il registro IP come puntatore alla istruzione da eseguire.
dove NOTA. Al nostro programma memorizzato dobbiamo solo aggiungere un interrupt. Solitamente, è bene scegliere INT 20, ma in questo caso, per poter vedere i cambiamenti apportati ai registri, scegliamo INT 3:
Ed ecco la sequenza di istruzioni eseguite:
A questo punto, se volessimo chiedere a debug.exe di eseguire una seconda volta la stessa sequenza di istruzioni, non potremmo più dare il comando G senza parametri, poichè debug.exe tenterebbe di eseguire l'istruzione contenuta nella locazione di memoria con offset 108H, che è il valore contenuto nel registro IP, ma dovremmo specificare l'offset della locazione di memoria dalla quale iniziare l'esecuzione:
Se, come ultima istruzione, avessimo eseguito INT 20, ci troveremmo i registri AX e BX reimpostati a zero ed il puntatore IP reimpostato alla locazione di memoria con offset 100H. FINE NOTA Prima di lanciare il comando G bisogna sempre verificare il valore contenuto nel registro IP, per assicurarsi che punti alla istruzione che vogliamo sia eseguita. Per vedere il valore del registro IP ed eventualmente modificarlo, c'è il comando:
Il registro IP deve sempre contenere il valore 100H affinchè venga eseguito l'intero programma.
Quando terminate il programma con l'istruzione INT 3 ricordate che i valori di tutti i registri vengono preservati e non azzerati o riportati al loro valore iniziale, mentre il registro IP conterrà l'offset della locazione di memoria dell'ultima istruzione eseguita!
INT 20, al contrario, al termine del programma, azzera tutti i registri e imposta il registro IP al suo valore iniziale: 100H.
Il patrametro
debug.exe eseguirà il programma fino all'offset 103H, stampando a video il contenuto di tutti i registri. Vedere i comandi P e T. N ( Name ). Specifica il nome del file che verrà utilizzato dai comandi L o W.
L ( Load ). Carica un file o il contenuto di un settore del disco in memoria.
dove:
debug.exe caricherà, nelle locazioni di memoria il cui primo indirizzo offset è 0100, 7 settori, a partire dal settore 5, dal floppy disk ( 0 ). La directory root inizia proprio dal settore numero 5 ed occupa 7 settori. NOTA ( da Tutorial Assembler di Giorgio OBER ). La numerazione di settori logici proseguirà in sequenza sulla medesima traccia ( Cylinder ) del lato ( Head ) corrente e proseguirà ( in verticale ) sulla stessa traccia del lato successivo fino all'esaurimento di tutte le facce disponibili; per riprendere, lato dopo lato, sulla traccia ( cilindro ) successiva, verso l'interno del disco. Questo modo logico di trovare i settori di un disco è tipico del DOS ed è diverso da quello assoluto/fisico usato dal BIOS e costituito dalla terna di numeri detta sinteticamente CHS ( Cylinder, Head, Sector ). Così, per esempio, il primo settore fisico, il boot record, identificato dal numero 0,0,1, corrisponde al settore logico 0.
La stringa si legge: carica ( L ), a partire dall'indirizzo 0100, dal disco 0 ( driver A ) il settore n°0 ( il primo ) nella quantità 1 ( un solo settore, cioè 512 bytes ). Analogamente per analizzare le 2 copie della FAT ( File Allocation Table ) di un floppy disk da 1,44M, possiamo dare il comando seguente, che le copia a partire dall'indirizzo ( offset ) 1000H. Notoriamente ciascuna FAT occupa, in questo caso, 9 settori, subito dopo quello del boot record, analizzato in precedenza; perciò il comando sarà ( per la prima copia di FAT ): carica ( L ), a partire dall'indirizzo 1000, dal disco 0 ( driver A ), i settori a partire dal n°1 ( il primo ) nella quantità 9:
oppure ( per la seconda copia di FAT ): carica ( L ), a partire dall'indirizzo 1000, dal disco 0 ( driver A ), i settori a partire dal n°10 ( in exa 0AH ) nella quantità 9:
Se vuoi continuare con questo gioco ricorda che, in un floppy disk da 1,44M, la directory occupa i 14 settori successivi alle 2 copie della FAT; è facile concludere che il comando da dare sarà: carica dal disco 0 ( driver A ) i settori a partire dal n°19 ( cioè 1+9+9=19, in esadecimale 13H ) nella quantità 14 ( in esadecimale 0EH ):
FINE NOTA. Se il comando viene invocato senza alcun parametro, debug.exe ricaricherà il file che era stato specificato nella linea di comando all'apertura di debug.exe, all'indirizzo
debug.exe, inoltre, imposta i registri
Nel nostro caso,
Sarebbe buona cosa verificare il nome del file in modo da assicurarsi che il file o il programma su cui si sta lavorando sia sul disco. H ( Hex ). Calcola la somma e la differenza dei due valori dati.
dove 03C4 è la somma, mentre FE3A è la differenza. I ( input ). Legge e stampa a video un byte dalla porta specificata. L'indirizzo della porta può essere un valore composta da 8 o 16 bit. E' possibile avere fino a 65,535 indirizzi di porta ( FFFF ).
M ( move ). Copia il contenuto di un blocco di memoria in un altro blocco.
dove
E' possibile utilizzare questo comando quando si deve inserire una istruzione in un programma già scritto. Supponiamo di dover inserire l'istruzione Mov ax,5600 nella locazione di memoria con offset 160 in un programma su cui stiamo lavorando.
Abbiamo copiato tutto il programma, a partire dalla locazione di memoria con offset 160H, nel blocco di memoria che inizia dall'offset 1000H. Poi, abbiamo assemblato la nuova istruzione alla locazione di memoria con offset 160H. Infine, per riportare il programma nel blocco precedentemente occupato, ( avendolo copiato in parte all'offset 1000H ), abbiamo ricopiato il contenuto del blocco che inizia dalla locazione di memoria con offset 1000H nel blocco che ha inizio dall'offset 163H ( 163 è l'offset successivo alla nuova istruzione inserita ). O ( output ). Invia un byte alla porta specificata.
dove
P ( proceed ). Esegue un loop, una istruzione ripetuta, un interrupt, una subroutine, o esegue un trace all'interno di una qualsiasi altra istruzione.
dove S ( search ). Cerca all'interno di un'area della memoria un byte o una sequenza di byte.
dove
cerca, nell'area di memoria compresa fra gli offset 100 e 250, la sequenza di byte: 41 55 66. Per cercare, all'interno della stessa area di memoria, la stringa testuale "Data Institute":
Il comando S ( search ) è case sensitive. T ( trace ). Esegue una istruzione alla volta e stampa a video il contenuto dei registri, lo stato delle FLAG e l'istruzione successiva da eseguire.
dove U ( unassemble ). Disassembla, stampando a video i byte ed i corrispondenti codici sorgente ( nella loro forma mnemonica ), con gli indirizzi ed i valori.
dove
NOTA. E' importante riportare alcune osservazioni tratte dal Tutorial Assembler di Giorgio OBER.
W ( write ). Scrive su un file
per salvare un file su disco, o uno specifico numero di settori del disco specificato.
per scrivere i settori del disco, dove
NOTA - NON fate esperimenti con il comando W! Il comando può essere utilizzato per creare nuovi file sul disco, ma solo se usato in modo appropriato. Provare a scrivere direttamente in un settore dell'hard disk sarebbe raramente da considerare un uso appropriato di questo comando! Scrivere direttamente su hard disk utilizzando i numeri di settore può comportare perdita di dati o addirittura l'impossibilità di riavviare il sistema! Windows XP e successivi impediscono di sovrascrivere direttamente i settori dell'hard disk, ma permettono di sovrascrivere i settori di floppy disk, ( A: o B: ). FINE NOTA Per scrivere un file su disco, bisogna prima nominarlo con il comando N ( name ), specificando eventualmente anche il percorso, per esempio:
Una volta assegnato un nome al nostro file, bisogna specificare la dimensione totale del file in byte, impostandola nei registri
Fate attenzione: il registro BX è a zero prima di scrivere un file COM su disco. Ciascuna cifra in BX rappresenta 65000 byte ( in decimale ). Per il nostro scopo, BX sarà sempre a zero. NOTA. E' importante riportare alcune osservazioni tratte dal Tutorial Assembler di Giorgio OBER.
Il linguaggio Assembly è un linguaggio di bassissimo livello che utilizza prefissi mnemonici seguiti da uno o due operandi. I comandi sono in inglese e utilizzano parole comuni. Per esempio:
Questi comandi sono abbastanza comprensibili, proprio grazie all'uso di parole facili da ricordare.
Debug permette di scrivere piccoli programmi usando le istruzioni, nella loro forma mnemonica, del set delle istruzioni per i microprocessori 8088. Ci viene richiesto di specificare l'indirizzo utilizzato dal programma nella forma di numero offset. Questo significa che dobbiamo fare molta attenzione alle locazioni di memoria di tutte le istruzioni utilizzate nel programma.
Tutte le informazioni che seguono sono relative alla famiglia di microprocessori 8086, che include i processori 80286, 80386 e 80486. Noi parleremo del set di istruzioni della CPU 8088. Per gli altri microprocessori, dovrete recuperare il set di isrtuzioni addizionale relativo a quella CPU. Questi processori sono compatibili tra di loro: tutti processori successivi comprendono il set di istruzioni per il processore 8088. Il microprocessore 8088 è un chip del tipo HMOS TTL a 40 PIN che utilizza una fonte a 5 volt. IBM lo definisce un processore a 16 bit, ma in realtà si tratta di un processore a 8 bit. Questa discrepanza deriva dal fatto che il processore ha solo 8 PIN per i dati provenienti dall'esterno, ma ha, al suo interno, registri per i dati a 16 bit. In altre parole, i dati vengono trasferiti al processore 8 bit alla volta, ma il processore 8088 opera, attraverso i registri, su gruppi di 16 bit. All'interno del processore 8088 ci sono diversi registri che vengono utilizzati per memorizzare temporaneamente i dati e / o modificarli per un brevissimo tempo. Questi registri possono essere pensati come latch di tipo D ( latch è un circuito elettronico con almeno due stati stabili e che è quindi in grado di memorizzare un bit di informazione ) o flip-flop di tipo JK a 16 bit. Appena completata una operazione sul dato presente in uno dei registri, il dato deve essere spostato in una locazione RAM, per una memorizzazione di maggior durata, fino a quando non verrà salvato su file. MICROPROCESSORE HMOS 8088 8-BIT
DESCRIZIONE DELLE FUNZIONI DEI PIN ( immagine CPU 8088 )
Il processore 8088 gestisce indirizzi a 20 bit per indicare il byte di memoria selezionato. La memoria è gestita come un blocco delle dimensioni massime di 1 megabyte, con indirizzi compresi tra i valori:
Questa memoria viene ulteriormente suddivisa in blocchi più piccoli chiamati segmenti, la cui dimensione è 64 Kbyte. Ci sono quattro tipi di segmento, utilizzati per i dati e per il codice di un programma:
L'indirizzamento viene realizzato combinando uno dei quattro registri di segmento ad alta velocità, CS, DS, ES, e SS, con un registro IP ( instruction pointer ). Il registro di segmento punta al segmento di memoria, mentre il registro IP punta ad una locazione ( offset ) all'interno di quel segmento. I registri sono divisi in gruppi in base alla loro funzione. I registri con una funzione generica ( General purpose registers ) sono a 16 o a 8 bit. Il sistema operativo alloca nella memoria bassa, dalla locazione 00000 alla locazione 0B000, fino a 3 programmi differenti. L'ultimo indirizzo utilizzato dipende dalla versione DOS e dal numero di driver caricati durante il processo di boot.
MS-DOS è un sistema operativo a tre livelli, composto da tre programmi che sono i tre livelli di programmazione.
Tutte le sub-routine che fanno parte di questi tre programmi, scritte da Microsoft e IBM, eseguono funzioni specifiche nel computer e sull'hardware. Per esempio, ci sono sub-routine che sono state scritte solo per controllare il monitor video e come i dati vengono mostrati sullo schermo. Queste routine sono utilizzate per muovere il cursore, modificare il colore dei caratteri, le dimensioni dei caratteri, stampare stringhe di testo e molte altre funzioni legate al monitor. Altre sub-routine controllano ogni elemento hardware connesso al sistema. Queste sub-routine usate dal sistema operativo richiedono molte istruzioni 8088 per portare a termine un determinato compito e sono molto complesse. Fortunatamente, IBM e MicroSoft hanno creato un sistema che permette al programmatore di utilizzare tutte queste sub-routine, testate e provate, nei suoi programmi. Questo rende la programmazione un po' più facile per un programmatore, visto che spenderà meno tempo a scrivere codice che è già stato scritto da programmatori più esperti. Il processore 8088 ha una istruzione speciale chiamata INT che esegue ognuna di queste sub-routine dall'interno di un programma. INT è ciò che chiamiamo un interrupt software, perchè, quando eseguito da un programma, il microprocessore si interrompe per un momento mentre la sub-routine in IBMBIO o IBMDOS viene eseguita. Terminata la sub-routine, il microprocessore può tornare al programma originario e completare le istruzioni in esso contenute. SET DELLE ISTRUZIONI ( 41 ISTRUZIONI ) PER IL PROCESSORE 8088 Per avere una lista completa delle istruzioni per i processori Intel, vai alla pagina Intel http://developer.intel.com/products/processor/manuals/index.htm e scarica i manuali:
Istruzioni di trasferimento dati
Istruzioni aritmetiche
Per prevedere i risultati delle istruzioni logiche, occorre prima convertire i valori esadecimali in numeri binari e eseguire la funzione logica su ciascuno dei bit. Le istruzioni logiche servono per la manipolazione dei bit nei registri specificati. Esse rappresentano il solo modo per impostare o azzerare i bit all'interno di qualsiasi registro. Ecco la tabella per convertire numeri in notazione decimale, esadecimale e binaria:
Per convertire un numero in notazione esadecimale in binario, sostituisci ciascuna cifra esadecimale con i quattro bit del corrispondente binario:
Per convertire un numero in notazione binaria in esadecimale, sostituisci ciascun gruppo di 4 bit con il corrispondente esadecimale:
Se il registro AX contiene il numero in notazione esadecimale 45A1, la sua rappresentazione binaria sarà, quindi:
Istruzioni di stringa
Istruzioni di trasferimento di controllo
Controllo del processore
Istruzioni speciali
Istruzioni di trasferimento di controllo
Il processo di boot inizia quando il circuito di reset, allocato nel chip di generazione dell'orologio, invia un impulso al PIN 21 ( RESET ) della CPU 8088. Questo PIN è connesso al circuito di reset del microprocessore e il suo compito è di impostare un indirizzo di reset sul bus degli indirizzi A0 ... A20. A questo punto, viene eseguita una istruzione di JUMP all'indirizzo F000:FFF0 della ROM bios che punta alla prima istruzione del BIOS. Il programma della ROM bios è di circa 8 Kbyte e controlla tutto l'hardware presente nel sistema. I chip della CPU vengono inizializzati con i valori appropriati di default per controllare cose quali il monitor, i dischi, le porte delle stampanti e la tastiera. Terminata l'inizializzazione dell'hardware, il programma esegue una diagnostica completa: ROM, RAM etc., per completare il processo definito POST o Power On Self Test. Se non è rilevato alcun errore durante l'esecuzione del POST, il BIOS cerca il disco A e verifica se la porta è aperta o chiusa. Nel caso la porta fosse chiusa, la testina si posizionerebbe sulla posizione track 0, head 0, sector 0 del disco A, e il boot loader verrebbe trasferito in memoria. Se la porta al disco A fosse, invece, aperta, il processo di boot continuerebbe dal disco C, se esistente. A questo punto, il boot loader prende il controllo della CPU, eseguendo una serie di istruzioni e cercando nella directory del disco i file di sistema, IBMBIO e IBM DOS. Se questi file sono presenti nel disco, vengono caricati nella memoria bassa, in quell'ordine, insieme ad eventuali driver elencati nell'istruzione
del Debug è molto utile quando si desideri eseguire un debug di un programma o eseguire un programma per la prima volta. Quando un programma contiene un errore nel codice e viene eseguito, molto probabilmente il computer lo chiuderà costringendoti a riavviare la macchina. Questo può essere evitato eseguendo solo una piccola parte alla volta del programma per verificare che sia OK e che faccia esattamente ciò che ti aspettavi che facesse. Sotto c'è il codice di un piccolo programma e la spiegazione di come io utilizzerei l'indirizzo ed i parametri di breakpoint per eseguire un debug. Assembla il seguente programma e salvalo su disco con il nome BREAK.COM. Per utilizzare i breakpoint dovrai caricare il programma dal disco ogni volta in cui il programma terminerà normalmente.
Questo è un semplicissimo programma che stampa un breve messaggio a video. Il messaggio si trova alla locazione di memoria con offset 200 e l'ultimo carattere del messaggio è il dollaro ( $ ) o 24 in esadecimale. Il codice 24 ( $ ) viene chiamato end of file marker ( EOF ), o marcatore di fine file. Una volta stampato a video il messaggio, il programma continua, restando in attesa di un input da parte nostra. Appena avremo premuto un tasto, il codice ascii corrispondente al tasto premuto viene memorizzato nel registro AL. Il programma, quindi, verificherà se il tasto premuto corrisponde al codice ascii 1B. Se sì, il programma termina e il controllo torna a DOS. In caso contrario, il programma salta all'istruzione che richiede un altro input. Il programma terminerà solo quando sarà premuto il tasto ESCAPE. Ho suddiviso il programma in blocchi di codice, ciascuno dei quali esegue un compito preciso. Le funzioni in questo programma sono:
Per eseguire il debug, useremo i comandi P e G per avanzare nel programma una istruzione o un interrupt alla volta oppure eseguendo un gruppo di istruzioni fino ad uno specifico offset. Dopo aver spezzato il programma in gruppi di istruzioni che eseguono ciascuno una funzione, eseguiremo questi gruppi di istruzioni per verificare che funzionino correttamente. Per i prossimi passi, fai riferimento al programma appena scritto. Le istruzioni da 0100 a 0106 stampano a video un messaggio ( istruzione INT 21 ). Per testare questo gruppo di istruzioni, usiamo il comando G per impostare un break-point, utilizzato da debug per controllare il flusso del programma. Impostiamo il registro IP a 0100 e scriviamo:
Questo comando eseguirà tutte le istruzioni a partire dall'offset 0100 fino all'offset 0107. Se il codice in questa parte del programma è valido, verrà stampato a video il messaggio memorizzato a partire dall'offset 200. Il comando G, infatti, viene usato con un indirizzo di memoria di partenza, seguito dall'indirizzo finale dove il microprocessore interrompe l'esecuzione del programma ( break-point ). Se, per una qualsiasi ragione, il messaggio non venisse stampato a video, potremmo concludere che c'è un problema nel codice, in quel gruppo di istruzioni. Per eseguire una istruzione alla volta, reimposteremo il registro IP a 0100 ed inseriremo il comando P.
Il comando P è molto simile al comando T ( trace ), poichè esegue una istruzione alla volta. Tuttavia, il comando P esegue anche l'istruzione INT, al contrario del comando T. Si può utilizzare uno dei due comandi indifferentemente, visto che l'istruzione da testare è un'istruzione MOV, che sarà verificata controllando il valore del registro AH subito dopo l'esecuzione dell'istruzione MOV AH,9. Sia il comando P che il comando T mostrano i contenuti dei registri, dopo aver eseguito una istruzione. Il registro IP viene aggiornato dal microprocessore, puntando alla istruzione seguente del programma. Se questa istruzione appena eseguita funziona in modo appropriato, digitiamo:
Con questo comando, verrà eseguita l'istruzione MOV successiva, all'offset 102. Ancora una volta, verificheremo i registri per controllare l'esito dell'istruzione MOV DX,200. Ora digitiamo:
e verrà eseguita l'istruzione INT all'offset 0105, che stamperà a video il messaggio. Questo processo dovrà proseguire fino a quando avremo trovato il codice che non funziona correttamente nel programma, utilizzando i comandi:
Creazione di un programma COM che stampa a video un file di testo Con un editor di testo, crea un file di testo che vuoi stampare a video, chiudendolo con un simbolo dollaro ( $ ). All'inizio del file di testo, inserisci una riga di spazi, così da conservare 80 byte da utilizzare più tardi. Salva il documento come
Carica in memoria il file con il comando:
Verifica, con il comando Dump ( D ) che il file sia stato caricato in memoria correttamente. Noterai che i primi 50 ( in notazione esadecimale ) byte contengono il valore 20, che è il codice ASCII per lo spazio. Durante la creazione del file, infatti, hai inserito 80 spazi, e 80 è la notazione decimale del numero esadecimale 50. Ora assembla il seguente codice, a partire dall'offset 100:
Trova l'indirizzo di offset della fine del file ( $ ) e verifica che il registro CX sia impostato con il numero di byte. Con il comando name ( n ), inserisci il nuovo nome del file con la nuova estensione .COM ( TEXT.COM ).
Inserisci il comando w per scrivere il nuovo file su disco.
Chiudi debug e scrivi il nome del file COM che hai appena assemblato. Se il programma funziona, il file verrà stampato a video. Come assemblare un programma COM Dopo aver convertito il flusso delle istruzioni del tuo programma in codice scritto su file, puoi iniziare il processo di assemblaggio utilizzando Debug come assembler. Al prompt di debug ( - ), inserisci il comando:
Sulla tua sinistra, vedrai il valore della locazione di memoria, espresso con la coppia: segment-offset. Si parte sempre dall'offset 100 perchè i primi 100 byte sono riservati al Program Segment Prefix ( 0-FF ). Se alla locazione di memoria con offset 100 non c'è alcuna istruzione valida, il programma bloccherà il computer, mandandolo in crash, obbligandoti a riavviare il sistema. Segui questi passi per assemblare un programma che stamperà a video il codice ASCII in una modalità differente.
Quando si trova in modalità assemble ( A ), debug calcola automaticamente l'offset per l'istruzione successiva. Una volta inserita la prima istruzione all'offset 100, debug mostra l'offset successivo disponibile per l'istruzione successiva. In questo programma, l'istruzione MOV CX,7D0 richiede 3 byte di memoria, a partire dall'offset 0100 fino all'offset 0102. L'istruzione successiva, quindi, ( MOV AH,09 ) partirà dall'offset 0103. L'ultima istruzione del programma deve essere INT 20, per chiudere correttamente il programma. Si tratta di un'istruzione a due byte, allocata, nel nostro esempio, all'offset 0111. Si può evincere l'utilizzo di due byte osservando l'offset successivo: 0113. Per salvare il programma su disco, occorre nominarlo:
impostare la grandezza del file nel registro CX e lanciare il comando Write ( W ).
In quest'ultimo esempio, abbiamo inserito il nome del file con il percorso completo, in modo da salvare il file in una directory specifica del disco ( A:\ ). La dimensione del file è stata calcolata sottraendo 100 ( l'offset di inizio del programma ) da 113 ( il byte successivo all'ultima istruzione ). Se tutto è corretto, possiamo chiudere debug ( q ) e lanciare il programma dal prompt di DOS, dopo aver controllato la corretta creazione del file:
Il file deve essere di 19 byte ( in notazione decimale ) o 13 byte ( in notazione esadecimale ). Per lanciare il programma:
Prelevare i byte di stato della tastiera L'interrupt 16, funzione 2 restituisce un byte di informazioni relative allo stato di determinati tasti della tastiera, e più precisamente: Invio, Caps lock, Num lock, Scroll lock, Alt, Ctrl, Left shift e right shift. L'interrupt indica se il tasto è attivo, impostando un bit nel registro AL. Se un bit è attivo ( uno ), il tasto è premuto; se il bit è a zero, il tasto corrispondente non è premuto. NOTA - A questo punto, la guida si interrompe, con il testo: Still under construction ( ancora in costruzione ). FINE NOTA |
|
DEBUG / ASSEMBLY TUTORIAL | Disclaimer: this link points to content provided by other sites. |