NetBSD dtrace格闘記(道半ば)

NetBSD6.0はdtrace対応している。

手順は http://wiki.netbsd.org/how_to_enable_and_run_dtrace/ に書いてあったから簡単に行くと思いきや・・・。なかなか上手くいかないもので。

未だ格闘中で使えるようにはなっていないけれど、途中色々調べたことがあったので、長いけれどまとめてみることにします。

(1)build.shでdistributionを作るときの注意
http://wiki.netbsd.org/how_to_enable_and_run_dtrace/ によれば、「You also need to build distribution with the options MKMODULAR=yes and MKDTRACE=yes.」とある。この"option"の指定の仕方は-Vオプションで指定する。
つまり、

./build.sh -O ../obj -T ../tools -u -V MKMODULAR=yes -V MKDTRACE=yes distribution

のように。
(正確には"option"というよりも"make variables"らしい。これは/etc/mk.confでも指定が可能である。)

(2)新しいユーザランドをインストールするときの注意

build.shに-Uオプションをつけると
don't know how to make /usr/src../obj/destdir.i386/METALOG. Stop
と言われてしまうようなので、-Uオプションをつけない。
ただし、この場合、rootで作業する必要がある。

(3)モジュールがロードできない!

Running hello worldの項で

modload solaris
modload dtrace
modload sdt
modload fbt

とあるが、modload solaris以外は失敗してしまう。このときのエラーメッセージは「No such file or directory」である。

dtraceモジュールなどが生成されていないと思われるので、ルートディレクトリの下でfindコマンドを実行して探してみる。
すると、solarisモジュールは/stand/i386/6.0/modules/solarisに存在する(他にも存在するがいずれも/usr/srcもしくは/usr/objの下かもしくは明らかにmoduleでなさそうなファイルなので除外)に対し、dtraceはいずれも/usr/srcの下かもしくは/usr/objの下にしか存在しない。(sdt,fbtも同様)

しかも/stand/i386/6.0/modules/solarisの日付をlsコマンドで確かめると、./build.shで配布物を構築しているときに相当している。これはどういうことだろう。

ということはシステムにdtrace,sdt,fbtがインストールされていないようだ。

ここで二つの疑問が生じる。

疑問1.なぜ、これら3つのモジュールがシステムに反映されていないのか
疑問2.modloadが見に行くディレクトリはどうなっているのか
まずは疑問1について。まずは./build.shとタイプしてヘルプを見る。すると、Other operationsのところに install=idirとは別にinstallmodules=idirというオペレーションがある。
このことより、モジュールのインストールは別途行う必要があると思われる。
しかし、installmodulesに指定するディレクトリにつき、何が正解なのかわからない。

そこで、疑問2.を解決してみることにする。

modloadが見に行くディレクトリが分かれば、そのディレクトリをinstallmodulesオペレーションで指定することによって正しくモジュールがインストールできる見込みが立つからである。

疑問2を解消すべく、sbin/modloadのソースを読む。

まず、modloadのmain.cを追う。modload実行時は、オプションはつけていないので、output_propsはfalse。よって、else節が実行される。
else節にあるprog_modctl()でモジュールのロードをやっているのだろう。

prog_ops.hでprog_modctlの定義がある。
おそらくはCRUNCHOPSの定義はないので、op_modctlポインタ経由での呼び出しになる。
op_modctlを初期化している箇所は2箇所あるが、rumpはとりあえず関係なさそうなので、modctlが関数ポインタの実体となっていることが判断できる。

modctlについてgrepなどで調べた結果、どうやらシステムコールらしい事が分かる。

カーネルソースを引き続き読んでみる。sys_modctlではcmdを見てMODCTL_LOADだった場合はhandle_modctl_loadを呼び出す。
う〜ん、sbin/modloadのソースの時点から気になっていた「prop」がまだついて回っている。(sys_modctlとhandle_modctl_loadはkern/sys_module.c)

とりあえず先に進んで、module_loadを読んでみる。(module_loadはkern/kern_module.c)
module_loadでの処理本体はmodule_do_loadが担っている模様。

module_do_loadではmodule_builtinsリストを調べる。そしてロードしたいモジュールが見つかった場合はreturnしているように見える。

module_buildinsリストで見つからない場合、module_bootlistリストを調べる。
そして見つかったら、module_bootlistリストからpendingリストにモジュールを移し替える。
見つからない場合「if(isdep)」の節には入らずに(isdepはfalse)、module_newmodule関数がコールされる。(エラーだったときのメッセージから判断するに、これはモジュールをロードするための領域確保の関数ではないか?)
次にmodule_load_vfs_vecがコールされる。そして正常終了した段階でモジュールをpendingリストに加える。(pendingリストに加えられたモジュールはロードに成功した段階でリストから外される。)
以後の処理はロードしようとしているモジュールの妥当性の確認とロードそのものに関する処理のように見えるので、これらの処理を読んでも疑問は解消されないと思われる。よって、

(1)module_builtinsリストとは何者?
(2)module_bootlistリストとは何者?
(3)module_load_vfs_vec関数の処理内容

を追ってみることで、modloadがどこのディレクトリを探しにいくのかがわかるかな、と思う。

(1)module_builtinsリストについて
module_builtin_add()によってこのリストにエントリが追加される。
module_builtin_add()は(rumpを別にすれば)module_init()から呼ばれている。

module_init()では

__link_set_decl(modules, modinfo_t)

__link_set_foreach(mip,modules)

があり、このforeachの中でmodule_builtin_add()が呼ばれている。
__link_set_decl()の実体はsys/cdefs_elf.h内で以下のように定義されている。

#define	__link_set_decl(set, ptype)					\
	extern ptype * const __start_link_set_##set[];			\
	extern ptype * const __stop_link_set_##set[]			\

つまり、__link_set_decl(modules, modinfo_t)は

	extern modinfo_t * const __start_link_set_modules[];
	extern modinfo_t * const __stop_link_set_modules[]

と展開される。
__start_link_set_modulesおよび__stop_link_set_modulesについてカーネルソースを検索してみるが、rumpのldscript以外には存在しない。
試しにビルドを行ったNetBSD側のsysの下をgrepすると、netbsd.mapファイルに記述があるようだ。
netbsd.mapによると

link_set_modules
0xc0bbd8ac 0x1e4 load address 0x00bbd8ac
0xc0bbd8ac . = ALIGN (0x4)
0xc0bbd8ac PROVIDE (__start_link_set_modules, ABSOLUTE (.))

となっており、以後は

link_set_modules
0xc0bbd8ac 0x4 accf_data.o
link_set_modules
0xc0bbd8b0 0x4 accf_http.o
(略)
link_set_modules
0xc0bbda8c 0x4 xc5k.o
0xc0bbda90 PROVIDE (__stop_link_set_modules, .)

となっている。
試しにnetbsdに対してreadelf -Sを実行すると

[ 4] link_set_modules PROGBITS c0bbd8ac ...

という行が表示される。
これらの事から、module_builtinsリストには、link_set_modulesセクションの中身が順次突っ込まれると考えられる。
そこで、link_set_modulesセクションの中身について調べる。

link_set_modules
0xc0bbd8ac 0x4 accf_data.o

という記述があるので、試しにaccf_data.cを探してみて、その中に何が書かれているのかを見ることにする。
accf_data.cはnetinet/accf_data.cにある。中身はいくつかの関数と構造体定義があるのみである。
しかし、ソースの上の方に

MODULE(MODULE_CLASS_MISC, accf_dataready, NULL);

という行がある。
MODULEマクロはsys/module.hの中で以下のように定義されている。

 #define	MODULE(class, name, required)				\
static int name##_modcmd(modcmd_t, void *);			\
static const modinfo_t name##_modinfo = {			\
	.mi_version = __NetBSD_Version__,			\
	.mi_class = (class),					\
	.mi_modcmd = name##_modcmd,				\
	.mi_name = #name,					\
	.mi_required = (required)				\
}; 								\
__link_set_add_rodata(modules, name##_modinfo);

これは構造体の実体を定義するためのマクロであるが、末尾の__link_set_add_rodataが気になる。
これはsys/cdefs_elf.hで

#define	__link_set_add_rodata(set, sym)	__link_set_make_entry(set, sym)

と定義されている。__link_set_make_entryは同じくsys/cdefs_elf.hで

#define	__link_set_make_entry(set, sym)					\
	static void const * const __link_set_##set##_sym_##sym		\
	    __section("link_set_" #set) __used = &sym

と定義されている。
例えば「MODULE(MODULE_CLASS_MISC, accf_dataready, NULL);」
の場合、

__link_set_add_rodata(modules, accf_dataready_modinfo);
|
__link_set_make_entry(modules, accf_dataready_modinfo)
|
static void const * const __link_set_modules_sym_accf_dataready_modinfo \
__section("link_set_modules") __used = &accf_dataready_modinfo

のようにマクロ展開される。このことから、link_set_modulesセクションにはMODULEマクロで定義されたmodinfo_t型構造体のアドレスが格納される事が分かる。
おそらくシステムに静的に組み込まれているモジュールであろう。あとから動的に追加されるものではない。
よって、module_builtinsリストについてこれ以上追っても今回疑問に上がっている「modloadがどこのディレクトリを見にいくのか」についてはわからないということになる。

なので、次に「(2)module_bootlistリストとは何者?」について追ってみることにする。

(2)module_bootlistリストとは何者?
このリストにエントリを追加している関数はkern/kern_module.cのmodule_prime()である。
説明書きを見ると「ブートローダによってロードされたモジュールを内部リストに押し込む」とある。

arch/x86/x86/x86_machdep.cからのみ呼ばれている。中をみたがよくわからん。
ブートローダによってロードされた、と言っている時点で「modloadが何処のディレクトリを見にいくのか」についての疑問が解消する可能性が低そうである。
なので、(2)は置いておき、「(3)module_load_vfs_vec関数の処理内容」を追ってみることにする。

(3)module_load_vfs_vec関数の処理内容
module_load_vfs_vecは関数ポインタであり、module_load_vfs_init()でmodule_load_vfs()を指すように初期化される。(kern/kern_module_vfs.c)
module_load_vfs()はkern/kern_module_vfs.cにある関数である。

今回のような呼び出し経路の場合、autoloadはfalseで呼び出される。で、普通に「modload dtrace」とした場合「/」は含まないのでENOENTとなる。ENOENTの場合で指定したモジュール名に「/」を含まない場合、「module_baseで指定されるディレクトリ/モジュール名/モジュール名.kmod」というパスで示されるファイルをロードしようと試みる。
それでもダメならエラーで戻る。

となると、module_baseの内容が何か調べれば、今回の疑問が解消するかもしれない。
module_baseはどうやらkern/kern_module.cで宣言されているようだ。
この変数はmodule_init()で以下のように初期化されているようだ。

#if __NetBSD_Version__ / 1000000 % 100 == 99	/* -current */
	snprintf(module_base, sizeof(module_base), "/stand/%s/%s/modules",
	    module_machine, osrelease);
#else						/* release */
	snprintf(module_base, sizeof(module_base), "/stand/%s/%d.%d/modules",
	    module_machine, __NetBSD_Version__ / 100000000,
	    __NetBSD_Version__ / 1000000 % 100);
#endif

module_machineは何か?module_init()では

	if (!module_machine)
		module_machine = machine;

となっている。
今回のi386では特に初期化がされていないので、machineの内容がmodule_machineになる。
grepなどで追うのはかなり面倒(machineというキーワードに大量に引っかかるため)なので、実際のNetBSD環境の/standの下を見る。
すると、「i386」というディレクトリのみがあった。よって、machineの内容は「i386」だと思われる。
今回NetBSDのバージョンは6.0なので、最終的なmodule_baseは「/stand/i386/6.0/modules」となる。
実際のNetBSD環境と比較しても一致しているため、これで間違いが無いと思われる。

よって、modloadがモジュールの有無を見に行くディレクトリは「/stand/i386/6.0/modules」であると言えそうだ。

今日はここまで。次回は得られたパスを元にbuild.shのinstallmodulesを実施する事にする。