Assembly Avanzato con NASM
Capitolo 12: Servizi DOS per l'I/O su file
In questo capitolo analizzeremo brevemente i principali servizi offerti dal DOS per le
operazioni di I/O su file; come spesso accade, tali servizi vengono ottenuti attraverso
una chiamata alla INT 21h.
Naturalmente, le macchine virtuali (come VirtualBox) e gli emulatori DOS
forniscono i servizi per i file sfruttando quelli del SO (Windows, Linux,
etc) sotto il quale stanno girando.
12.1 Il filesystem
La possibilità per i programmi di accedere in I/O alle memorie di massa (hard disk,
floppy disk, pen drive, memory card, CD, DVD, etc) è legata alla presenza su tali supporti
di un cosiddetto filesystem; grazie al filesystem il SO fornisce ai programmi
una serie di procedure standard per le operazioni di creazione, lettura, scrittura, modifica,
cancellazione di file.
In assenza di un filesystem, l'accesso in I/O alle memorie di massa comporta il ricorso
a tecniche per la gestione a basso livello dei dispositivi; tali tecniche vengono utilizzate,
ad esempio, dai programmi diagnostici, dai programmi per il recupero di file danneggiati, etc.
Il filesystem del DOS (ma anche di altri SO) distingue innanzi tutto tra dispositivi
a caratteri (character devices) e dispositivi a blocchi (block devices).
I dispositivi a caratteri sono periferiche identificate attraverso un nome riservato come, ad
esempio, CON: (l'insieme della tastiera e del monitor), LPT1: (la stampante
collegata alla prima porta parallela), etc; attraverso questi nomi è possibile svolgere operazioni
dirette di I/O senza la necessità di dover indicare percorsi specifici.
I dispositivi a blocchi sono periferiche (generalmente, memorie di massa) identificate attraverso
una lettera dell'alfabeto (A:, B:, C:, etc) che rappresenta la cosiddetta
radice del dispositivo stesso; a partire dalla radice è possibile accedere in I/O
ai dati che si trovano memorizzati nella periferica sotto forma di file.
I file possono trovarsi direttamente nella radice o anche raggruppati in appositi contenitori
denominati directory; a loro volta, le directory possono contenere anche altre directory
denominate subdirectory.
Nei dispositivi a blocchi le directory assumono quindi una caratteristica struttura ad albero;
oltre alla radice, le directory e le subdirectory rappresentano i rami, mentre i file
rappresentano le foglie.
I file e le directory vengono identificati attraverso un nome lungo al massimo 8 caratteri
e una estensione opzionale lunga al massimo 3 caratteri (ad esempio, MIOFILE.TXT);
il nome e l'eventuale estensione devono essere separati da un punto ('.').
In base alla struttura ad albero appena descritta, per specificare in modo completo e univoco un
determinato file, è necessario indicare il cosiddetto percorso (path) da seguire lungo
l'albero stesso e cioè, la sequenza dei nomi formata dalla radice, dalle eventuali directory e
subdirectory e dal file; tutti i nomi devono essere separati dal carattere '\' (backslash).
Per un ipotetico file MIOFILE.TXT possiamo avere, ad esempio:
C:\UTENTE1\ARCHIVIO\DOCUM.TXT\MIOFILE.TXT
Il DOS (così come Unix/Linux) definisce anche due nomi riservati per le directory
e cioè, '.' e '..'; il simbolo '.' indica la directory corrente
(cioè, la directory in cui ci troviamo), mentre il simbolo '..' indica la directory
superiore (cioè, la directory che contiene quella corrente).
12.2 Le handle
Le versioni meno vecchie del DOS hanno introdotto il concetto di handle
(maniglia) per la gestione dei file; ogni volta che un programma chiede al DOS
l'autorizzazione per l'accesso in I/O ad un file, riceve dal SO una handle che
identifica in modo univoco il file stesso.
L'uso delle handle è molto comodo in quanto permette di delegare al DOS tutte le
operazioni preliminari (allocazione della memoria per contenere le strutture dati destinate al
controllo del file); nelle prime versioni del DOS si utilizzavano i FCB (File
Control Blocks) e tutte le operazioni preliminari erano a carico del programma che intendeva
accedere al filesystem.
Grazie alle handle, il DOS può effettuare tutti i necessari controlli di sicurezza sui
file in modo da evitare che vengano compiute operazioni non lecite; lo stesso DOS può,
ad esempio, chiudere tutti i file lasciati aperti da un programma che ha terminato l'esecuzione
in modo normale o forzato (ad esempio, per un crash).
12.3 Servizi DOS per la gestione del filesystem
Tutti i servizi DOS per la gestione del filesystem vengono forniti dalla INT 21h;
il codice principale del servizio viene passato attraverso il registro AH. La tipica
chiamata di un servizio DOS per i file assume quindi il seguente aspetto:
In caso di successo, si ottiene CF=0, mentre in caso di insuccesso si ottiene CF=1;
se CF=1, il registro AX contiene uno dei codici di errore illustrati dalla Figura
12.1.
Analizziamo ora l'elenco completo dei servizi che presuppongono il riferimento ai file tramite
il metodo delle handle.
12.3.1 Servizio n. 39h: Create Subdirectory (MKDIR)
Questo servizio permette di creare una nuova subdirectory.
Questo servizio permette di creare una nuova subdirectory in una directory già esistente o
nella radice del dispositivo corrente; il nome della subdirectory deve essere contenuto in
una stringa C puntata da DS:DX.
Se il programmatore indica un singolo nome, la nuova subdirectory verrà creata nella directory
corrente; in alternativa, è possibile specificare il percorso completo.
12.3.2 Servizio n. 3Ah: Remove Subdirectory (RMDIR)
Questo servizio permette di rimuovere una subdirectory esistente.
Questo servizio permette di eliminare una subdirectory esistente con tutti gli eventuali file
in essa presenti; non è possibile eliminare una subdirectory che, al suo interno, contiene
anche altre subdirectory e non è neanche possibile eliminare la directory corrente.
Il nome della subdirectory da eliminare deve essere contenuto in una stringa C puntata
da DS:DX. Se il programmatore indica un singolo nome, verrà eliminata la relativa
subdirectory contenuta nella directory corrente; in alternativa, è possibile specificare il
percorso completo.
12.3.3 Servizio n. 3Bh: Set Current Directory (CHDIR)
Questo servizio permette di impostare la nuova directory corrente.
Questo servizio permette di entrare in una directory la quale, di conseguenza, diventerà la
nuova directory corrente; il nome della directory in cui spostarsi deve essere contenuto in
una stringa C puntata da DS:DX
Se il programmatore indica un singolo nome, sta dando per scontato che la directory in cui
spostarsi è contenuta nella directory corrente; in alternativa, è possibile specificare il
percorso completo.
12.3.4 Servizio n. 3Ch: Create or Truncate File (CREAT)
Questo servizio permette di creare un nuovo file.
Questo servizio permette di creare un nuovo file; in caso di successo, il file appena creato
viene automaticamente aperto per le operazioni di I/O a partire dalla posizione iniziale
del file stesso.
Il nome del file da creare deve essere contenuto in una stringa C puntata da
DS:DX; se il nome esiste già, il relativo file viene troncato alla dimensione di
0 byte e tutti gli eventuali dati in esso presenti vengono persi!
Se il programmatore indica un singolo nome, sta dando per scontato che il file deve essere
creato nella directory corrente; in alternativa, è possibile specificare il percorso completo.
Chiamando questo servizio, il programmatore può anche specificare gli attributi da assegnare
al file; tali attributi possono essere successivamente cambiati con il servizio 43h.
Gli attributi devono essere specificati attraverso i bit del registro CX in base a
quanto riportato in Figura 12.2; se il bit vale 0 il corrispondente attributo viene
disattivato, mentre se il bit vale 1 il corrispondente attributo viene attivato
(ovviamente, è anche possibile combinare tra loro due o più attributi).
12.3.5 Servizio n. 3Dh: Open Existing File (OPEN)
Questo servizio permette di aprire un file esistente.
Questo servizio permette di aprire un file che deve essere già presente nel filesystem; in
caso di successo, il file appena aperto risulta disponibile per le operazioni di I/O
a partire dalla posizione iniziale del file stesso e secondo la modalità di accesso
specificata attraverso il registro AL.
Il nome del file da aprire deve essere contenuto in una stringa C puntata da
DS:DX; il programmatore può indicare un singolo nome (file relativo alla directory
corrente) o, in alternativa, il percorso completo.
Chiamando questo servizio, il programmatore può anche specificare la modalità con la quale
avverrà l'accesso al file; a tale proposito, è necessario utilizzare il registro AL i
cui bit assumono il significato mostrato in Figura 12.3.
I bit in posizione 0, 1, 2 possono assumere i soli valori:
- 000b - accesso in sola lettura
- 001b - accesso in sola scrittura
- 010b - accesso in lettura e scrittura
Il bit in posizione 3 è riservato e deve valere 0.
I bit in posizione 4, 5, 6 esprimono le modalità con le quali il file può
essere condiviso tra più applicazioni e/o tra più computer connessi in rete; i soli valori
possibili sono:
- 000b (COMPATIBLE) - il file è accessibile da qualunque applicazione
purché residente sul computer in cui si trova il file stesso
- 001b (DENYALL) - l'accesso in lettura e scrittura è permesso solamente
all'applicazione che ha aperto il file
- 010b (DENYWRITE) - l'accesso in scrittura è permesso solamente
all'applicazione che ha aperto il file
- 011b (DENYREAD) - l'accesso in lettura è permesso solamente
all'applicazione che ha aperto il file
- 100b (DENYNONE) - nessun divieto
12.3.6 Servizio n. 3Eh: Close File (CLOSE)
Questo servizio permette di chiudere un file già aperto in precedenza.
Questo servizio permette di chiudere un file precedentemente aperto da una applicazione; si
tratta di un servizio molto importante in quanto la sua chiamata provoca la scrittura nel file
di eventuali dati ancora nel buffer e la restituzione della handle al SO.
12.3.7 Servizio n. 3Fh: Read from File or Device (READ)
Questo servizio permette di leggere da un file già aperto in precedenza.
Questo servizio permette di leggere dati da un file precedentemente aperto da una applicazione;
la lettura inizia a partire dalla posizione corrente nel file stesso (se si desidera leggere da
una posizione differente si veda più avanti il servizio 42h).
Il numero di byte effettivamente letti, restituito in AX, potrebbe essere differente
dal numero di byte richiesto (ad esempio, dalla console si possono leggere al massimo
128 caratteri per riga); se in AX viene restituito il valore 0 significa
che in fase di lettura è stata raggiunta la fine del file (EOF o End Of File).
Tutti i dati letti dal file vengono copiati in un buffer puntato da DS:DX; la memoria
per tale buffer deve essere allocata dal programmatore.
12.3.8 Servizio n. 40h: Write to File or Device (WRITE)
Questo servizio permette di scrivere in un file già aperto in precedenza.
Questo servizio permette di scrivere dati in un file precedentemente aperto da una applicazione;
la scrittura inizia a partire dalla posizione corrente nel file stesso (se si desidera scrivere
in una posizione differente si veda più avanti il servizio 42h).
Tutti i dati da scrivere nel file vengono copiati da un buffer puntato da DS:DX;
ovviamente, tale buffer deve essere appositamente predisposto dal programmatore.
12.3.9 Servizio n. 41h: Delete File (UNLINK)
Questo servizio permette di cancellare un file esistente.
Questo servizio permette di rimuovere fisicamente un file esistente; è importante
assicurarsi che il file venga chiuso prima della sua cancellazione.
Il nome del file da cancellare deve essere contenuto in una stringa C puntata da
DS:DX; il programmatore può indicare un singolo nome (file relativo alla directory
corrente) o, in alternativa, il percorso completo.
12.3.10 Servizio n. 42h: Set Current File Position (SEEK)
Questo servizio permette di spostare il puntatore di I/O all'interno di un file esistente.
Questo servizio permette di spostare il puntatore che indica la posizione corrente nel file;
come è stato spiegato in precedenza, tutte le operazioni di I/O relative ad un file
si svolgono a partire dalla posizione corrente.
Il modo di posizionamento deve essere specificato nel registro AL; gli unici
tre valori permessi sono:
- 00h - incremento puntatore a partire dall'inizio del file
- 01h - incremento puntatore a partire dalla posizione corrente
- 02h - incremento puntatore a partire dalla fine del file
L'incremento da effettuare deve essere specificato nella coppia CX:DX; si tratta di un
intero con segno a 32 bit che ci permette di esprimere un incremento compreso tra
-2147483648 e +2147483647.
Se l'operazione ha successo, la nuova posizione viene restituita nella coppia DX:AX;
si tratta di un numero intero senza segno che indica la posizione corrente a partire
dall'inizio del file.
Sfruttando un semplice espediente, possiamo usare questo servizio per ottenere in
DX:AX la lunghezza in byte di un file; a tale proposito, basta porre AL=02h
(posizionamento relativo alla fine del file) e CX:DX=00000000h (incremento nullo).
12.3.11 Servizio n. 4300h: Get File Attributes (ATTRIB)
Questo servizio permette di conoscere gli attributi assegnati ad un file esistente.
Questo servizio permette di conoscere gli attributi che caratterizzano un file esistente; in
caso di successo, le informazioni richieste vengono restituite nel registro CX i cui
bit assumono lo stesso significato già illustrato in Figura 12.2.
Il nome del file che ci interessa deve essere contenuto in una stringa C puntata da
DS:DX; il programmatore può indicare un singolo nome (file relativo alla directory
corrente) o, in alternativa, il percorso completo.
12.3.12 Servizio n. 4301h: Set File Attributes (CHMOD)
Questo servizio permette di modificare gli attributi assegnati ad un file esistente.
Questo servizio permette di modificare gli attributi precedentemente assegnati ad un file
esistente; i nuovi attributi devono essere specificati nel registro CX secondo lo
schema già illustrato in Figura 12.2.
Il nome del file che ci interessa deve essere contenuto in una stringa C puntata da
DS:DX; il programmatore può indicare un singolo nome (file relativo alla directory
corrente) o, in alternativa, il percorso completo.
12.3.13 Servizio n. 56h: Rename/Move File or Directory (RENAME)
Questo servizio permette di cambiare il nome di un file o di una directory; lo stesso
servizio può essere usato per spostare un file da una directory ad un'altra.
Lo scopo principale di questo servizio è la modifica del nome di un file o di una directory.
Il vecchio nome del file/directory da modificare deve essere contenuto in una stringa C
puntata da DS:DX e non può avere caratteri jolly (* e ?); il nuovo
nome del file/directory da modificare deve essere contenuto in una stringa C puntata
da ES:DI e può avere caratteri jolly.
Il programmatore può indicare un singolo nome (file/directory relativo alla directory corrente)
o, in alternativa, il percorso completo.
Il servizio 56h può essere usato anche per spostare un file da una directory ad un'altra;
a tale proposito, basta indicare nel nuovo nome un percorso differente da quello vecchio. Non è
consentito usare questo servizio per spostare una directory.
12.4 Libreria FILELIB
In analogia a quanto visto nei precedenti capitoli, anche per la gestione dell'I/O su
file conviene scriversi una apposita libreria linkabile a tutti i programmi che ne hanno
bisogno; nella sezione Librerie di supporto per il corso assembly dell’
Area Downloads di questo sito,
è presente una libreria, denominata FILELIB, che può essere linkata
ai programmi destinati alla generazione di eseguibili in formato EXE.
All'interno del pacchetto filelibexe.zip è presente la documentazione, la libreria
vera e propria FILELIB.ASM, l'include file FILELIBN.INC per il NASM,
l'include file FILELIBM.INC per il MASM e l'header file FILELIB.H per
eventuali programmi scritti in linguaggio C (purché destinati sempre alla modalità reale).
Per la creazione dell'object file di FILELIB.ASM è richiesta la presenza della
libreria di macro LIBPROC.MAC (Capitolo 29 della sezione Assembly Base con NASM);
l'assemblaggio consiste nel semplice comando:
nasm -f obj filelib.asm
L'object file così ottenuto è perfettamente linkabile anche ai programmi
scritti con MASM; a tale proposito, è necessario ricordarsi di chiamare le varie
procedure della libreria secondo le convenzioni C (passaggio dei parametri da
destra verso sinistra e pulizia dello stack a carico del caller).
La possibilità di linkare la libreria ai programmi scritti in C è legata al fatto che
le varie procedure definite in FILELIB seguono le convenzioni C per il passaggio
degli argomenti e per il valore di ritorno; da notare, inoltre, la presenza degli underscore
all'inizio di ogni identificatore globale definito nella libreria stessa.
Diverse procedure della libreria richiedono argomenti passati per indirizzo; tale indirizzo
deve essere sempre di tipo FAR!
Ricordando le convenzioni C per il passaggio degli argomenti (da destra verso sinistra),
dobbiamo stare attenti quindi a mettere nella lista di chiamata, prima la componente
Offset e poi la componente Seg della variabile da passare per indirizzo; in
questo modo, la componente Offset si troverà a precedere la componente Seg in
memoria, nel rispetto della convenzione Intel (con il Pascal avremmo dovuto
disporre le due componenti in ordine inverso).
All'interno delle procedure, le variabili passate per indirizzo vengono gestite con le coppie
DS:SI e ES:DI inizializzate con le istruzioni LDS e LES; come
sappiamo, tali istruzioni lavorano in modo corretto solo quando la coppia Seg:Offset,
da caricare in DS:SI o ES:DI, si trova disposta in memoria (in questo caso,
nello stack) secondo la convenzione Intel!
12.5 Esempi pratici
Vediamo un semplice esempio pratico rappresentato da un programma che crea un nuovo file nella
directory corrente, lo riempie con 80x24 lettere 'X' in verde chiaro su sfondo
blu scuro e, infine, trasferisce tutti questi dati nella memoria video in modo testo; la
Figura 12.4 mostra il relativo listato.
Prima di tutto, il programma libera tutta la memoria convenzionale in eccesso; osservando il
map file possiamo notare che il programma stesso ha bisogno di circa 370
paragrafi i quali, per sicurezza, vengono estesi a 400.
Lo scopo del passaggio appena descritto è legato al fatto che, invece di scrivere i singoli
byte direttamente nel file, utilizziamo un apposito buffer di memoria (allocato attraverso il
servizio 48h della INT 21h) in modo da velocizzare notevolmente le operazioni;
anche la fase di lettura del file viene svolta in modo molto efficiente grazie al fatto che
tutte le informazioni vengono copiate in un colpo solo in memoria video.
Le dimensioni del buffer sono pari a 80x24x2=3840 byte (240 paragrafi) in quanto,
come sappiamo, la memoria video in modo testo assegna ad ogni cella 1 byte per il codice
ASCII del carattere da stampare e 1
byte per gli attributi video; nel nostro esempio, il carattere da stampare è 58h='X',
mentre gli attributi video sono rappresentati dal valore 1Ah (background blu scuro e
foreground verde chiaro).
Una volta creato il buffer di memoria, lo riempiamo con tutti i 3840 byte di informazioni;
in seguito trasferiamo in un colpo solo tutti questi 3840 byte in un file
FILETEST.DAT appositamente creato con la procedura _create_file della libreria
FILELIB.
Subito dopo la scrittura, dobbiamo tenere presente che il puntatore al file si trova posizionato
all'offset 3840 di FILETEST.DAT; proprio per questo motivo, prima di effettuare
l'operazione di lettura, dobbiamo utilizzare la procedura _seek_file per riposizionarci
all'inizio dello stesso FILETEST.DAT.
La procedura _read_file copia in un colpo solo tutti i 3840 byte, dalla sorgente
FILETEST.DAT, alla destinazione B800h:0000h (indirizzo iniziale della VRAM
in modo testo); se tutto si è svolto correttamente, vedremo sullo schermo 1920 lettere
'X' in verde chiaro su sfondo blu scuro.
Si osservi che il file FILETEST.DAT viene trattato implicitamente come binario per
cui, aprendolo con un editor di testo, si vedrebbero delle 'X' intervallate con dei
simboli strani dovuti al fatto che il valore 1Ah viene erroneamente interpretato come
codice del carattere di controllo (non stampabile) SUB!
Un altro aspetto importante è dato dal fatto che i servizi legati alle procedure _read_file
e _write_file possono anche restituire CF=0 nonostante un numero di byte, letti o
scritti, diverso da quello richiesto dal programmatore; proprio per questo motivo, è importante
controllare sempre il valore restituito in AX anche quando si ottiene CF=0!
12.5.1 Versione C di FILETEST.ASM
Come è stato spiegato in precedenza, la libreria FILELIB può essere interfacciata anche
con programmi scritti in C per la modalità reale; la Figura 12.5 illustra una versione
C semplificata di FILETEST.ASM.
In realtà FILELIB segue le convenzioni C solo per comodità, ma è concepita per
essere interfacciata principalmente con programmi in Assembly (i quali, ad esempio,
possono facilmente controllare lo stato di CF dopo la chiamata delle varie procedure);
il C dispone di proprie librerie per l'I/O su file e si consiglia vivamente di
utilizzare quelle!
Bibliografia
Cristopher, Feigenbaum, Saliga - MS-DOS MANUALE DI PROGRAMMAZIONE - Mc Graw Hill