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