outb_p() の実装はどこにあるの? †i8253 PIT をアクセスするデバイスドライバ のモジュール i8253_ref.c で使われていた outb_p() のリンク先を辿っても、待ち処理が入った IO ポート出力処理は見つかりません。x86 と x86-64 アーキテクチャの場合はどこに実装されているのか、探してみます。 探し方を 2 つ挙げます。はじめは高度な判断を必要としない「コンパイル途中で生成される一時ファイルを保存する」を行ってみることをお勧めします。 コンパイル途中で生成される一時ファイルを保存する †ソースファイルを gcc でコンパイルするとオブジェクト・ファイルを生成します。gcc に -save-temps=obj オプションを付加するとプリプロセス結果を格納したファイル .i と アセンブリ言語で書かれたソース .s ファイルを生成することができます。gcc がどのようにソースを解釈したか知ることができるファイルです。 Linux kernel の Makefile は通常 .i, .s ファイルはを生成しません。.i, .s ファイルを生成するとディスク残容量を大きく減らし、構築時間が長くなります。「何も起きていないとき」は生成しないほうが作業の進みが速くなります。 Makefile に -save-temps=obj を加える †i8253_ref.ko を構築する Makefile i8253_ref-module-Makefile を修正して、.i, .s ファイルを生成するようにします。 make 変数 ccflags-y に gcc のオプションに付加したい -save-temps=obj を設定します。Linux kernel ソースツリーへ応用する場合も ccflags-y+= -save-temps=obj のように少しの応用で適応できるでしょう。 ccflags-y のドキュメント ccflags-y の他にas に渡すオプション asflags-y, ld に渡すオプション ldflags-y という make 変数があります。これらの説明は Documentation/kbuild/makefiles.txt にあります。
モジュールを再構築します。いくつかの .i, .s ファイルができます。.tmp_i8253_ref.i と .tmp_i8253_ref.s が i8253_ref.c の大部分に対応するプリプロセス結果とアセンブリ出力です。 $ make clean make -C /lib/modules/4.1.27-local+/build M=/home/furuta/work/lkdq1stpfdriver clean make[1]: Entering directory `/home/furuta/git/linux-stable' CLEAN /home/furuta/work/lkdq1stpfdriver/.tmp_versions CLEAN /home/furuta/work/lkdq1stpfdriver/Module.symvers make[1]: Leaving directory `/home/furuta/git/linux-stable' $ make make -C /lib/modules/4.1.27-local+/build M=/home/furuta/work/lkdq1stpfdriver modules make[1]: Entering directory `/home/furuta/git/linux-stable' CC [M] /home/furuta/work/lkdq1stpfdriver/i8253_ref.o gcc: warning: -pipe ignored because -save-temps specified Building modules, stage 2. MODPOST 1 modules CC /home/furuta/work/lkdq1stpfdriver/i8253_ref.mod.o gcc: warning: -pipe ignored because -save-temps specified LD [M] /home/furuta/work/lkdq1stpfdriver/i8253_ref.ko make[1]: Leaving directory `/home/furuta/git/linux-stable' $ ls -1as --block=k total 2224K 4K . 4K .. 4K .git 4K .i8253_ref.ko.cmd 28K .i8253_ref.mod.o.cmd 28K .i8253_ref.o.cmd 700K .tmp_i8253_ref.i 272K .tmp_i8253_ref.s 4K .tmp_versions 4K Makefile 0K Module.symvers 12K i8253_ref.c 172K i8253_ref.ko 4K i8253_ref.mod.c 636K i8253_ref.mod.i 64K i8253_ref.mod.o 168K i8253_ref.mod.s 112K i8253_ref.o 4K modules.order アセンブリ言語出力を読む †次のテキストは i8253_ref_counter_read() の中で outb_p() に相当する部分です。outb 命令が見つかったということは「inline で実装された関数」、「マクロで実装され展開された」、「コンパイラの組み込み関数が展開された」かです。つづいて pv_cpu_ops 構造体メンバーが指す先を間接 call している命令が見つかります。i8253_ref_counter_read() のコードから容易に見当がつかない命令と call 先です。 コメントに散見される ./arch/x86/include/asm/io.h、./arch/x86/include/asm/paravirt.h は有力な手掛かりです。 .tmp_i8253_ref.s
/arch/x86/include/asm/io.hを読んでみましょう。BUILDIO マクロで作られたテンプレートが見つかります。 OpenGrok(linux-4.1.27/arch/x86/include/asm/io.h:273)
out##bwl##_p が outb_p() inline 関数を定義しているのが見えてきます。slow_down_io() は名前からして、IO の処理に待ちを加えている様に見えます。 OpenGrok(linux-4.1.27/arch/x86/include/asm/io.h:288)
BUILDIO マクロを使っているところも見つかります。マクロ引数として bwl=b、bw=b、typw=byte を渡している行があるので、これで outb_p() を定義しています。 OpenGrok(linux-4.1.27/arch/x86/include/asm/io.h:313)
slow_down_io()を見ていきます。pv_cpu_ops に繋がるか辿ります。CONFIG_PARAVIRT は定義されているので、 /arch/x86/include/asm/paravirt.h を include しています。先のアセンブリ言語出力に入ったコメントとも一致します。 OpenGrok(linux-4.1.27/arch/x86/include/asm/io.h:257)
/arch/x86/include/asm/paravirt.h の slow_down_io() の実装の中に pv_cpu_ops が使われています。先のアセンブリ言語出力の内容の通りです。 OpenGrok(linux-4.1.27/arch/x86/include/asm/paravirt.h:308)
io_delay の実装も追ってみます。pv_cpu_ops の中で native_io_delay() を指すように実装されています。 OpenGrok(linux-4.1.27/arch/x86/kernel/paravirt.c:395)
native_io_delay() は時間待ち処理です。io_delay_type が CONFIG_IO_DELAY_TYPE_NONE 以外であれば、何も起きない(厳密にいえばバスに OUT トランザクションだけが発生する) IO ポート操作か、udelay() を呼ぶ実装になっています。 OpenGrok(linux-4.1.27/arch/x86/kernel/io_delay.c:20)
ここまで辿り、outb_p() の実装を確認しました。途中マクロによるテンプレート展開があると追跡が困難になります。困難な場合、アセンブリ言語出力を軽く読むだけで追跡のヒントは多く得られます。 逆アセンブル †どの様な実装になっているかよくわからない場合、もう一つの方法は逆アセンブルです。 逆アセンブル出力を得る objdump -d †ソースコード i8253_ref.c をコンパイルして得られた i8253_ref.ko ファイルを逆アセンブルします。逆アセンブルのコマンドラインは objdump -d i8253_ref.ko です。 逆アセンブル出力の中から i8253_ref_counter_read() 部分に注目します。 i8253_ref.disasm.txt
この中で outb_p() に渡す引数の計算も含めて outb_p() に相当する命令列は次の場所です。out 命令が生成されていることは分かりました。続く callq *0x0(%rip)は奇妙な命令です。コメントに有るように次に続く命令の並びを分岐先アドレスと見なして call している様に見えます(C 言語風に書くと (*(rip+0))(); の様な動作をする)。i8253_ref_counter_read() 関数にこのような動作をする記述は見つかりません。 i8253_ref.disasm.txt
callq 命令のバイト列をみると ff 15 00 00 00 00 となっています。00 が 4 バイト続いています。何か情報が欠落している感があります。00 が続く部分はメモリにロードされた後、relocate (あるいは fixup) される場所です。.ko ファイルにはこの部分を「ロード時に確定させて下さい」という情報が含まれています。 00 が 4 バイト続く場所を計算します。逆アセンブルリストのアドレス部分は d3: です。これに ff 15 の分を足すと、0xd5 になります。アドレス 0xd5 に対して「ロード時に確定させて下さい」と言う情報を探します。 relocation 情報を得る objdump -r †objdump -r i8253_ref.ko で relocation table を出力します。この中で d5 と言う文字列を検索します。00000000000000d5 R_X86_64_PC32 pv_cpu_ops+0x00000000000000cc が見つかりました。 前後の行も含めてみると次の様になっています。前後の並びが、i8253_ref_counter_read() で使っているグローバル変数、関数の並びに良く一致しています。pv_cpu_ops を手かがりにソースコードを探せば辿れそうなことが分かります。pv_cpu_ops+0xcc なので、構造体の先頭からオフセット 0xcc にあるメンバが関数ポインタになっていて、間接 call だろうという予測も付きます。 i8253_ref.reloc.txt
以降は、コンパイル途中で生成される一時ファイルを保存する の後半で説明している追跡経過とほぼ同様にソースコードを探す事ができます。 |