公開技術情報

[English] [Japanese]

64bit 版 Raspberry Pi OS でカーネルモジュールを使った際に遭遇したエラーと対応方法

8GB 版 Raspberry Pi を購入したので、 せっかく 8GB を使うなら 32bit よりも 64bit 版の方が良いだろうと軽い気持で 64bit 版 Raspberry Pi OS をインストールしたものの、何故かエラーが発生して動かない、 ということがあったので、その事例と対応方法をまとめておく。

デバイスドライバの ioctl が ENOTTY エラーする

Linux のデバイスは、 /dev 以下のデバイスファイルを介して扱います。

ファイルなので、デバイスドライバへのアクセスは次の 4 つの API が基本です。

  • open
  • read
  • write
  • close

これら API は、ファイルアクセスの基本 API でもあるので説明の必要はないでしょう。

そして、上記の 4 つに加えて次の API が代表的です。

  • ioctl

今回この ioctl で ENOTTY エラーが発生しました。

この ENOTTY エラーが何なのか?以下の URL によると、

<https://linuxjm.osdn.jp/html/LDP_man-pages/man2/ioctl.2.html>

  • ENOTTY

    • fd がキャラクター型のスペシャルデバイスを参照していない。
  • ENOTTY

    • 指定されたリクエストはディスクリプター fd が参照する種類のオブジェクトには適用することができない。

なるほど分からん。。

まぁ、 ENOTTY エラーの説明はこの際放っておいて、何故このエラーが発生するかを説明します。

ENOTTY エラーの原因

これは 64bit カーネル環境で 32bit アプリを動かした場合に発生する現象です。 32bit カーネルで 32bit アプリ、 64bit カーネルで 64 bit アプリを動かした場合は発生しません。

つまり、カーネルとアプリとでアーキテクチャに不整合がある時だけ発生します。 なお、 ioctl ではエラーが発生しますが、 open や read では発生しませんでした。

では、なぜ ioctl で発生し、 open や read などでは発生しないかというと、 ioctl のインタフェースの仕様によります。

以下が ioctl のインタフェースです。

int ioctl(int fd, unsigned long request, ...);

ここで各パラメータは、

  • fd は、 open() で取得した fd です。
  • request はデバイスドライバで定義する命令コードです。
  • … は、その命令コードのパラメータです。

これらパラメータはデバイスドライバ側で任意に決めるものですが、 request 引数に関しては、linux/ioctl.h に次のマクロが用意されています。

  • _IO(group,num)
  • _IOW(group,num,type)
  • _IOR(group,num,type)
  • _IOWR(group,num,type)

ここで group と num は、命令コードの種別を示し、 type はパラメータの型を示します。

例えば _IOW( 1, 2, int ) のようにマクロを利用します。 これによって、 group:1, num:2 の命令で、パラメータが int 長の命令を実行するための request が定義されます。 さらに具体的には、次のように _IOW() を使って ioctl をコールします。

ioctl( fd, _IOW( 1, 2, int ), param );

ここで param は、 int 型のパラメータです。

なお、 ioctl 等の命令はデバイスドライバにアクセスするアプリが利用する API です。 一方でデバイスドライバ側は、それらに対応する処理を登録しています。

つまり、 ioctl に相当する処理がデバイスドライバ側に定義されていて、 アプリ側が各 API をコールすると、デバイスドライバ側の関数が実行されます。

ioctl の場合、 _IOW() などのマクロから定義された request から、 何の命令が要求されているのかを判断し、 その要求に応じて付加されているパラメータを取得し、 処理を実行します。

具体的には、次のような処理がデバイスドライバには書かれています。

switch ( request ) {
   case _IOR( 1, 0, int ):
      // group:1, num:0 の処理
      break;
   case _IOR( 1, 1, int ):
      // group:1, num:1 の処理
      break;
   case _IOR( 1, 2, int ):
      // group:1, num:2 の処理
      break;
   default:
      // エラー処理
}

ここで問題となるのが、アーキテクチャ(ビット)の違いです。

_IOW( 1, 2, int* )

上記のように、パラメータが int* (int のポインタ) 型の場合、 一般的に 32bit アーキテクチャでは、ポインタのサイズは 32bit。 64bit アーキテクチャでは、ポインタ型は 64bitと、 ポインタのサイズはアーキテクチャに依存します。

ポインタ以外にも、型のサイズが異なるケースがあります

つまりアーテクテチャによって、 _IOW() などのマクロの結果が変ってくる、ということです。

この違いによって、 アプリ側で _IOW( 1, 2, int* ) とした結果と、 ドライバ側で _IOR( 1, 2, int* ) とした結果が異なってしまい、 上記の switch-case 文で条件にマッチせずにエラー処理に落ちてしまいます。

ただ、この違いが問題になるのは、 デバイスドライバとアプリのアーテクテチャ(ビット)が異なる場合だけです。

デバイスドライバとアプリのアーテクテチャ(ビット)が同じであれば、 _IOW() の結果に不整合は発生しません。

なお、ここまで説明しておいてなんですが、 ENOTTY エラーになる原因は、これとは少し異なります。

そもそも 64 ビットカーネル環境で 32 ビットアプリを動かした場合、 デバイスドライバの通常の ioctl 処理はコールされず、 下位互換専用の ioctl 処理がコールされます。 しかし、デバイスドライバに下位互換専用の ioctl 処理の定義自体がないと、 ENOTTY になります。

今回問題になったのは、 デバイスドライバに下位互換専用の ioctl 処理の定義自体がなかったのが原因でした。

下位互換専用の ioctl 処理がなぜ必要かというと、 アーキテクチャの違いを想定していないデバイスドライバがあった場合、 下位互換専用ではない通常の ioctl を動かしてしまうと、 どのような不具合が発生するか保証が出来ません。 そこで、安全方向に振って下位互換専用の ioctl 処理の定義がない場合は ioctl を ENOTTY エラーに落すようにしていると考えられます。

エラーの対応方法

このエラーの対応方法は次のいずれかです。

  • アプリを 64 ビットでビルドする
  • 64ビット環境下のデバイスドライバを、 32bit ioctl 対応する。

アプリを 64 ビットでビルドして動かせればそれが最も簡単な対応方法ですが、 現状の Raspberry Pi OS では 64ビットアプリのビルド & 実行は簡単ではありません。

そこで、ここではデバイスドライバを 32bit ioctl 対応します。

デバイスドライバを 32bit ioctl 対応するには、 デバイスドライバのソース修正が必須です。

まずは、デバイスドライバのソースを取得します。

そして、デバイスドライバのソースから file_operations 構造体を定義している箇所を探します。

ioctl をサポートするデバイスドライバの定義には、 次のように ioctl を定義している箇所が必ずあります。

 .unlocked_ioctl = driver_unlocked_ioctl,

ここに、次の .compat_ioctl を追加します。

 .unlocked_ioctl = driver_unlocked_ioctl,
 .compat_ioctl = driver_compat_ioctl, // 追加

この .compat_ioctl が、下位互換専用の ioctl です。

そして、driver_unlocked_ioctl の関数定義をコピーして、 driver_compat_ioctl の関数定義を作成します。

このとき気をつけるのが、 ioctl() の request の処理です。

_IOR( 1, 2, int* ) のようにそのままマクロを利用すると int* が 64bit 長になってしまうので、 32bit 長になるように int* 部分を適宜書き換えが必要です。

BUS ERROR (SIGBUS)

BUS ERROR は、アクセスしているアドレスに問題があることを示すエラーです。

https://www.wdic.org/w/TECH/SIGBUS

上記 URL の情報によると、次の 2 つのケースを表わします。

  • 有効なメモリーアドレスだが、アクセス許可が無い領域にアクセスしようとした場合
  • アドレス境界(ワード境界、ダブルワード境界など)を無視してアクセスしようとした場合 (アドレスエラー)

今回問題になったのは、後者のケースです。

これはどういうことかというと、次のようなケースで発生します。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int32_t buf[ 2 ];
    int32_t * pVal = (int32_t*)((int8_t*)buf+1);
    *pVal = 0;  // error
    printf( "%p: %d\n", pVal, *pVal );
    return 0;
}

上記の *pVal = 0; でエラーが発生します。

ただし多くの環境で、上記のサンプルコードはエラーしません。

これは、アライメントされていない非境界整列へのアクセスがあった場合、 エラーとせずに CPU 側で処理するように設定がされているからです。

ARM では、これをエラーとするかどうかを制御する CPU の制御フラグがあります。

raspberry pi OS の 32bit, 64bit 版の違いによって、このフラグの設定が異なるようです。

raspberry pi OS フラグ
32bit カーネル エラーとしない
64bit カーネル エラーとする

なぜこのような違いがあるかネットで調べてみましたが、 そのものズバリな回答はありませんでした。

ただ、ネットで調べた情報から推測すると以下が考えられます。

非境界整列へのアクセスが