Linuxでモジュールパラメータの値を確認する方法 (+ live kernel debugについて)

本内容はx86_64 Fedora36(kernel 6.0)で動作確認をしています。

# 基本的な解決策
builtinモジュールまたはロードされているloadableモジュールについて、/sys/module/\<module_name>/parametersを見れば存在するパラメータと現在の値を確認できます。

```sh
$ grep "" -r /sys/module/ipv6/parameters/
/sys/module/ipv6/parameters/disable:0
/sys/module/ipv6/parameters/disable_ipv6:0
/sys/module/ipv6/parameters/autoconf:1
```

なお動的に変更可能なパラメータについてはファイルにwrite権が付いているので値を書き込むことで変更できます(通常root権限が必要になるでしょう)。

## 補足1: モジュールパラメータの意味が知りたい
各モジュールパラメータの意味はmodinfoコマンドで確認できます。

```sh
$ modinfo -p ipv6 # -p ... パラメータ情報のみ表示
disable:Disable IPv6 module such that it is non-functional (int)
disable_ipv6:Disable IPv6 on all interfaces (int)
autoconf:Enable IPv6 address autoconfiguration on all interfaces (int)
```

## 補足2：そもそも何のモジュールがあるのか知りたい

/lib/modules/\`(uname -r)\`以下にモジュール本体や関連する情報のファイルが置かれています。コンパイルされているモジュールについて、builtinモジュールはmodules.builtin,loadableモジュールはmodules.orderを見れば確認できます。

```sh
$ cat /lib/modules/`(uname -r)`/modules.builtin | head -n 3
kernel/arch/x86/events/amd/amd-uncore.ko
kernel/arch/x86/kernel/msr.ko
kernel/arch/x86/kernel/cpuid.ko

$ cat /lib/modules/`(uname -r)`/modules.order | head -n 3
kernel/arch/x86/events/amd/power.ko
kernel/arch/x86/events/intel/intel-uncore.ko
kernel/arch/x86/events/intel/intel-cstate.ko
```

現在ロードされているloadbleモジュールはlsmodコマンドで確認します。
```sh
$ lsmod | head -n 3
Module                  Size  Used by
tls                   126976  0
xfs                  2117632  1
```
なおbuiltinモジュールはこの中にはでてきません。

## 補足3: ブートオプションや設定ファイルで指定されているオプションを知りたい
モジュールパラメータの値をデフォルトから変更する場合は以下の方法を利用します。

1. kernelブートオプションに"module_name.parameter=value"の形で指定する
2. modprobe.confの設定ファイル(/etc/modprobe.d/XXX.conf他)で指定する
3. modprobeの引数としてロード時に明示的に指定する

1と2について実際にどのような指定がされているのかを手動で確かめるのは大変ですが、modprobeの-cオプションを利用することで機械的に設定を確認できます。

```sh
$ modprobe -c | grep -e "^options" # alias等の情報も出てくるのでここではoptionだけgrep
options b43 nohwcrypt=1 qos=0
options bonding max_bonds=0
options dummy numdummies=0
options vhost max_mem_regions=509
````
ここでは例えばvhostモジュールがロードされる時、自動で```max_mem_regions=509```が指定されます。実際にこのシステムで確認してみると、modinfoによるとデフォルト値は64ですがロードされたパラメータ値は509になっています。
```sh
$ modinfo -p vhost
max_mem_regions:Maximum number of memory regions in memory map. (default: 64) (ushort)
max_iotlb_entries:Maximum number of iotlb entries. (default: 2048) (int)

$ sudo modprobe vhost
$ grep "" -r /sys/module/vhost/parameters/
/sys/module/vhost/parameters/max_iotlb_entries:2048
/sys/module/vhost/parameters/max_mem_regions:509
```

modprobe.confについての詳細は ```man modprobe.conf``` を見てください。

## 補足4: デフォルトのモジュールパラメータの値を確認したい
パラメータのデフォルト値を確認する統一的な方法はありません（モジュールによってはmodinfoの説明の中に書いてあります）。これはモジュールによってはシステムの構成等から初期化時にパラメータの値を決定している場合もあるからだと思います。どうしても知りたい時はモジュールのソースコードを見れば大体はどこかにまとまって記述されています。

# /sys/module/\<module_name\>/parameters以下にファイルがないとき

ここからおまけ(あるいは本題)です。

モジュールによってはparameters以下にパラメータのファイルが作られないことがあります。
例えば単に目に付いたのでorangefsを例にとると、modinfoからは4つのパラメータを確認できます。
```sh
$ modinfo -p orangefs
hash_table_size:size of hash table for operations in progress (int)
module_parm_debug_mask:debugging level (see orangefs-debug.h for values) (ulong)
op_timeout_secs:Operation timeout in seconds (int)
slot_timeout_secs:Slot timeout in seconds (int)
```

しかしモジュールをロードしてみるとそのうちの1つしかparameters以下に存在していません。
```sh
$ sudo modprobe orangefs
$ grep "" -r /sys/module/orangefs/parameters/
/sys/module/orangefs/parameters/module_parm_debug_mask:0
```

これはなぜでしょうか？

簡単に答えるとパラメータをsysfsに表示するかどうかはコードで決定できるからです。モジュールパラメータはソースコードではmodule_parm()マクロ(およびその亜種)で定義されます。ここでmodule_paramに渡している最後の値が作成されるファイルのパーミッションですが、これに0を指定するとそもそもsysfsファイルが作られません。よってこの場合は動作中のパラメータの値を確認することは不可能になります。実際以下のようにsysfsから見えないパラメータには0が渡されており、その場合はsysfsのエントリを作る処理がスキップされます。
```c
/* SPDX-License-Identifier: GPL-2.0 */
// include/linux/moduleparam.h
126 #define module_param(name, type, perm)        \
127   module_param_named(name, name, type, perm)

// linux/fs/orangefs/orangefs-mod.c
54 module_param(hash_table_size, int, 0);
55 module_param(module_parm_debug_mask, ulong, 0644);
56 module_param(op_timeout_secs, int, 0);
57 module_param(slot_timeout_secs, int, 0);

// linux/paramas.c
697 /*
698  * module_param_sysfs_setup - setup sysfs support for one module
...
703  * Adds sysfs entries for module parameters under
704  * /sys/module/[mod->name]/parameters/
705  */
706 int module_param_sysfs_setup(struct module *mod,
707            const struct kernel_param *kparam,
708            unsigned int num_params)
709 {
710   int i, err;
711   bool params = false;
712
713   for (i = 0; i < num_params; i++) {
714     if (kparam[i].perm == 0)
715       continue; // permが0だとskipされる
716     err = add_sysfs_param(&mod->mkobj, &kparam[i], kparam[i].name);
...
// (補足: 上はloadableモジュール用で、builtinモジュールについてはparam_sysfs_builtinで作成しています)
```

**さてパーミッションが0になっていることはユーザーがその値を知る必要はない(はず)ということですが、せっかくなので値を確認する方法がないか考えみます。**

そもそもモジュールパラメータがプログラムの中でどう表現されているかというと、コードを見るとすぐに分かりますが基本的には同じ名前のグローバル変数になっています。グローバル変数ということはメモリ上にロードされているプログラムの中に値が存在しています。そしてkernel(vmlinux)のメモリは/proc/kcoreという仮想ファイルにマッピングされています。ということで/proc/kcoreの中を探せば見えていないモジュールパラメータの値も分かるはずです。

## live kernel debugで/proc/kcoreを読む
/proc/kcoreの中身を読むためにlive kernel debugの手法を使います。これには古くからはgdbベースのcrash, 最近ではdrgnなどのツールが利用できます。crashはkernelがpanicした際に作成されるcrash dumpを解析するために使われることが多いと思いますが、live systemの/proc/kcoreに対しても同様の解析を行うことできます(当然ながらroot権限が必要かつread-olnly)。いろいろとできることはありますがグローバル変数のlookupは簡単に実行できることの一つです。

なおこれらのツールを使用するためにはツール自身に加えてkernel debuginfoのインストールが必要です。メジャーなディストリビューションならばすぐにインストールできると思います。Fedoraなら以下のようになるでしょう。

```sh
$ sudo dnf install -y crash drgn kernel-debuginfo
```

crashは何も引数を指定せずに実行すると自動で/proc/kcoreを見に行きます。対話型のプロンプトが立ち上がるのでそこでコマンドを実行します。pコマンドを使用するとシンボルの名前(ここではつまりグローバル変数の名前)を用いてその値をlookupすることができます。

```sh
$ sudo modprobe orangefs
$ sudo crash
# 立ち上がったらpコマンドでグローバル変数(ここではモジュールパラメータ)の値を読む
crash> p hash_table_size
hash_table_size = $1 = 509
crash> p module_parm_debug_mask
module_parm_debug_mask = $2 = 0
crash> p op_timeout_secs
op_timeout_secs = $3 = 20
crash> p slot_timeout_secs
slot_timeout_secs = $4 = 900

crash> q # 終了

# パラメータの値を変えてロードし直して確認してみる
$ modprobe -r orangefs
$ modprobe orangefs slot_timeout_secs=777
$ sudo crash
crash> p slot_timeout_secs
slot_timeout_secs = $1 = 777
```
なおpコマンドの際に'p: gdb request failed:'`というエラーが発生した時はモジュールのデバッグ情報が読み込まれていないので、mod -Sコマンドを使用してデバッグ情報をロードしてください。
```sh
crash> mod -S /usr/lib/debug/lib/modules/6.0.5-200.fc36.x86_64/kernel # 実行中のkernelのデバッグ情報へのパス
```

drgnの場合も同様で何も引数を指定しないと/proc/kcoreが選択され対話型のプロンプト(python intepreter)が立ち上がります。コマンドの与え方は以下のようになります。
```sh
sudo drgn
>>> prog['hash_table_size']
(int)509
>>> prog['module_parm_debug_mask']
(ulong)0
>>> prog['op_timeout_secs']
(int)20
>>> prog['slot_timeout_secs']
(int)900

>>> quit() # 終了
```

ということでsysfsに表示されていなくてもモジュールパラメータの値を確認することができました。crashやdrgnの詳細については以下のURLに示すそれぞれのヘルプページを参照してください。
- crash: https://crash-utility.github.io/help.html
- drgn: https://drgn.readthedocs.io/en/latest/user_guide.html


## シンボル名が重複するとき
以上のようにグローバル変数のlookupは簡単なのですが、ここで1つ問題が起こる可能性があります。

グローバル変数はstaticである可能性があります。Cでいうstatic globalはそのファイルの中でしか使われない変数という意味です。つまりkernel全体としては同じ名前のシンボル名=グローバル変数が複数存在する可能性があります。

実際にcdromドライバの例を見てみると(こちらも全てのパラメータがsysfsには表示されません)、パラメータの中にdebugといういかにも怪しい名前のものがあります。
```sh
$ modinfo -p cdrom
debug: (bool)
autoclose: (bool)
autoeject: (bool)
lockdoor: (bool)
check_media_type: (bool)
mrw_format_restart: (bool)
```

crashでプログラム全体のシンボルを検索するsymコマンドを利用すると"debug"という名前の変数が複数あることがわかります。このときもpコマンドで名前によりlookupはできますが、 crash(というよりgdb)ではシンボル名が重複すると一致するシンボルのいずれかの値を返すため目的の変数かどうかわかりません。

```sh
crash> sym debug
# debugというシンボル名(変数名)が複数存在している
ffffffffba279ae8 (d) debug
ffffffffbaa2bb04 (d) debug
ffffffffbadacb1d (b) debug
ffffffffbadc83de (b) debug
ffffffffc0924270 (b) debug [igb]

# シンボル名が重複していてもlookupは可能だがどのロケーションを見ているか不明
crash> p debug
debug = $4 = false
```

この問題を解決するためにはlookupの際にソースコードディレクトリにおけるファイルのパス名を指定します(これはgdbの機能です)。
```sh
crash> p 'drivers/cdrom/cdrom.c'::debug
$6 = false
crash> p 'cdrom.c'::debug
$7 = false
```
上の通りファイルのパス名は(ビルドのトップディレクトリからの)フルパスである必要はありませんが、当然ながらパスの部分も重複している場合はシンボルが一つに絞れませんので注意が必要です。

drgnの場合も同じようにファイルのパスを指定してlookupすることができます。
```sh
# filename引数でパスを指定する
>>> prog.variable('debug', filename='drivers/cdrom/cdrom.c')
(bool)0
>>> prog.variable('debug', filename='cdrom.c')
(bool)0
```

## スクリプトにする
ここまできたのでついでにモジュール名を指定するとsysfsに表示されないものも含めてパラメータの一覧と値を出力するスクリプトにしてみます。なおdrgnはpythonに統合されておりスクリプトにしやすいのでここではdrgnを使います。

素朴に考えるとmodinfoの情報からパラメータ名を取得してその名前でlookupしていけばよいのですが、実はmodinfoから見えるパラメータ名とグローバル変数名が異なっている場合があります(module_param_named()マクロを使うとパラメータ名を変えられる)。

もう少し打率が上がりそうな方法がないかソースコードを眺めていると以下の事がわかります。
- loadableモジュールを表現する構造体(struct module)はグローバル変数```module```を起点とするリストにつながれる
- struct moduleの中にはパラメータの情報を保持するstruct kernel_paramの配列が存在する
- 各struct kernel_paramはパラメータの名前とその変数へのアドレスの情報を持つ
- builtinモジュールについてはパラメータの情報がvmlinuxの___start_param ~ ___stop_paramセクションの中にコピーされている

よって

 - loadableモジュール: moduleリストをたどって各moduleのstruct moduleからstruct kernel_paramの情報を取得する
 - bultinモジュール: ___start_param ~ ___stop_paramセクションの中を見てstruct kernel_paramの情報を取得する

ということをすればパラメータの情報が得られそうです。kernel_parmの情報を見るとアドレスが分かるので直接その場所をreadすることもできますが、シンボル名でlookupするといい感じに型のハンドリングをしてくれるので基本的にはシンボル名でlookupするようにします。シンボル名が重複した場合は"module_name.c"がファイル名であると仮定してlookupを試み、それでも駄目ならアドレスからreadします(このあたりを真面目にやるのが大変なので手を抜いています)。

以上を踏まえてスクリプトにするとこんな感じになります: https://github.com/t-msn/list_modparam/blob/main/list_modparam.py

これを使用すると以下になります(ここではorangefsがloadableモジュール、 cdromがbuiltinモジュールです):
```sh
$ sudo modprobe orangefs slot_timeout_secs=777 op_timeout_secs=33
$ sudo ./list_modparam.py orangefs cdrom
orangefs
        slot_timeout_secs = (int)777
        op_timeout_secs = (int)33
        module_parm_debug_mask = (ulong)0
        hash_table_size = (int)509
cdrom
        mrw_format_restart = (bool)1
        check_media_type = (bool)0
        lockdoor = (bool)1
        autoeject = (bool)0
        autoclose = (bool)1
        debug = (bool)0 **symbol name duplicates, value might be incorrect**
```

一応補足するシンボル名が重複した際のハンドリングが正確でない可能性があったり、パラメータが配列になっていたりする場合(カンマ区切りで指定する)などに対応していませんのでPoCだと思ってください。またsysfs以下に出力されるパラメータの値は変数の値そのものではないことがあったり(例えば文字列に変換される)、対応する変数が無かったり場合もあるのでこの出力と一致しないことがあります。そもそもこのようなことをする必要が生じた時はソースコードを読んでいるはずなのでそちらと突き合せて確認した方が良いでしょう。

# おわりに
モジュールパラメータの値を確認していただけなのに気が付いたらlive kernel debugをしていました。今回の用途ではoverkillだと思いますが、live kernel debugではモジュールパラメータに限らずglobal変数をlookupできるので色々と使えることがあると思います。またもし実際にsysfsに表示されないモジュールパラメータの値を知る必要がある場面に遭遇したことのある人がいたら教えてください。