先の hello_world.c を修正して、課題だったパラメータを入力する、エラーコードを返す実装をしてみましょう。make 方法と Makefile は hello_world.c と変わらないので省略します。コンパイルが済んだと言う前提でこのページを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | - | | | ! - | ! - | | ! - ! |
|
モジュールはパラメータを受け取ることができます。主な手段は module_param() マクロ, DEVICE_ATTR マクロ, debugfs, procfs です。ここでは module_param() を試します。次の表にそれぞれの説明をまとめておきます。パラメータ引き渡し方法にファイル・システム名が書かれている場合は、そのファイル・システム上に作られたノードを通して設定します。設定単位はノードと設定対象の対応です。.config は linux-stable ディレクトリ(ソース・コードの基底ディレクトリ)にある .config に書かれた構築条件です。=Y になっている場合に利用可能です。
手段 | パラメータ引き渡し方法 | 設定単位 | .config | ドキュメント、特徴 |
module_param | insmod or modprobe command line, boot parameter, sysfs node | モジュール毎 | CONFIG_SYSFS | もっとも簡単です。変数名とパラメータ名を独立に指定できる module_param_named()、Call Back を実装できる module_param_cb() があります。 ノードは /sys/module/module_name/parameters/nameにできます。 |
DEVICE_ATTR | sysfs node | デバイス毎 | CONFIG_SYSFS | ドキュメントは Documentation/driver-model/device.txt, Documentation/filesystems/sysfs.txt に有ります。デバイス毎にパラメータを設定できます。デバイス毎ですので、モジュール・ロード時にデバイスが無い場合はパラメータを設定できません。実装か必要なコード量は多いです。 ノードは /sys/devices/subsystem_base/subsystem/* にできます。バスやクラス構造的に追いやすい /sys/bus/* 以下、/sys/class/* 以下、/sys/block/* 以下からもシンボリックリンクを通じてアクセスできます。 |
debugfs | debugfs node | 任意構造 | CONFIG_DEBUG_FS | ドキュメントは debugfs にあります。主に使う関数は debugfs_create_dir(), debugfs_create_file(), debugfs_remove(), debugfs_remove_recursive() です。整数を扱う単純なノードであればドキュメントあるいは include/linux/debugfs.h から使用例を探して使うのが良いでしょう。 普通の Linux の構成であれば debugfs は /sys/kernel/debug に mount されます。あるいは mount して使うことができます。 |
procfs | procfs node | 任意構造 | CONFIG_PROC_FS | procfs を中心にした使い方のドキュメントは有りません。include/linux/proc_fs.h より辿れる関数と Documentation/filesystems/seq_file.txt に使用例があるので参考にしてください。proc_create(), proc_create_data(), proc_mkdir(), remove_proc_entry() などの関数とマクロの使われ方を参考に使用して下さい。モジュールロード時にパラメータとして指定できません。module_init() で指定した関数内でノードを作成し、ロード直後にパラメータを設定することができます。 procfs は /proc に mount されます。 |
sysfs, procfs, debugfs は必ずあるのか?
sysfs, procfs, debugfs は .config の記述により有無を選択できます。このうち sysfs, procfs とも必ず存在すると仮定しても良いでしょう。sysfs または procfs を無くすと相当に不便です。system call に匹敵する機能を分担しています。
debufs はディストリビューションや評価環境によっては「無し」に設定されていることが有ります。debugfs で提供されるノードの機能は開発時は便利です。しかし、製品としてリリースできないようなセキュリティ・ホール、コンテンツ保護違反、NDA(機密保持契約)違反、動作が不安定になる機能が実装される傾向があります。debugfs (あるいはその一部ノード)を製品リリースに含めるかどうか精査した方が良いでしょう。
いずれの方法もノードができます。ノードは属性の設定次第で read/write が可能です。userland - kernel 双方向で更新して情報交換できます(次の注意参照)。
module_param を pchar 型パラメータで使う場合の注意
module_param を pchar 型(文字列)で扱う場合は、バッファ確保動作に注意して下さい。ユーザーがノードに書き込む場合は param_set_charp() 読み出す場合は param_get_charp() が動作します。 param_set_charp() の内部で動的にメモリ確保が行われます。巧妙な kmalloc_parameter() と maybe_kfree_parameter() の動作により初期値として指していたメモリ領域は誤って解放されないようになっています。一度でもユーザーがノードを経由して書き込んだ場合、動的にバッファは確保され、そのサイズは書き込んだ文字列を strlen() で測った長さ + 1 (NUL 終端された文字列を格納するのに必要十分な長さ) となります。十分なバッファが確保されていない可能性が有ります。Kernel 内から module_param 経由で文字列を返す場合ノードの属性を read only にして、初期文字列(バッファ)を使用し続ける様にするか、module_param_cb() を使用することを検討して下さい。
まず、パラメータ無しで insmod, rmmod を試します。ソース・コードに書かれたデフォルト値で動作しているのがわかります。
$ sudo insmod hello_world.ko [sudo] password for user_name:password $ sudo rmmod hello_world.ko $ dmesg
dmesg が出力した最後は次のようになります。your_name と return_value の値がそのまま表示されました。
[355028.315777] hello_world: module verification failed: signature and/or required key missing - tainting kernel [355028.319261] hello_world_init: Hello Taro san. return_value=0 [355036.306520] hello_world_exit: Goodbye Taro san.
パラメータを付けて起動しましょう。your_name=Hanako とします。
$ sudo insmod hello_world.ko your_name=Hanako $ sudo rmmod hello_world.ko $ dmesg
dmesg が出力した最後は次のようになります。your_name に指定した Hanako が表示されました。アプリケーション・プログラムと仕組みは違いますがパラメータを受け取る手段が有ります。
[355218.292794] hello_world_init: Hello Hanako san. return_value=0 [355236.027700] hello_world_exit: Goodbye Hanako san.
Kernel に組み込んだ後、パラメータを変えてみます。パラメータは /sys/module/hello_world/parameters の下にノードとして存在します。return_value と your_name のつのパラメータを作ったのでノードは 2 つできています。パーミッションは module_param の引数で指定したとおりになっています。
$ sudo insmod hello_world.ko $ ls -la /sys/module/hello_world/parameters total 0 drwxr-xr-x 2 root root 0 8月 22 10:34 . drwxr-xr-x 6 root root 0 8月 22 10:34 .. -rw-r--r-- 1 root root 4096 8月 22 10:34 return_value -rw-r--r-- 1 root root 4096 8月 22 10:34 your_name
ノードのサイズが 4096 です。理由を探ってみましょう。sysfs_add_file_mode_ns() に size = PAGE_SIZE; と書いてあるところから由来します。binary data を直接扱わないノードであれば、4096 に固定されます。書き込みは最大で PAGE_SIZE バイトだけできます。内部では kernfs_fop_write() の実装により PAGE_SIZE + 1 だけバッファが確保されます。
module_param を charp 型で使う場合の長さ制限
module_param() を charp 型で使う場合、書き込みの長さは param_set_charp() の実装により終端 NUL を含まない文字列の長さは 1024 バイトに制限されます。
ノードを読み出してみます。ls -la (stat) で報告された長さではなく、格納されている内容に合ったサイズで読み出せます。ノードからの読み出しに高水準ファイル IO ライブラリを使う場合、stat で報告されたファイルサイズと実際に読めるサイズの食い違いがあっても問題なく動作するか確認しておくと良いでしょう。
$ cat /sys/module/hello_world/parameters/return_value 0 $ cat /sys/module/hello_world/parameters/your_name Taro $ cat /sys/module/hello_world/parameters/return_value | od -A x -t x1 000000 30 0a 000002 $ cat /sys/module/hello_world/parameters/your_name | od -A x -t x1 000000 54 61 72 6f 0a 000005
your_name ノードに書き込んでみます。その後、rmmod してみます。モジュールがロードされている間にノードを書き換えた結果が反映されています。
$ sudo sh -c "echo -n Hanako > /sys/module/hello_world/parameters/your_name" $ sudo rmmod hello_world $ dmesg ...省略... [355334.490992] hello_world_init: Hello Taro san. return_value=0 [355455.780006] hello_world_exit: Goodbye Hanako san.
Linux Kernel 内のエラーの扱い方を見ていきます。関数の結果が 成功 or 失敗(エラー) を示す場合、int 型の戻り値で成功は 0、エラーは負の errno 番号 (-Exxx) を使うというのが作法になります。モジュール外から呼ばれる関数、あるいは他のモジュールの関数を呼び出す場合はこの作法に従う(従っている)と考えて実装・ソースの読解を進めて下さい。
自分が作るモジュール内の作法は?
自分で作るモジュール内は色々な都合で独自の作法でも良いでしょう。ただし、エラーの伝搬過程でややこしい事にならないように注意が必要です。関数の結果が成功か失敗ではなく論理的な true か false の場合は、bool 型もしくは (int)1 or (int)0 を使う所も普通に見られます。どんな関数でも戻り値が 0 か 負の値である必要はありません。
module_init() で指定した関数からエラーを返すとどうなるか試してみます。insmod の引数に return_value=-22 を追加します。-22 は -EINVAL です。EINVAL は /usr/include/asm-generic/errno-base.h または include/uapi/asm-generic/errno-base.h でマクロ定義されています。insmod が Invalid parameters エラーメッセージを表示します。
$ sudo insmod hello_world.ko return_value=-22 insmod: ERROR: could not insert module hello_world.ko: Invalid parameters $ dmesg ...省略... [790263.729065] hello_world_init: Hello Taro san. return_value=-22 $ sudo insmod hello_world.ko return_value=-22 insmod: ERROR: could not insert module hello_world.ko: Invalid parameters $ dmesg ...省略... [790263.729065] hello_world_init: Hello Taro san. return_value=-22 [790515.608190] hello_world_init: Hello Taro san. return_value=-22
何回試しても、同じようにエラーになります。insmod が失敗し、hello_world.ko モジュールは組み込まれません。Linux Kernel 内でエラーを関数の戻り値で伝搬する場合、負の errno 番号 (-Exxx) を返します。成功した場合は 0 を返します。
エラーが発生した場合、状況に合わせて include/uapi/asm-generic/errno-base.h または include/uapi/asm-generic/errno.h に定義されたマクロから値を 1 つ選び、負の値にして返します。値の選び方は errno-base.h で定義されている中から、状況に合っていそうな値を探します。どうしても見つからない場合は errno.h から選びます。チームで開発している場合は、errno 番号とその状況について仕様を決めてチーム内で了解しておくと良いでしょう。
module_init() で指定した関数から仕様外の正の値を返すとどうなるか試してみます。insmod の引数に return_value=22 を追加します。insmod コマンドは何も表示しないので成功しています。hello_world.ko は Kernel に組み込まれます。将来もこの挙動になるかというと、保証は無いでしょう。
$ sudo insmod hello_world.ko return_value=22 $ dmesg ...省略... [792581.467679] hello_world_init: Hello Taro san. return_value=22 [792581.467700] do_init_module: 'hello_world'->init suspiciously returned 22, it should follow 0/-E convention [792581.467700] do_init_module: loading module anyway... [792581.467740] CPU: 1 PID: 15413 Comm: insmod Tainted: G OE 4.1.27-local #1 [792581.467766] Hardware name: innotek GmbH VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006 [792581.467779] 0000000000000000 ffff8800776b7d18 ffffffff817d0d19 ffffffffc0148020 [792581.467793] ffff8800048258c0 ffff8800776b7d48 ffffffff817cd175 00000000008d4ecc [792581.467803] ffff8800776b7eb0 0000000000000001 ffffffffc0148070 ffff8800776b7e98 [792581.467813] Call Trace: [792581.467839] [<ffffffff817d0d19>] dump_stack+0x63/0x81 [792581.467851] [<ffffffff817cd175>] do_init_module+0x91/0x1b2 [792581.467867] [<ffffffff811026bc>] load_module+0x1eec/0x2740 [792581.467916] [<ffffffff810fe260>] ? store_uevent+0x40/0x40 [792581.467937] [<ffffffff811030ee>] SYSC_finit_module+0x7e/0xa0 [792581.467950] [<ffffffff8110312e>] SyS_finit_module+0xe/0x10 [792581.467963] [<ffffffff817d8b72>] system_call_fastpath+0x16/0x75
dmesg 出力を見るとスタック・ダンプ (stack dump) が見つかります。今までは訳が分らない出力だと思っていたかもしれません。これからは貴重な情報として見る必要が有ります。Call Trace の一番始めの行に dump_stack() 関数が有ります。これは、do_init_module() から呼ばれたスタックの内容を表示する関数です。メッセージに dump_stack() の呼び出し元が表示されない場合は、メッセージの文字列を検索してソースの場所を特定します。この例では "init suspiciously returned" や "loading module anyway" を検索します。付近に条件式が書かれている場合が多く、なぜエラーになったのか理解の助けになります。
dump_stack() は割り込みが遅延するなどの時間的問題を除いて、Kernel の動作に影響を与えません。万が一、dump_stack() が何かの問題でスタックをトレースできずにハングアップしてしまった場合はスタックが破壊されています。
dump_stack() を呼ぶまでにどのような関数 call を経て実行されたかを追跡できます。呼ばれた逆順で do_init_module(), load_module(), SYSC_finit_module(), system_call_fastpath() を通過したことが判ります。途中の分岐条件などから、変数の値を推測しおおよその状況を把握できます。
SyS_finit_module() の追跡は省略します
SYSCALL_DEFINEx() マクロ と __SYSCALL_DEFINEx() マクロ の内容と使われ方を参考にして下さい。
インライン展開された関数はスタック・ダンプに現れません
gcc の最適化により関数がインライン展開されることがあります。この場合スタック・ダンプに関数は表示されません。関数呼び出しの入れ子を飛ばしてしまった様に見えるかもしれません。50 行程度の実装量がある関数もインライン展開されることがあります。
始めの方に表示される "CPU: 1 PID: 15413 Comm: insmod " も貴重な情報です。PID は process id です。ユーザー・ランドのプロセス稼働状況が分っていれば、どのプロセスからの呼び出しで問題が起きたのか判明します。Comm はプロセス名の一部です。これも貴重な情報です。
割り込み処理の場合は PID と Comm は割り込み処理が「乗った」プロセスです
割り込み処理内でも dump_stack() は使えます。この場合 PID と Comm は割り込み処理が「乗った」プロセスを表示します。「乗った」を詳細に言い換えます。割り込みが発生すると、実行中のプロセス・コンテキストをそのまま使い、割り込み処理を始めると言うことです。割り込みを起こしたデバイスに全く無関係なプロセスのコンテキストで割り込み処理が実行されることも普通に有ります。
グローバル変数 your_name 周辺の実装を見てバッファ・オーバーランが起きないのだろうか?と直感するでしょう。module_param() 周りでかなり巧妙な実装がされています。Linux Kernel のソース・コードが追いにくい面も触れます。
先に結論から示します。param_set_charp() 関数内部で 1024 文字(NUL 終端を含めれば 1025 バイト) に制限されています。ここまで追ってみましょう。
module_param() を出発点にします。マクロは次々と module_param_named(), module_param_cb() に展開されることが分ります。module_param_named() の中で param_ops_##type と言う記述に注目します。このことから、何処かに param_ops_int や param_ops_charp が有るのでしょうか?
param_ops_int は見つかりません。OpenGrok で近いものが見つかるので、何となく場所の当たりは付くと思います。幸いなことに param_ops_charp は見つかりました。kernel/params.c にあります。見つからない定義と、見つかる定義があり、混乱するでしょう。include/linux/moduleparam.hを良く読んでみると param_ops_int は external 宣言されています。何処かにシンボルを定義する記述があるはずです。kernel/params.c の中にある STANDARD_PARAM_DEF() マクロ がその記述です。マクロの ## 演算子を使って巧妙なテンプレートを構成しています。この中に struct kernel_param_ops param_ops_##name いう記述が見つかります。ここで param_ops_int はグローバル変数として確保されています。
ここにたどり着くのに kernel_param_ops 構造体が使われている場所を探す筋もあります。あるいは構築して得られた .o ファイルを grep で探す筋も有りでしょう。
改めて見つかった結果をよく見ると、kernel_param_ops 構造体は get, set メソッド(関数ポインタ)をメンバとして持っています。 param_get_##name, param_set_##name です。include/linux/moduleparam.hの中でこのパターンを持つ関数群が external 宣言されているのも確認できます。文字列のモジュール・パラメータを扱う関数が param_get_charp() と param_set_charp() だということが分りました。
マクロを使った巧妙なテンプレートを使っていると思われる場合、関数を追いかけるため param_ops_, param_get_, param_set_ の様に短縮してみて探すのも手です。
param_get_charp() と param_set_charp() はユーザー・ランドの system call と次のように対応しています。他のメソッド名も合わせて示します。
メソッド名接尾辞または一部 | system call との対応 |
_get | read() |
_set | write() |
_read | read() |
_write | _write() |
Linux Kernel の中では _get, _put の組み合わせもよく使われます。この組は参照カウンタを操作する組です。
メソッド名接尾辞または一部 | 機能 |
_get | 参照カウンタを増やす。 |
_put | 参照カウンタを減らす。カウンタが 0 になったら _release を呼び出す(あるいは相当する解放処理を実行する)。 |
param_set_charp()の実装を見ていきます。関数の入り口で (strlen(val) > 1024) が成立した場合、-ENOSPCでエラーになるように作られています。strlen(ノードに書き込んだ文字列) <= 1024 で制限されていることが分ります。
次に興味深いのは maybe_kfree_parameter(*(char **)kp->arg); と言う処理です。list_for_each_entry() というマクロによってリストkmalloced_paramsを全て走査し、一致する entry があれば、kfree() で解放して、list_del()でリストより削除しています。リストに追加する (list_add() する)側は kmalloc_parameter() です。この関数も param_set_charp() より呼ばれます。これらの実装より、module_param() に初期値として与えられた静的な領域は解放しない様になっています。
リスト操作は別のページで扱う予定です
リスト操作は排他制御を伴うのが普通です。一見した限りでは maybe_kfree_parameter() と kmalloc_parameter() には排他制御が見当たりません。大丈夫なのか「param_set_charp の排他制御は大丈夫?」探ってみました。
slab_is_available() とは何でしょうか?モジュールを Kernel に静的に結合すればモジュール・パラメータは kernel の boot command line からも与えることができます。一部の kernel の起動処理はメモリ・アロケータが初期化されていない状態で進みます。その特殊な状態を判断するための関数です。start_kernel(), parse_args(), parse_one(), param_sysfs_builtin() 辺りのコードが関係します。
kernel boot command line の module パラメータ書式
次の書式で Kernel の boot command line (あるいは kernel parameter、boot parameter とも呼ばれている) にモジュール・パラメータを含めることができます。
module.parameter_name=value
printk で構築できる文字列の長さは vprintk_emit() で 991 (== 1024 - 32 - 1) 文字に制限されています。kernel/printk/printk.c の中で LOG_LINE_MAX と PREFIX_MAX でマクロ定義された値に由来します。実装部分 (.c) で定義された値です。他から参照することはできません。
printk() から追いかけると printk_func, vprintk_default(), vprintk_emit(), vscnprintf() 引数に指定したバッファサイズ LOG_LINE_MAX まで辿れます。
このページのサンプルでは幅指定が無い %s 書式を使いました。vprintk_emit() にて制限されていることを前提としています。用心をするならば %s に幅指定をして下さい。printk に含める他の文字列の分を差し引いて、最大で 800 ~ 960 文字程度にします。x64 アーキテクチャのスタック・サイズ Documentation/x86/x86_64/kernel-stacks に有るように Kernel の中はスタック・サイズが小さいです。x64 アーキテクチャにて 8kibyte (== THREAD_SIZE == PAGE_SIZE * 2) です。Kernel 内の stack overflow をより慎重に避けるならば、数 10 文字程度に制限し、思わぬ攻撃を避けて下さい。
ここまでは、単純なモジュールの作り方を見てきました。次は少しだけデバイスドライバらしくする予定です。