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 にあります。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
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 の大部分に対応するプリプロセス結果とアセンブリ出力です。

$ 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 は有力な手掛かりです。

file.tmp_i8253_ref.s
168
169
170
171
172
173
174
175
176
177
178
179
180
181
        .file 2 "./arch/x86/include/asm/io.h"
        .loc 2 313 0
#APP
# 313 "./arch/x86/include/asm/io.h" 1
        outb %al, %dx
# 0 "" 2
#NO_APP
.LBE109:
.LBE108:
.LBB110:
.LBB111:
        .file 3 "./arch/x86/include/asm/paravirt.h"
        .loc 3 310 0
        call    *pv_cpu_ops+208(%rip)

/arch/x86/include/asm/io.hを読んでみましょう。BUILDIO マクロで作られたテンプレートが見つかります。

fileOpenGrok(linux-4.1.27/arch/x86/include/asm/io.h:273)
Expand allFold all
273
274
275
276
277
278
 
 
-
|
|
!
#define BUILDIO(bwl, bw, type)                                          \
static inline void out##bwl(unsigned type value, int port)              \
{                                                                       \
        asm volatile("out" #bwl " %" #bw "0, %w1"                       \
                     : : "a"(value), "Nd"(port));                       \
}                                                                       \

out##bwl##_p が outb_p() inline 関数を定義しているのが見えてきます。slow_down_io() は名前からして、IO の処理に待ちを加えている様に見えます。

fileOpenGrok(linux-4.1.27/arch/x86/include/asm/io.h:288)
Expand allFold all
288
289
290
291
292
 
-
|
|
!
static inline void out##bwl##_p(unsigned type value, int port)          \
{                                                                       \
        out##bwl(value, port);                                          \
        slow_down_io();                                                 \
}                                                                       \

BUILDIO マクロを使っているところも見つかります。マクロ引数として bwl=bbw=btypw=byte を渡している行があるので、これで outb_p() を定義しています。

fileOpenGrok(linux-4.1.27/arch/x86/include/asm/io.h:313)
Expand allFold all
313
314
315
 
 
 
BUILDIO(b, b, char)
BUILDIO(w, w, short)
BUILDIO(l, , int)

slow_down_io()を見ていきます。pv_cpu_ops に繋がるか辿ります。CONFIG_PARAVIRT は定義されているので、 /arch/x86/include/asm/paravirt.h を include しています。先のアセンブリ言語出力に入ったコメントとも一致します。

fileOpenGrok(linux-4.1.27/arch/x86/include/asm/io.h:257)
Expand allFold all
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
 
 
 
 
 
-
|
|
|
|
|
|
!
 
 
#if defined(CONFIG_PARAVIRT)
#include <asm/paravirt.h>
#else
 
static inline void slow_down_io(void)
{
        native_io_delay();
#ifdef REALLY_SLOW_IO
        native_io_delay();
        native_io_delay();
        native_io_delay();
#endif
}
 
#endif

/arch/x86/include/asm/paravirt.hslow_down_io() の実装の中に pv_cpu_ops が使われています。先のアセンブリ言語出力の内容の通りです。

fileOpenGrok(linux-4.1.27/arch/x86/include/asm/paravirt.h:308)
Expand allFold all
308
309
310
311
312
313
314
315
316
 
-
|
|
|
|
|
|
!
static inline void slow_down_io(void)
{
        pv_cpu_ops.io_delay();
#ifdef REALLY_SLOW_IO
        pv_cpu_ops.io_delay();
        pv_cpu_ops.io_delay();
        pv_cpu_ops.io_delay();
#endif
}

io_delay の実装も追ってみます。pv_cpu_ops の中で native_io_delay() を指すように実装されています。

fileOpenGrok(linux-4.1.27/arch/x86/kernel/paravirt.c:395)
Expand allFold all
395
 
        .io_delay = native_io_delay,

native_io_delay() は時間待ち処理です。io_delay_typeCONFIG_IO_DELAY_TYPE_NONE 以外であれば、何も起きない(厳密にいえばバスに OUT トランザクションだけが発生する) IO ポート操作か、udelay() を呼ぶ実装になっています。

fileOpenGrok(linux-4.1.27/arch/x86/kernel/io_delay.c:20)
Expand allFold all
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 
 
 
-
-
|
|
|
|
|
|
|
|
-
|
|
|
|
|
!
|
|
|
!
!
 * Paravirt wants native_io_delay to be a constant.
 */
void native_io_delay(void)
{
        switch (io_delay_type) {
        default:
        case CONFIG_IO_DELAY_TYPE_0X80:
                asm volatile ("outb %al, $0x80");
                break;
        case CONFIG_IO_DELAY_TYPE_0XED:
                asm volatile ("outb %al, $0xed");
                break;
        case CONFIG_IO_DELAY_TYPE_UDELAY:
                /*
                 * 2 usecs is an upper-bound for the outb delay but
                 * note that udelay doesn't have the bus-level
                 * side-effects that outb does, nor does udelay() have
                 * precise timings during very early bootup (the delays
                 * are shorter until calibrated):
                 */
                udelay(2);
        case CONFIG_IO_DELAY_TYPE_NONE:
                break;
        }
}

ここまで辿り、outb_p() の実装を確認しました。途中マクロによるテンプレート展開があると追跡が困難になります。困難な場合、アセンブリ言語出力を軽く読むだけで追跡のヒントは多く得られます。

逆アセンブル

どの様な実装になっているかよくわからない場合、もう一つの方法は逆アセンブルです。

逆アセンブル出力を得る objdump -d

ソースコード i8253_ref.c をコンパイルして得られた i8253_ref.ko ファイルを逆アセンブルします。逆アセンブルのコマンドラインは objdump -d i8253_ref.ko です。

逆アセンブル出力の中から i8253_ref_counter_read() 部分に注目します。

filei8253_ref.disasm.txt
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
00000000000000a0 <i8253_ref_counter_read>:
  a0:   e8 00 00 00 00          callq  a5 <i8253_ref_counter_read+0x5>
  a5:   55                      push   %rbp
  a6:   48 89 e5                mov    %rsp,%rbp
  a9:   41 55                   push   %r13
  ab:   41 54                   push   %r12
  ad:   53                      push   %rbx
  ae:   48 89 fb                mov    %rdi,%rbx
  b1:   48 c7 c7 00 00 00 00    mov    $0x0,%rdi
  b8:   48 83 ec 08             sub    $0x8,%rsp
  bc:   e8 00 00 00 00          callq  c1 <i8253_ref_counter_read+0x21>
  c1:   49 89 c5                mov    %rax,%r13
  c4:   0f b6 43 18             movzbl 0x18(%rbx),%eax
  c8:   48 8b 53 10             mov    0x10(%rbx),%rdx
  cc:   c1 e0 06                shl    $0x6,%eax
  cf:   83 c8 04                or     $0x4,%eax
  d2:   ee                      out    %al,(%dx)
  d3:   ff 15 00 00 00 00       callq  *0x0(%rip)        # d9 <i8253_ref_counter_read+0x39>
  d9:   48 8b 53 08             mov    0x8(%rbx),%rdx
  dd:   ec                      in     (%dx),%al
  de:   44 0f b6 e0             movzbl %al,%r12d
  e2:   ff 15 00 00 00 00       callq  *0x0(%rip)        # e8 <i8253_ref_counter_read+0x48>
  e8:   48 8b 53 08             mov    0x8(%rbx),%rdx
  ec:   ec                      in     (%dx),%al
  ed:   48 c7 c7 00 00 00 00    mov    $0x0,%rdi
  f4:   89 c3                   mov    %eax,%ebx
  f6:   4c 89 ee                mov    %r13,%rsi
  f9:   e8 00 00 00 00          callq  fe <i8253_ref_counter_read+0x5e>
  fe:   48 83 c4 08             add    $0x8,%rsp
 102:   89 d8                   mov    %ebx,%eax
 104:   c1 e0 08                shl    $0x8,%eax
 107:   5b                      pop    %rbx
 108:   44 09 e0                or     %r12d,%eax
 10b:   41 5c                   pop    %r12
 10d:   41 5d                   pop    %r13
 10f:   5d                      pop    %rbp
 110:   c3                      retq   
 111:   66 66 66 66 66 66 2e    data32 data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
 118:   0f 1f 84 00 00 00 00 
 11f:   00 

この中で outb_p() に渡す引数の計算も含めて outb_p() に相当する命令列は次の場所です。out 命令が生成されていることは分かりました。続く callq *0x0(%rip)は奇妙な命令です。コメントに有るように次に続く命令の並びを分岐先アドレスと見なして call している様に見えます(C 言語風に書くと (*(rip+0))(); の様な動作をする)。i8253_ref_counter_read() 関数にこのような動作をする記述は見つかりません。

filei8253_ref.disasm.txt
 66
 67
 68
 69
 70
 71
  c4:   0f b6 43 18             movzbl 0x18(%rbx),%eax
  c8:   48 8b 53 10             mov    0x10(%rbx),%rdx
  cc:   c1 e0 06                shl    $0x6,%eax
  cf:   83 c8 04                or     $0x4,%eax
  d2:   ee                      out    %al,(%dx)
  d3:   ff 15 00 00 00 00       callq  *0x0(%rip)        # d9 <i8253_ref_counter_read+0x39>

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 だろうという予測も付きます。

filei8253_ref.reloc.txt
 17
 18
 19
 20
 21
 22
00000000000000b4 R_X86_64_32S      i8253_lock
00000000000000bd R_X86_64_PC32     _raw_spin_lock_irqsave-0x0000000000000004
00000000000000d5 R_X86_64_PC32     pv_cpu_ops+0x00000000000000cc
00000000000000e4 R_X86_64_PC32     pv_cpu_ops+0x00000000000000cc
00000000000000f0 R_X86_64_32S      i8253_lock
00000000000000fa R_X86_64_PC32     _raw_spin_unlock_irqrestore-0x0000000000000004

以降は、コンパイル途中で生成される一時ファイルを保存する の後半で説明している追跡経過とほぼ同様にソースコードを探す事ができます。


添付ファイル: filei8253_ref.reloc.txt 252件 [詳細] file.tmp_i8253_ref.s 248件 [詳細] filei8253_ref.disasm.txt 276件 [詳細] file.tmp_i8253_ref.i 715件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-09-27 (水) 11:03:48 (2406d)