Linuxカーネルモジュールを作った話(調査編)

  • 8
    Like
  • 0
    Comment

はじめに

SRA Advent Calendarの8日目です。
ネットワークシステムサービス第1事業部の ふじまき です。

AsusのX205TAという格安ノートPC(以下、X205TA)にLinuxをインストールすると円記号、アンダースコアのキーが動作しない。色々調べたところキーボード側のバグだったので、それを直すカーネルモジュールを作った、という話です。
https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/tree/drivers/hid/hid-asus.c?h=v4.7

X205TAは32bit UEFIというちょっと変なマシンでインストールにコツが必要だったりしますが、その辺の説明は省きます。

原因調査その1

  1. キーボードの素性調査
    使えないのは一部のキーだけなので、evtestでデバイスを確認

    
    x205ta# evtest
    No device specified, trying to scan all of /dev/input/event*
    Available devices:
    /dev/input/event0:  Lid Switch
    /dev/input/event1:  Power Button
    /dev/input/event2:  Sleep Button
    /dev/input/event3:  USB2.0 VGA UVC WebCam
    /dev/input/event4:  PC Speaker
    /dev/input/event5:  Asus WMI hotkeys
    /dev/input/event6:  PDEC3393:00 0B05:8585
    /dev/input/event7:  Elan Touchpad
    /dev/input/event8:  Video Bus
    Select the device event number [0-8]:
    

    それっぽいのを試すと"PDEC3393:00 0B05:8585"がキーボードだと判明。(ただし、円記号、アンダースコアを押下した場合は無反応)
    起動時のログに同じ文字列が"Keyboard"という単語と一緒に出てくるので、とりあえず間違いなさそう。
    
    [    3.968842] input: PDEC3393:00 0B05:8585 as /devices/platform/80860F41:00/i2c-0/i2c-PDEC3393:00/0018:0B05:8585.0001/input/input6
    [    3.970835] hid-generic 0018:0B05:8585.0001: input,hidraw0: I2C HID v1.00 Keyboard [PDEC3393:00 0B05:8585] on i2c-PDEC3393:00
    

    "I2C","HID"をキーワードにググるとUSB Human Interface Device(HID)の物理層をI2Cというシリアルバスに置き換えたものをマイクロソフトが提唱していたらしい。
    ということでX205TAのキーボードはプロトコル的にはUSB接続キーボードと同じ"HID"、電気的な接続はI2C、カーネル的にはdrivers/hid/が関連していそう、というところまで判明。
  2. どこで止まってる?
    drivers/hid/のKconfig,Makefileを見ると特定のデバイス向けのモジュールが沢山あること、hid-core.cとhid-input.cがメインっぽいことが分かる。
    printk()デバッグから、正常なキーと無反応キーの違いはhid-core.cの以下の処理と特定

static void hid_input_field(struct hid_device *hid, struct hid_field *field,
                            __u8 *data, int interrupt)
{
        unsigned n;
        unsigned count = field->report_count;
        unsigned offset = field->report_offset;
        unsigned size = field->report_size;
        __s32 min = field->logical_minimum;
        __s32 max = field->logical_maximum;
        __s32 *value;

        value = kmalloc(sizeof(__s32) * count, GFP_ATOMIC);
        if (!value)
                return;

        for (n = 0; n < count; n++) {

                value[n] = min < 0 ?
                        snto32(hid_field_extract(hid, data, offset + n * size,
                               size), size) :
                        hid_field_extract(hid, data, offset + n * size, size);

                /* Ignore report if ErrorRollOver */
                if (!(field->flags & HID_MAIN_ITEM_VARIABLE) &&
                    value[n] >= min && value[n] <= max &&
                    field->usage[value[n] - min].hid == HID_UP_KEYBOARD + 1)
                        goto exit;
        }

        for (n = 0; n < count; n++) {

                if (HID_MAIN_ITEM_VARIABLE & field->flags) {
                        hid_process_event(hid, field, &field->usage[n], value[n], interrupt);
                        continue;
                }

                if (field->value[n] >= min && field->value[n] <= max
                        && field->usage[field->value[n] - min].hid
                        && search(value, field->value[n], count))
                                hid_process_event(hid, field, &field->usage[field->value[n] - min], 0, interrupt);

                if (value[n] >= min && value[n] <= max
                        && field->usage[value[n] - min].hid
                        && search(field->value, value[n], count))
                                hid_process_event(hid, field, &field->usage[value[n] - min], 1, interrupt);
        }

        memcpy(field->value, value, count * sizeof(__s32));
exit:
        kfree(value);
}

正常なキーと無反応キーの違いは

  • 正常なキー:2つめのforループ内の2,3番目のif文からhid_process_event()が呼ばれる。
  • 無反応キー:hid_process_event()が呼ばれない

どちらのキーの場合でも押下時、リリース時共通でhid_input_field()が2回ずつ呼ばれる。
具体的な例を挙げるとは"1"を押下した場合、min==0,max==101,value[0]==30、円記号を押下した場合、min==0,max==101,value[0]==137となっていた。
min/maxは同じだが、円記号の場合value[0]がmaxを超えているためhid_process_event()が呼ばれない、というところまではprintk()デバッグで特定。
だがHIDプロトコルを理解していないのでこのif文が何を意味しているか理解不能:sob:当然原因もわからず。

ということでHIDが何なのか調べてみる。

HID(Human Interface Device)

読んで字のごとく、コンピューターの周辺機器で人間が使う入力機器の規格。USBデバイス用が最初だが、その後MicrosoftがHID over I2C、HID over Bluetoothとかを提唱したらしい。
USB HIDの詳しい内容はDevice Class Definition for HID 1.11(※PDF注意)を参照。。。で終わらせると書くこと無くなるので超簡単に説明すると、

  • デバイス接続時、デバイスからPCへ「レポートディスクリプタ」というデータを送る。「レポートディスクリプタ」は後述のレポートのフォーマットを定義する。
  • 人間がデバイスを操作すると対応する「レポート」がPCへ送られる。
  • 「レポート」を受け取ったOSは事前に「レポートディスクリプタ」で定義されたフォーマットに従ってその内容を解読して人間の操作に応じた動作を行う。

「レポート」にはInput,Output,Featureの3種類がある。キーボードのキーを押下した際に送られるのは"Inputレポート"。キーボードのLEDを光らせるのはOutputレポート。(Featureは調べて無いので知らない)
実際に送られるデータのうち、個々の「値」はHID Usage Tables 1.12(※PDF注意)で定義されている。P.57,60を見ると円記号とアンダースコアのUsage IDが137135となっており、前項のprintk()デバッグで確認した値と一致。
Universal Serial Bus (USB) HID Usage Tables P.57より

Hut_P57.png

↑Keyboard International1と3(Footnotes15と17)が動作しないキー↓
Universal Serial Bus (USB) HID Usage Tables P.60より

Hut_P60.png

原因調査その2

X205TAで実際のレポートディスクリプタを調べる。
HIDデバイスのレポートディスクリプタはsysfs配下にあり(find /sys -name 'report_descriptor'で探す)、以下のようにhidrd-convertを通すことで解読できる。

$ cat /path/to/report_descriptor | hidrd-convert -o code

X205TAのレポートディスクリプタをhidrd-convertに通すと以下のようになっている。

0x05, 0x01,         /*  Usage Page (Desktop),               */
0x09, 0x06,         /*  Usage (Keyboard),                   */
0xA1, 0x01,         /*  Collection (Application),           */
0x85, 0x01,         /*      Report ID (1),                  */
0x05, 0x07,         /*      Usage Page (Keyboard),          */
0x19, 0xE0,         /*      Usage Minimum (KB Leftcontrol), */
0x29, 0xE7,         /*      Usage Maximum (KB Right GUI),   */
0x15, 0x00,         /*      Logical Minimum (0),            */
0x25, 0x01,         /*      Logical Maximum (1),            */
0x75, 0x01,         /*      Report Size (1),                */
0x95, 0x08,         /*      Report Count (8),               */
0x81, 0x02,         /*      Input (Variable),               */
0x95, 0x01,         /*      Report Count (1),               */
0x75, 0x08,         /*      Report Size (8),                */
0x81, 0x03,         /*      Input (Constant, Variable),     */
0x95, 0x05,         /*      Report Count (5),               */
0x75, 0x01,         /*      Report Size (1),                */
0x05, 0x08,         /*      Usage Page (LED),               */
0x19, 0x01,         /*      Usage Minimum (01h),            */
0x29, 0x05,         /*      Usage Maximum (05h),            */
0x91, 0x02,         /*      Output (Variable),              */
0x95, 0x01,         /*      Report Count (1),               */
0x75, 0x03,         /*      Report Size (3),                */
0x91, 0x03,         /*      Output (Constant, Variable),    */
0x95, 0x06,         /*      Report Count (6),               */
0x75, 0x08,         /*      Report Size (8),                */
0x15, 0x00,         /*      Logical Minimum (0),            */
0x25, 0x65,         /*      Logical Maximum (101),          */
0x05, 0x07,         /*      Usage Page (Keyboard),          */
0x19, 0x00,         /*      Usage Minimum (None),           */
0x29, 0xDD,         /*      Usage Maximum (KP Hexadecimal), */
0x81, 0x00,         /*      Input,                          */
0xC0,               /*  End Collection,                     */
/* 以下略 */

5~12行目。モディファイア(修飾キー)用のInputレポートのフィールド

0x05, 0x07,         /*      Usage Page (Keyboard),Usage Page指定。0x07 == Keyboard/Keypadを指定。
                     *      以降、別のUsage Page指定で上書きされるまでKeyboard/KeypadページのUsageが使用される
                     */
0x19, 0xE0,         /*      Usage Minimum (KB Leftcontrol),Usageの最小値指定。0xE0==左コントロール */
0x29, 0xE7,         /*      Usage Maximum (KB Right GUI),Usageの最大値指定。0xE7==右GUI(右Winキー/右Appleキーのこと)。最小値の定義とあわせるとUsageは0xE0~0xE7の8個 */
0x15, 0x00,         /*      Logical Minimum (0),個々のレポートの最小値。0を設定 */
0x25, 0x01,         /*      Logical Maximum (1),個々のレポートの最大値。1を設定 */
0x75, 0x01,         /*      Report Size (1),個々のレポートのサイズは1bit */
0x95, 0x08,         /*      Report Count (8),レポートの数は8個 */
0x81, 0x02,         /*      Input (Variable),Inputレポートを定義。ここまでに定義された属性をまとめると
                     *      このレポートは各1bitのレポート8個の配列(計1バイト)で最下位ビットが左コントロールの状態、以降、順番に最上位ビットは右GUIの状態を表していると解釈される。
               */

13~15行目。パディング

0x95, 0x01,         /*      Report Count (1),レポートの数は1個 */
0x75, 0x08,         /*      Report Size (8),個々のレポートのサイズは8bit(1Byte) */
0x81, 0x03,         /*      Input (Constant, Variable),Inputレポートを定義
                     *      1Byteが1個の"定数"のレポート。定数なのでただのパディング
                     */

16~21行目。ここはキーボードのLEDを操作するOutputレポートのフィールド

0x95, 0x05,         /*      Report Count (5),レポートの数は5個 */
0x75, 0x01,         /*      Report Size (1),個々のレポートのサイズは1bit */
0x05, 0x08,         /*      Usage Page (LED),Usage PageをLED(0x08)に変更 */
0x19, 0x01,         /*      Usage Minimum (01h),Usageの最小値は0x01==Num Lock */
0x29, 0x05,         /*      Usage Maximum (05h),Usageの最大値は0x05==Kana(かな) */
0x91, 0x02,         /*      Output (Variable),Outputレポートを定義
                     *      各1bitのレポートが5個の配列で最下位bitがNumLock、最上位bitがKana(かな)
                     *      ちなみにLogical MinimumとLogical Maximumは前出のInputレポート定義で宣言されて以降変更されていないのでそのまま0と1
                     */

22~24行目。引き続きLED用のOutputレポート。↑が5bitだったのでパディング用に3bit

0x95, 0x01,         /*      Report Count (1),レポートの数は1個 */
0x75, 0x03,         /*      Report Size (3),レポートのサイズは3bit */
0x91, 0x03,         /*      Output (Constant, Variable),Outputレポートを定義
                     *      3bitが1個の定数のOutputレポート。パディング
                     */

25~32行目。Usage Pageをキーボードに再設定してInputレポートのフィールドを定義

0x95, 0x06,         /*      Report Count (6),レポートの数は6個 */
0x75, 0x08,         /*      Report Size (8),個々のレポートのサイズは8bit(1Byte) */
0x15, 0x00,         /*      Logical Minimum (0),個々のレポートの最小値は0 */
0x25, 0x65,         /*      Logical Maximum (101),個々のレポートの最大値は101(0x65) */
0x05, 0x07,         /*      Usage Page (Keyboard),Usage PageをKeyboard(0x07)に再設定 */
0x19, 0x00,         /*      Usage Minimum (None),Usageの最小値は0(0はエラー等を通知する為に予約) */
0x29, 0xDD,         /*      Usage Maximum (KP Hexadecimal),Usageの最大値は0xDD(HIDの定義では"Hexadecimal"というキーだが実在しない?) */
0x81, 0x00,         /*      Input,Inputレポートを定義。ここまでに定義された属性をまとめると
                     *      このレポートは各8bitのレポート6個の配列(計6バイト)、個々のレポートの最小値0、最大値は101
                     *      実際にキーボードから送られるUsageの最小値は0、最大値は221
                     */

件の円記号のUsage IDは0x89(137)なので、このInputレポートのフィールド(Usageの範囲が0x00~0xDD)に含まれている。
円記号を押すと6個のレポートのうち1個が0x89(137)になったInputレポートが送られる。。。でも個々のレポートの値の範囲は0~101。。。
ん?範囲超えてる。。。?!
ピコーン(AA略

原因調査その1のprintk()デバッグに繋がったところで続く。

おまけ

定義されたInputレポートのフィールド3つを定義された順番に並べると以下のようになる。


+--------+--------+--------+--------+--------+--------+--------+--------+
|        |        |        |        |        |        |        |76543210|
+--------+--------+--------+--------+--------+--------+--------+--------+
  Byte 7   Byte 6   Byte 5   Byte 4   Byte 3   Byte 2   Byte 1   Byte 0
     |        |        |        |        |        |       |         |
     +--------+--------+---+----+--------+--------+    パディング   |
                           |                                        |
                           |                      各モディファイアキーの状態を1bitで表したもの(押下=1)
               モディファイア以外のキー入力       0: 左Control
                  1 Byteが6個の配列               1: 左Shift
                                                  2: 左Alt
                                                  3: 左GUI(Windowsでは左Winキー、Macでは左Appleキー)
                                                  4: 右Control
                                                  5: 右Shift
                                                  6: 右Alt
                                                  7: 右GUI(Windowsでは右Winキー、Macでは右Appleキー)

※実際のX205TAでは上記バイト列の先頭にレポートIDという1Byteの識別子が付いているが省略(hidrd-convertの出力の4行目参照)

キーボードの"A"を押下すると上図の"Byte 2"のところが0x04、他は全部ゼロというバイト列がキーボードから送られる。
動作しない円記号を押下した場合、"Byte 2"のところが0x87になったバイト列が送られてくる。が、最大値(0x65)を超えているので破棄される。