デバイスドライバにありがちなi386依存の罠


このテキストはFreeBSD Press No.12 特集「BSDで動かそう 後編」のNetBSD関連記事のコラムとして掲載されたものです。Webでの公開にあたり投げやりなhtml化を含め一部の表記は見直していますが、基本的にAPI解説としては当時のソースのままであり、最新のNetBSDバージョンにおける変更には追従していません。

本テキストの著作権は筒井泉が有しています。obsoleteな不正確な情報が拡散するのもあまりよろしくないので、転載は控えて下さい。


ここではFreeBSDやLinuxのデバイスドライバでよく見かける「無意識にi386に依存してしまっている記述」についていくつか例をあげる。ドライバの作成やデバッグの際には「i386で動いてるんだからドライバは問題ないはず」という考えは捨て、最初から機種依存しないような書き方を心がける必要がある。

vtophys()

本文中で述べたとおり、MIなデバイスドライバ内で物理アドレスを得るために vtophys() を使うのは御法度である。バスマスタDMAデバイスに設定する転送アドレスが物理アドレスであるとは限らないという問題以前に、vtophys() は完全にMDな関数であり、すべてのアーキテクチャで用意されているわけではない。MIデバイスドライバ内で純粋に物理アドレスが必要となるケースはほとんどないはずであり、DMA転送アドレス設定の際には bus_dma関数を使うように記述すべきである。

バイトオーダー依存

これも別項の中で述べているが、バイトオーダー依存の問題はCPUのバイトオーダーとバスのバイトオーダーが異なる場合の問題と純粋なプログラム記述的な問題との2つに分けられる。前者の場合はデバイス特定のバイトオーダーが必要な箇所を特定して htole32()/le32toh() 等のマクロを追加するだけでよく、特にドライバの実装面での考慮はあまり必要ない[脚注1]。後者の問題も記述の問題でありドライバの本質とはあまり関係がないが、本来の型と異なるポインタでのデータアクセスや構造体中のビットフィールドの使用など、データ構造がバイトオーダー依存となる記述は物理的なハードウェアが対象であるデバイスドライバでは避けるよう普段から心がけるべきである。

BIOSによる初期化

PC用として販売されているストレージ系のPCIデバイスには、多くの場合ボード上にPC用BIOSが載っていてデバイスの初期化などを行っているが、デバイスドライバの中には初期化がBIOSによって行われることを前提としていてドライバ内で何も処理していなかったり、BIOSによって設定された各種レジスタの値を読み込んで来て別の項目の設定に使用していたりするものも存在する。しかし、たとえPC用として販売されているものであっても、ハードウェア仕様上はすべてのPCIデバイスはPCIバスを持つあらゆるマシンで使えるように作られているはずで、ボード上のBIOSが実行されるかどうかということはデバイスの仕様とは本来関係がない。したがって本当にMIなデバイスドライバとするためにはデバイスの初期化はすべてドライバが自力で行う必要がある[脚注2]

もっとも、どこをどういうふうに初期化しなければならないかということを知るためにはデバイスのデータシートが必要になるが、特にPC用として開発された多くのデバイスではロクな資料がないことも多い。そういう場合にはLinuxのドライバがソース付きで用意されていないかとか、もっと泥臭くPC上のBIOSが各種レジスタをどのような値にしているのかとかを調べなければならない場合もある。

ワードサイズとアラインメント

これらは通常アプリケーションにおいてもデータ互換性の問題としてあげられる項目である。デバイスを設定するための各種の値をメモリ上に配置する必要がある場合に、データの型として long やポインタを使用すると、CPUによってはそれが32ビットだったり64ビットだったりするために本来デバイスが要求するデータを設定することが出来ない[脚注3]。このような場合は longint などの型ではなく uint32_tuint64_t など[脚注4] のサイズの規定された型を使わなければならない。

同様に、上記のようなデータの配置に異なるデータサイズを持つメンバーが混在する構造体を用いると、各メンバー間のpaddingはCPU依存となるため問題となる場合がある。このためこれらのデータ定義の際には構造体を使わずただの配列にするとか、構造体を使う場合でも複数のサイズを混在させないとか構造体の定義で __attribute__((__patcked__)) の属性を指定してpaddingについては明示的に指定する等の処理が必要である。[脚注5]

蛇足だが、sparc64の場合はビッグエンディアンであるため上記のLP64問題はさらに複雑化する。たとえば long型として64ビット分確保されたメモリに32ビット型変数のポインタ経由でデータを書き込んでしまった場合、リトルエンディアンであるalphaの場合はLSB側からデータが置かれるため、最初の32ビット分の値は64ビット型でも同じ値になり見かけ上正しい値が書かれているように見える。しかし、ビッグエンディアンのsparc64では最初の32ビットはMSB側であり、本来LSB側32ビットに書かれるはずの値がMSB側32ビットに書かれることになり不正な値となってしまう。このようなビッグエンディアンでしか発現しないLP64問題は特にLP64BE問題とも言われる。

unaligned access

i386では奇数アドレスでの16ビットワードデータの読み込みといったunalignedなデータアクセスが許容されているが、大部分のRISC CPUではこういったの操作は許されておらずCPU例外等を引き起こす[脚注6]。Linuxの一部のドライバなどではこれらの操作を技巧的(?)に使っている場合があり注意が必要である。


[脚注1] ただしよく考えて書かないといつどこでどっちのバイトオーダーになっているのかわかんなくなってしまったりするので注意は必要。

[脚注2] ただしPCIのVGAに関してはデバイスレベルではなくボード毎に異なる初期化が必要であり、ドライバがすべてのボードに対応する一般的な初期化ルーチンを持つことはほとんど不可能である。このため、ボード上のBIOS内のi386のコードをエミュレーションするといった機能の必要性も一部で取りざたされているが、これが実現されるのはかなり先の話になるであろう。

[脚注3] alphaやsparc64では longとポインタは64ビット(int は32ビット)であり、これらの型が32ビットであることを仮定していることに起因する不具合はLP64問題といわれる。これらの問題はデバイスドライバに限らずユーザーランドプログラムでも常に付きまとう問題である。

[脚注4] これらの型は <sys/types.h> やそこからincludeされる <machine/types.h><machine/int_types.h> などで定義される。

[脚注5] この属性の指定方法は実際には処理系依存(コンパイラ依存)である。ここで挙げている指定方法はgccでの機能であり、gccのinfoファイルの中に説明がある。

[脚注6] panic でもしてくれるならまだいいが、いきなりハングアップしたりするアーキテクチャもあるので何が起きているのかわからず結構始末が悪い。


NetBSDのページに戻る
tsutsui@ceres.dti.ne.jp