Avanti Indietro Indice

2. Usare le porte I/O nei programmi C

2.1 Il metodo normale

Le routine per accedere alle porte I/O sono in /usr/include/asm/io.h (o linux/include/asm-i386/io.h nella distribuzione del sorgente del kernel). Tali routine sono delle macro inline, quindi è sufficiente usare #include <asm/io.h>; non serve nessuna libreria aggiuntiva.

A causa di una limitazione in gcc (presente in tutte le versioni di cui sono al corrente, compreso egcs), bisogna compilare tutti i sorgenti che usano tali routine con l'ottimizzazione abilitata (gcc -O1 o maggiore), oppure usare #define extern static prima di #include <asm/io.h> (ricordarsi poi di mettere #undef extern).

Per il debugging si può usare gcc -g -O (almeno con le ultime versioni di gcc), sebbene l'ottimizzazione possa causare, a volte, un comportamento un pò strano del debugger. È possibile evitare tale inconveniente mettendo le routine che accedono alla porta I/O in un file sorgente separato e compilare solo quest'ultimo con l'ottimizzazione abilitata.

Permessi

Prima di accedere ad una qualsiasi porta, bisogna dare al programma il permesso per farlo. Ciò si fa chiamando la funzione ioperm() (dichiarata in unistd.h e definita nel kernel) da qualche parte all'inizio del programma (prima di qualunque accesso ad una porta I/O). La sintassi è ioperm(from, num, turn_on), dove from è il primo numero di porta e num il numero di porte consecutive a cui dare l'accesso. Per esempio, ioperm(0x300, 5, 1) dà l'accesso alle porte da 0x300 a 0x304 (per un totale di 5 porte). L'ultimo argomento è un valore booleano che specifica se dare (true (1)) o togliere (false (0)) al programma l'accesso alle porte. È possibile chiamare più volte ioperm() per abilitare più porte non consecutive. Vedere la pagina di man di ioperm(2) per i dettagli sulla sintassi.

La chiamata ioperm() necessita che il programma abbia i privilegi di root; quindi bisogna eseguirlo da utente root, oppure renderlo suid root. Dopo la chiamata ioperm per abilitare le porte che si vogliono usare, si può rinunciare ai privilegi di root. Alla fine del programma non è necessario abbandonare esplicitamente i privilegi di accesso alle porte con ioperm(..., 0), ciò verrà fatto automaticamente quando il processo termina.

Un setuid() ad un utente non root non disabilita l'accesso alla porta fornito da ioperm(), mentre un fork() lo disabilita (il processo figlio non ottiene l'accesso, il genitore invece lo mantiene).

ioperm() può fornire l'accesso solo alle porte da 0x000 a 0x3ff; per porte più alte bisogna usare iopl() (che, con una sola chiamata, fornisce l'accesso a tutte le porte). Per fornire al programma l'accesso a tutte le porte I/O usare 3 come argomento di livello (cioè iopl(3)) (quindi attenzione: accedere alle porte sbagliate può provocare un sacco di sgradevoli cose al computer). Di nuovo, per effettuare la chiamata a iopl() sono necessari i privilegi di root. Per maggiori dettagli vedere le pagine di man di iopl(2).

Accedere alle porte

Per leggere (input) un byte (8 bit) da una porta, chiamare inb(port), che restituisce il byte presente all'ingresso. Per scrivere (output) un byte, chiamare outb(valore, porta) (notare l'ordine dei parametri). Per leggere una word (16 bit) dalle porte x e x+1 (un byte da ognuna per formare una word con l'istruzione assembler inw) chiamare inw(x). Per scrivere una word sulle due porte si usa outw(valore, x). Se si hanno dubbi su quali istruzioni (byte o word) usare per le porte, probabilmente servono inb() e outb() --- la maggior parte dei dispositivi sono progettati per gestire l'accesso alle porte a livello di byte. Notare che tutte le istruzioni per accedere alle porte richiedono almeno un microsecondo (circa) per essere eseguite.

Le macro inb_p(), outb_p(), inw_p() e outw_p() funzionano esattamente come le precedenti, ma introducono un ritardo, di circa un microsecondo, dopo l'accesso alla porta. Si può allungare tale ritardo a circa quattro microsecondi mettendo #define REALLY_SLOW_IO prima di #include <asm/io.h>. Queste macro, di solito (a meno non si usi #define SLOW_IO_BY_JUMPING, che probabilmente è meno preciso), effettuano una scrittura sulla porta 0x80 per ottenere il ritardo e quindi, prima di usarle, bisogna dargli l'accesso alla porta 0x80 con ioperm() (le scritture fatte sulla porta 0x80 non hanno conseguenze su nessuna parte del sistema). Più avanti è spiegato come ottenere dei ritardi con sistemi più versatili.

Nelle raccolte di pagine di man per Linux, nelle versioni ragionevolmente recenti, ci sono le pagine di man di ioperm(2), iopl(2) e per le suddette macro.

2.2 Un altro metodo: /dev/port

Un altro modo per accedere alle porte I/O è quello di aprire, in lettura e/o scrittura, con open(), il dispositivo a caratteri /dev/port (numero primario 1, secondario 4) (le funzioni stdio f*() hanno un buffer interno e sono quindi da evitare). Poi posizionarsi con lseek() sul byte appropriato nel file (posizione 0 del file = porta 0x00, posizione 1 del file = porta 0x01 e così via...) e leggere (read()), o scrivere (write()), un byte o una word da, o in, esso.

Ovviamente, perché ciò funzioni, il programma avrà bisogno dell'accesso in lettura/scrittura a /dev/port. Questo metodo è probabilmente più lento del metodo normale precedentemente descritto, ma non ha bisogno né di ottimizzazioni in compilazione né della funzione ioperm(). Non serve nemmeno l'accesso da root, se si fornisce al gruppo o agli utenti non root l'accesso a /dev/port -- ma, in termini di sicurezza del sistema, far questo è una pessima idea perché è possibile danneggiare il sistema, e forse anche ottenere l'accesso a root, usando /dev/port per accedere direttamente agli hard disk, alle schede di rete, ecc.

Non è possibile usare select(2) oppure poll(2) per leggere /dev/port, perché l'hardware non ha la capacità di notificare alla CPU il cambiamento del valore in una porta d'ingresso.


Avanti Indietro Indice