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: 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:

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: 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