2. C プログラムから I/O ポートを使う

2.1. 普通の方法

I/O ポートをアクセスするためのルーチンは /usr/include/asm/io.h (またはカーネルのソースパッケージの中の linux/include/asm-i386/io.h) に定義されています。必要なルーチンは、 この中でインラインマクロとして定義されていま すので、#include <asm/io.h> とするだけで十分でしょう。特別なライブラリなどは不要です。

gcc (私の知る限り全てのバージョン、egcs も含む) からの制限のため、 これらのルーチンを使うソースをコンパイルするにあたり、最適化をオン にする( gcc -O1 またはそれ以上)、あるい は #include <asm/io.h> の前に #define extern static という宣言を おこなっておく必要があります。 (あとで #undef extern しておくのを 忘れないように。)

デバッグのために、(少なくとも最近の gcc では) gcc -g -Oを使うことができますが、 最適化されたコードではデ バッガが変な挙動を示すこともあります。このような場合には、I/O ポート アクセスを含んだコードを独立したファイルにしておいて、そのファイルの コンパイルの時にだけ最適化をオンにするという方法を使うこともできます。

2.1.1. パーミッション

ポートをアクセスする前に、そのポートをアクセスする許可をプログラムに与え なければなりません。これには、ioperm() 関数 (unistd.h で宣言され、カーネルの中で 定義されている)を呼び出します。この関数はプログラムの最初の方で( I/O ポート アクセスをする前に)呼び出す必要があります。 この関数の書式は ioperm(from, num, turn_on)です。 fromはアクセスを許すポートの最初のアドレス、 numfromから いくつの連続アドレスのアクセスを許すか、を指定します。 例えば、ioperm(0x300, 5, 1) は、0x300 から 0x304 (合計で 5 つのポート)に対する許可を指定し、最後のパラメータでは、 そのポートに対するアクセス許可を与えるのか禁止するのかを論理値(true : 1 = アクセスを許可する, false : 0 = アクセスを禁止する)で指定します。 連続していないポートの場合には、複数回ioperm() を呼び出して、適切な許可を指定します。 文法の詳細に関しては ioperm(2) のマニュアル ページを参照してください。

ioperm() を呼び出すには、そのプログラムが root 特権で走っていることが必要です。 つまり、そのプログラムをルートユーザで実行するか、setuid root (訳注: 実 行ファイルのオーナーを root にしておき、chmod 4755 hogehoge というやつ です。)しておく必要があります。 必要なポートに対する許可を ioperm() で与え たあとに、root 特権を落すこともできます。 プログラムの最後で、明示的に ioperm(..., 0) を呼び出して許可を落す必要はありません。 これはプログラムの終了時に自動的に行なわれますから。

root 以外のユーザに setuid() することで ioperm() の与えたポートアクセス許可が なくなることはありませんが、一方で fork() すると許可がなくなります。 (子プロセスはアクセス許可を持っていませんが、親プロセスは持っています。)

ioperm() では、0x000 から 0x3ff までの ポートに対するアクセス許可しか与えることはできません。 これよりも上のポートに対しては、iopl() を使う必要があります。 (iopl() を使うと一度にすべてのポートに対 するアクセス許可を与えることになります。) すべてのポートに対してアクセス許可を与えるには、 レベルパラメータとして「3」を使います。つまり、iopl(3) という関数呼び出しをします。 (間違ったポートにアクセスするととんでもないことが起きるかも知れません から、使うときには注意してください。) もちろん、iopl() を呼び出す時には root 特権が必要です。 詳細に関しては iopl(2) のマニュアルページを 参照してください。

2.1.2. ポートのアクセス

ポートからバイトデータ(8bit)を入力する場合には inb(port) を呼び出します。 この関数はバイトデータを返します。 ポートにバイトデータを出力するには、outb(value, port) を呼び出します。 (パラメータの順番に注意して下さい。) (訳注:アセンブラや、MS-DOS のコンパイラライブラリなどの場合とは逆です。) ポートアドレス xx+1 からワードデータを入力する場合には inw(x) を呼び出します。(アセンブラのinw とまったく同じようにそれぞれのポートから 1 バイトずつ読んできて、ワード データを構成するわけですが。) ふたつのポートにワード( 2 バイト)を出力するには outw(value, x) を使います。 どちらの命令(バイトデータかワードデータか)を使えばいいのか分からないときは、 多分、inb()outb() を使えばいいでしょう。 大抵のデバイスはバイト単位のポートアクセスをするよう設計されています。 ポートアクセスの命令はすべて実行に少なくとも約 1 マイクロ秒かかるので 注意してください。

inb_p(), outb_p(), inw_p(), outw_p() といったマクロは、上に述べたのと 同じ動作をしますが、ポートにアクセスした後に少しだけ(約 1 マイクロ秒) ウエイトします。 #include <asm/io.h> の前に #define REALLY_SLOW_IO を 付け加えるとこのウエイト時間を 4 マイクロ秒に変えることができます。 これらのマクロは通常、0x80 番地の I/O ポートに出力することでウエイトを 実現しています。 (#define SLOW_IO_BY_JUMPING としている場合には別の方法が使われますが、これはあまり正確な遅延 をおこなうことができないはずです。) このような理由から、0x80 番地のポートに対する ioperm() を先に実行しておく必要が あります。(0x80 番地のポートに対する出力はシステムに対してなんら影響を 与えることはないはずです。) さらにさまざまな遅延を行なう方法については、以下を読み進めて下さい。

そこそこ最近のリリースの Linux マニュアルページには、 ioperm(2)iopl(2)、上述のマクロに関する説明があります。

2.2. I/O ポートアクセスをする別の方法:/dev/port

I/O ポートにアクセスするもう一つの方法は、open() を使って /dev/port (キャラクタデ バイス、メジャー番号 1、マイナー番号 4)をオープンして、read/write をお こなうという方法です。 (stdio の f*() 関数(訳注: fwrite() とか fread() とか...)は内 部でバッファリングをしているので、使えませんね。) オープンした後に、入出力したいポートのアドレスまで lseek() します。 (file position 0 が ポート 0x00 に、file position 1 が ポート 0x01 ... といった具合に相当します。) その後に read(), write() を使ってバイト/ワードを読み書きします。

この方法を使うには、もちろん そのプログラムが /dev/port に対する read/write 許可を持っていなければなりません。 この方法は上に述べた通常の方法よりもおそらく遅いでしょうが、コンパイル時の 最適化や ioperm() が不要であるという利点があり ます。 /dev/port に対してルート以外のユーザやグループの アクセス許可を与えれば root 特権も必要ありません。 でもこれはシステムセキュリティという意味ではとても良くないことです。 というのは、/dev/port を使ってハードディスク、 ネットワークカード、その他に直接アクセスすることで、システムに障害を与えたり、 さらには root 特権も与えてしまう可能性があるからです。

/dev/port からの読み込みにおいて select(2)poll(2) を使うことはできません。 なぜなら入力ポートの値が変化したときに、ハードウェアにはそれをCPUに 伝える機能がないからです。