Les routines pour accéder aux ports d'entrées / sorties sont situées dans le fichier d'en-tête /usr/include/asm/io.h (ou linux/include/asm-i386/io.h dans les sources du noyau Linux). Ces routines sont des macros, il suffit donc de déclarer #include <asm/io.h>; dans votre code source sans avoir besoin de bibliothèques additionnelles.
À cause d'une limitation de gcc (présente dans toutes les versions que je connais, egcs y compris) vous devez compiler le code source qui fait appel à ces routines avec le drapeau d'optimisation (gcc -O1 ou plus), ou alternativement en déclarant #define extern static avant la ligne #include <asm/io.h> (n'oubliez pas de rajouter ensuite #undef extern).
Pour le débogage, vous pouvez compiler avec les drapeaux suivants (tout du moins avec les versions les plus récentes de gcc) : gcc -g -O. Il faut savoir que l'optimisation engendre un comportement parfois bizarre de la part du débogueur. Si cela vous pose un réel problème, vous pouvez toujours utiliser les routines d'accès aux ports d'entrées / sorties dans un fichier source séparé, et ne compiler que ce fichier avec le drapeau d'optimisation activé.
Avant d'accéder aux ports, vous devez donner à votre
programme la permission de le faire. Pour cela, il vous faut
faire appel à la fonction ioperm()
(déclarée dans unistd.h et définie dans le
noyau) quelque part au début de votre programme (avant tout
accès aux ports d'entrées / sorties). La syntaxe est la
suivante :
ioperm(premier_port,
nombre,
activer)
, où
premier_port est le numéro
du premier port auquel on souhaite avoir accès et
nombre le nombre de ports
consécutifs auxquels on veut avoir la permission d'accéder.
Par exemple, ioperm(0x300, 5, 1)
donnerait
accès aux ports 0x300 jusqu'à
0x304 (au total 5 ports). Le
dernier argument est une valeur booléenne spécifiant si on
autorise l'accès aux ports (vrai [1]) ou si on le
restreint (faux [0]). Pour activer l'accès à plusieurs
ports non consécutifs, vous pouvez faire plusieurs appels à
ioperm()
. Reportez vous à la page de manuel
ioperm(2) pour plus de détails sur la syntaxe.
L'appel à ioperm()
dans votre
programme nécessite les privilèges de
super utilisateur (root). Il faut donc que votre programme
soit exécuté en tant qu'utilisateur root, ou qu'il soit rendu
setuid root. Vous pouvez abandonner les privilèges
d'utilisateur root après l'appel à
ioperm()
. Il n'est pas impératif
d'abandonner de façon explicite les privilèges d'accès aux
ports en utilisant ioperm( ... , 0 )
à la fin de votre programme, ceci est fait automatiquement
lorsque le processus se termine.
L'utilisation de setuid()
par un
utilisateur non privilégié ne supprime pas l'accès accordé aux
ports par ioperm()
. En revanche, lors d'un
fork()
, le processus fils n'hérite pas des
permissions de son père (qui lui les garde).
La fonction ioperm()
permet de contrôler
l'accès aux ports de 0x000 à
0x3ff uniquement. Pour les ports
supérieurs, vous devez utiliser iopl()
qui ouvre un accès à tous les ports d'un coup. Pour donner à
votre programme l'accès à tous les ports
d'entrées / sorties (soyez certains de ce que vous faites
car l'accès à des ports inappropriés peut avoir des
conséquences désastreuses pour votre système), il suffit de
passer à la fonction un argument de valeur
3 (iopl(3)
).
Reportez-vous à la page de manuel
iopl(2) pour plus de détails.
Pour lire un octet (8 bits) sur un port, un appel à
inb(port)
retourne la valeur de l'octet lu. Pour l'écriture d'un octet,
il suffit d'appeler la fonction
outb(valeur,
port)
(attention à
l'ordre des paramètres).
La lecture d'un mot (16 bits) sur les ports
x et
x+1 (un octet
sur chaque port pour constituer un mot grâce à l'instruction
assembleur inw
), faites appel à
inw(x)
. Enfin,
pour l'écriture d'un mot sur les deux ports, utilisez
outw(value,
x)
.
Si vous n'êtes pas certain quant à la fonction à utiliser
(octet ou mot), il est sage de se cantonner à l'appel de
inb()
et outb()
. La
plupart des périphériques sont conçus pour des accès sur un
octet. Notez que toutes les instructions d'accès aux ports
nécessitent un temps d'exécution d'au minimum une
microseconde.
Les macros inb_p()
,
outb_p()
,
inw_p()
et
outw_p()
fonctionnent de manière
identique à celles évoquées précédemment à l'exception du fait
qu'elles effectuent un court temps de pause additionnel après
l'accès au port (environ une microseconde). Vous avez la
possibilité d'allonger ce temps de pause à quatre
microsecondes avec la directive #define
REALLY_SLOW_IO avant de déclarer
#include <asm/io.h>. Ces macros
utilisent normalement une écriture sur le port
0x80 pour leur temps de pause (sauf en
déclarant un #define
SLOW_IO_BY_JUMPING, qui est en
revanche moins précis). Vous devez donc au préalable autoriser
l'accès au port 0x80 avec
ioperm()
(l'écriture sur le port
0x80 ne devrait avoir aucun effet
indésirable sur votre système).
Si vous êtes à la recherche de méthodes plus souples d'utilisation, lisez la suite …
Des pages de manuel pour ioperm(2), iopl(2) et les macros décrites ci-dessus sont disponibles dans les collections assez récentes des pages de manuel Linux.
Un autre moyen d'accéder aux ports d'entrées / sorties est
d'ouvrir en lecture ou en écriture le périphérique /dev/port (un périphérique en mode
caractère, numéro majeur 1, mineur
4) au moyen de la fonction
open()
. Notons que les fonctions en
f*()
de la bibliothèque stdio font appel à
des tampons mémoires internes, il vaut donc mieux les éviter. Il
suffit ensuite, comme dans le cas d'un fichier, de se
positionner sur l'octet approprié au moyen de la fonction
lseek()
(l'octet 0 du
fichier équivaut au port 0x00, l'octet
1 au port 0x01, et cætera)
et d'en lire (read()
) ou écrire
(write()
) un octet ou un mot.
Il est évident que l'application doit avoir la permission
d'accéder au périphérique /dev/port pour que cette méthode
fonctionne. Cette façon de faire reste certainement plus lente
que la première, mais elle ne nécessite ni optimisation lors de
la compilation ni appel à ioperm()
. L'accès
aux privilèges de super-utilisateur n'est pas impératif non
plus, si vous donnez les permissions adéquates à un utilisateur
ou un groupe pour accéder à /dev/port (cela reste tout de même
une très mauvaise idée du point de vue de la sécurité du
système, puisqu'il devient possible de porter atteinte au
système, peut-être même d'obtenir le statut de root en utilisant
/dev/port pour accéder
directement aux disques durs, cartes réseaux, et cætera).
Il n'est pas possible d'utiliser les fonctions select(2) ou poll(2) pour lire /dev/port puisque l'électronique du système n'a pas la possibilité d'avertir le microprocesseur qu'une valeur a changé sur un port d'entrée.