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


SRA Advent Calendarの9日目です。
ネットワークシステムサービス第1事業部の ふじまき です。
Linuxカーネルモジュールを作った話(解決編)です。

調査の結果、X205TAの一部のキーが動作しない原因はキーボードが送ってくるレポートディスクリプタ内の最大値の設定が間違っていて、日本語キーボード固有の円記号、アンダースコアの入力が無視されていた、という事が分かりました。
キーボードは直せないのでカーネル側でなんとかしよう、ということでパッチを作成してみることにしました。

カーネルモジュールの作成&投稿

暫定パッチ

いきなりカーネルモジュールを書くのは敷居が高いので、まず場当たり的に直してみました。(原因の推測が正しいかの確認も含めて)
レポートディスクリプタをパースしながら個々のフィールドを定義しているdrivers/hid/hid-core.cのhid_add_field()に

  • 機種がAsusのX205TAの日本語キーボードモデル
  • Logical Maximumが101、Usageの最大値が222のフィールド

上記の条件を満たすフィールドが出現したらフィールドを追加する前に問答無用でLogical Maximumを222に変更する処理を追加

--- linux-4.4.4_orig/drivers/hid/hid-core.c     2016-01-11 08:01:32.000000000 +0900
+++ linux-4.4.4/drivers/hid/hid-core.c  2016-03-06 22:37:13.649212892 +0900
@@ -30,6 +30,7 @@
 #include <linux/vmalloc.h>
 #include <linux/sched.h>
 #include <linux/semaphore.h>
+#include <linux/dmi.h>

 #include <linux/hid.h>
 #include <linux/hiddev.h>
@@ -225,6 +226,9 @@
        unsigned usages;
        unsigned offset;
        unsigned i;
+       const struct dmi_device *dev = NULL;
+       char const *bios_version;
+       char product_num[7];

        report = hid_register_report(parser->device, report_type, parser->global.report_id);
        if (!report) {
@@ -286,6 +290,24 @@
        field->unit_exponent = parser->global.unit_exponent;
        field->unit = parser->global.unit;

+       /*
+        * Fix ASUS X205TA(2015 Spring Japanese model) invalid report descriptor.
+        */
+       bios_version = dmi_get_system_info(DMI_BIOS_VERSION);
+       if (unlikely(strncmp(bios_version, "X205TA", 6) == 0)){
+               while ((dev = dmi_find_device( DMI_DEV_TYPE_OEM_STRING, NULL, dev))) {
+                       if ((sscanf(dev->name, "90NL073%7c", product_num) == 1)&&
+                           (strncmp(product_num, "1-M0441",7) == 0 ||
+                            strncmp(product_num, "2-M0442",7) == 0 ||
+                            strncmp(product_num, "4-M0443",7) == 0 )){
+                               if (field->logical_maximum == 101 &&
+                                   field->maxusage == 222) {
+                                       field->logical_maximum = 222;
+                               }
+                               break;
+                       }
+               }
+       }
        return 0;
 }

このパッチを適用したカーネルで起動したところ、無反応だったキーが動くようになりました。

モジュールの作り方の事前調査

暫定パッチが動作したので、ちゃんとしたカーネルモジュールの作成方法を調査。
drivers/hid/直下のソースをいくつか眺めたところ、hid-aureal.c,hid-elecom.cなどがレポートディスクリプタを修正していると判明

核となるのはhid_driver構造体で、

  • id_tableメンバに対象となるデバイスのVendorIDとProductIDを定義
  • report_fixupにレポートディスクリプタ書き換え処理を定義すればよさそう。

とりあえず目に付いたhid-elecom.cがhid-ortek.cをベースにしたと書いてあったので、同じように作ってみることにした。

カーネルモジュール作成

まずlinux-next.gitをclone

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git

cloneしたディレクトリに移動して名前、メールアドレスを設定

$ cd linux-next
$ git config --global user.name "Yusuke Fujimaki"
$ git config --global user.email [email protected]

モジュール追加場所はdrivers/hid/の下で、
1. hid-ids.hにDevice IDのマクロ追加
2. hid-core.cのhid_have_special_driverにVendor ID,Product IDのセットを追加
3. hid-asus.cをhid-elecom.cをベースに新規作成。(レポートディスクリプタの修正箇所が違うだけでやってることは殆ど同じ)
4. MakefileとKconfigそれぞれに追加モジュールの定義を追加

作成したパッチを"Signed-off-by"付きでコミット

$ git commit -s

エディタが起動するので、なぜこのパッチが必要なのかをログに書く。英語で
ログの1行目がgit format-patchコマンドの件名になるので、「"サブシステム名": 概要」のようにする。
このドライバはHIDサブシステムのドライバなので、1行目は「HID: Asus X205TA keyboard driver」、3行目から説明文、でコミット

動かないパッチを送っても恥ずかしいのでパッチ適用されたソースを別のところに展開、ビルドして動作確認しておきます。

$ git archive --format=tar --prefix=linux-next-20160318/ HEAD | ( cd /path/to/kernel-build ; tar xf -)

(以下、動作確認は省略)

パッチ投稿

投稿用パッチファイルの出力先ディレクトリを作成し、そこへパッチを出力

$ mkdir ~/x205patch
$ git format-patch -o ~/x205patch/ --no-numbered  HEAD~

以下のようなgit send-mailで使用するフォーマットで出力されます。

$ cat ~/x205patch/0001-PATCH-HID-Asus-X205TA-keyboard-driver.patch 
From b636602b8a7d727e968da981676879bde9484167 Mon Sep 17 00:00:00 2001
From: Yusuke Fujimaki <[email protected]>
Date: Sun, 20 Mar 2016 23:29:38 +0900
Subject: [PATCH] HID: Asus X205TA keyboard driver

Asus X205TA built-in keyboard contains wrong
logical maximum value in report descriptor.
(以下略)

パッチ1個で完結しているので、ファイル名の先頭の数字は外しておきます。

$ mv ~/x205patch/0001-PATCH-HID-Asus-X205TA-keyboard-driver.patch ~/x205patch/PATCH-HID-Asus-X205TA-keyboard-driver.patch

パッチの送付先はカーネルソースのscriptsディレクトリにある"get_maintainer.pl"というスクリプトで確認

$ ./scripts/get_maintainer.pl ~/x205patch/PATCH-HID-Asus-X205TA-keyboard-driver.patch
Jiri Kosina <[email protected]> (maintainer:HID CORE LAYER)
Benjamin Tissoires <[email protected]> (reviewer:HID CORE LAYER)
[email protected] (open list)
[email protected] (open list:HID CORE LAYER)

パッチ投稿にはgit send-mailコマンドを使います。今回のパッチはHIDサブシステム以外に影響することはなさそうなので、上記で確認した宛先のうち、linux-kernel以外に送信(というか小心者なのでlinux-kernelとか無理)

$ git send-email --to="Jiri Kosina <[email protected]>" \
  --cc="Benjamin Tissoires <[email protected]>" \
  --cc="[email protected]" \
  --smtp-server=smtp.gmail.com --smtp-encryption=tls \
  [email protected] --smtp-server-port=587 \
  --smtp-domain=hogehoge.example.jp  ~/x205patch/PATCH-HID-Asus-X205TA-keyboard-driver.patch

本当に送信するか確認を求められるので"y"を入力

Send this email? ([y]es|[n]o|[q]uit|[a]ll): y

投稿に使用するGMailアカウントのパスワードを入力

Password for 'smtp://[email protected]@smtp.gmail.com:587':

おわり。

OK. Log says:
Server: smtp.gmail.com
(以下略)

一番最初に投稿したメール
http://www.spinics.net/lists/linux-input/msg43796.html

最初の投稿の後、後継機のAsus E200HAも同じ症状かつ投稿したパッチでは動かなかったのでさらに修正パッチを送ったり、Jiri Kosinaがコメントを直してくれているのでリリースされたカーネルに含まれているものとは微妙に違います。

製作秘話(?)

  • 実際のパッチ作成当時は/sysのレポートディスクリプタやhidrd-convertなんて便利なコマンドは知らなかったので(後にMLの投稿で知った)、調査編であっさり原因特定しているのは嘘です。
  • なのでコミット時のログにhidrd-convertの出力結果っぽいものが付いてますが、あれはHIDの仕様書を読みながらカーネル内でレポートディスクリプタをパースしている箇所に地道にデバッグプリントを仕込んで調べたものです。
  • HIDを理解するのに苦労しましたが、ルネサスエレクトロニクスのドキュメント(PDF)にかなり助けられました。