printk()で%pS format string指定時のアドレス->シンボル名の探し方

Linuxカーネルのprintk()はformat stringが色々と拡張されていて(Documentation/printk-formats.txt)、例えば、IPv4/IPv6の表示用、UUID/GUIDの表示用などがあります。その中でsymbol関連のformat stringもいくつか合って、%pSの場合はこんなふうに呼び出し元関数、呼び出し位置/関数サイズというように表示できます。OOPSでのスタックトレースと同様ですね。

pr_info("%pS\n", __builtin_return_address(1));

出力結果はこのように。

[   53.483834] load_module+0x1dcc/0x25f0

これはkallsymsの機能を使っているのだけど、どのようにやっているのかを見るのが今回の目的。 基本的にはlib/vsprintf.cのvsprintf()から関数が呼ばれていって、symbol_string()からkernel/kallsyms.cにあるsprint_symbol()が呼ばれ、あとはkallsyms.cの関数が使われていく。

symbolのlookupはkallsyms_lookup()が使われる。関数のサイズやoffset等もここで取っている模様。

355 /* Look up a kernel symbol and return it in a text buffer. */
356 static int __sprint_symbol(char *buffer, unsigned long address,
357                            int symbol_offset, int add_offset)
358 {
359         char *modname;
360         const char *name;
361         unsigned long offset, size;
362         int len;
363 
364         address += symbol_offset;
365         name = kallsyms_lookup(address, &size, &offset, &modname, buffer);
366         if (!name)
367                 return sprintf(buffer, "0x%lx", address);

kallsyms_lookup()はこのようになっていて、対象のアドレスがvmlinux内に存在するのか、カーネルモジュールにあるのかで使う関数が変わる。

292 const char *kallsyms_lookup(unsigned long addr,
293                             unsigned long *symbolsize,
294                             unsigned long *offset,
295                             char **modname, char *namebuf)
296 {
297         namebuf[KSYM_NAME_LEN - 1] = 0;
298         namebuf[0] = 0;
299 
300         if (is_ksym_addr(addr)) {
301                 unsigned long pos;
302 
303                 pos = get_symbol_pos(addr, symbolsize, offset);
304                 /* Grab name */
305                 kallsyms_expand_symbol(get_symbol_offset(pos),
306                                        namebuf, KSYM_NAME_LEN);
307                 if (modname)
308                         *modname = NULL;
309                 return namebuf;
310         }
311 
312         /* See if it's in a module. */
313         return module_address_lookup(addr, symbolsize, offset, modname,
314                                      namebuf);
315 }

is_ksym_addr()はaddrがstext・endの範囲内にあるか、もしくはVSYSCALL_START・VSYSCALL_END内にあるかをチェックして、範囲内なら1を返す関数。ここで使うstext、endはカーネルビルド時にarch/x86/kernel/vmlinux.ldsに書かれてます。

今回はis_ksym_addr()が1を返した場合だけ見てきましょう。まずはget_symbol_pos()でsymbolを探します。
シンボルをどこから探しているのかというと、kallsyms_addresses[]から。

 35 
 36 /*
 37  * These will be re-linked against their real values
 38  * during the second link stage.
 39  */
 40 extern const unsigned long kallsyms_addresses[] __weak;
 41 extern const u8 kallsyms_names[] __weak;

この配列からアドレスを探すのだけど、さすがに先頭から舐めていくのは時間がかかるのでバイナリサーチが使われます。バイナリサーチして配列のインデックスを見つけたら、シンボルにはエイリアスが使われることもあるようなので、そのインデックスから下方向に同じアドレスを使うものがあるか見ていき、見つかったところにあるアドレスがsymbolの開始アドレス(symbol_start)となります。

237         /*
238          * Search for the first aliased symbol. Aliased
239          * symbols are symbols with the same address.
240          */
241         while (low && kallsyms_addresses[low-1] == kallsyms_addresses[low])
242                 --low;

次は、エイリアスじゃないアドレス(別のsymbol)の位置を探します。この場合、別のsymbolが見つかった場合はそのアドレスになるし、見つからなかった場合は_endや、テキストセクションの終端のアドレスが設定されます。これはsymbol_endという変数が使われます。

ここまででsymbolの開始・終了位置が分かりました。 次の処理でsymbolのサイズを計算します。これは単に終了アドレスから開始アドレスを引くだけですね。

264         if (symbolsize)
265                 *symbolsize = symbol_end - symbol_start;

その次はoffsetなので関数の呼び出し位置ですかね。対象になっているアドレスから開始位置を引くことで、開始位置から何バイト目で実行されたかが分かると。

266         if (offset)
267                 *offset = addr - symbol_start;

最後にkallsyms_addresses[]のインデックスとして見つけたlowを返して終了です。

get_symbol_pos()が終わると次はkallsyms_expand_symbol()でsymbol名の取得処理ですね。 まず、第一引数はget_symbol_offset()の戻り値でこれはkallsyms_expand_symbol()で使用するためのインデックスを決定しています。

名称はkallsyms_names、kallsyms_token_table、kallsyms_token_indexの3つの配列から探します。

後者の2個はこのように定義。

 50 extern const u8 kallsyms_token_table[] __weak;
 51 extern const u16 kallsyms_token_index[] __weak;

まず、kallsyms_names[]からで、offはget_symbol_offset()の返り値。これで長さを取ってますね。

 97         /* Get the compressed symbol length from the first symbol byte. */
 98         data = &kallsyms_names[off];
 99         len = *data;
100         data++;

次は名前を1byteずつresultにコピーしていく処理。テーブルの関連としてはkallsyms_token_tableに実際のsymbol名があって、そのテーブルを引くためにkallsyms_token_indexを使い、kallsyms_token_indexのテーブルを引くためにkallsyms_namesを使っていますね。

/*
109          * For every byte on the compressed symbol data, copy the table
110          * entry for that byte.
111          */
112         while (len) {
113                 tptr = &kallsyms_token_table[kallsyms_token_index[*data]];
114                 data++;
115                 len--;
116 
117                 while (*tptr) {
118                         if (skipped_first) {
119                                 if (maxlen <= 1)
120                                         goto tail;
121                                 *result = *tptr;
122                                 result++;
123                                 maxlen--;
124                         } else
125                                 skipped_first = 1;
126                         tptr++;
127                 }
128         }

kallsyms_lookup()が終わると%pSに必要な情報、symbol名、offset、サイズが全部取れているのであどは__sprint_symbol()の方で文字列を組み立ててあげます。 うーん、symbol名を得るところはテーブル引きまくっているので、書く配列でのデータの持ち方がわからないと細かい部分はわからないですが、概要としてはこれで足りるかな。。

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版