2. Utilisation des ports d'entrées / sorties en langage C

2.1. La méthode normale

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é.

2.1.1. Les permissions

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.

2.1.2. L'accès aux ports

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.

2.2. Une méthode alternative : /dev/port

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.