SIer だけど技術やりたいブログ

Linux debuginfo って何?

linux

debuginfo は、デバッグ情報が入っているリポジトリ。

検証環境

CentOS 8 を利用する。

]# cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)
]# uname -a
Linux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64 #1 SMP Fri Jan 3 23:55:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

デバッグ情報とは何か

C や C++ といったプログラム言語は、コンパイル時に ELF 形式のファイルに変換される。

C言語がコンパイル~実行されるまで - SIerだけど技術やりたいブログwww.kimullaa.com

C 言語で書いた次のプログラムを例に説明する。

#include <stdio.h>

int twice(int x) {
  return x * x;
}

int main(void) {
  int hoge = 9;
  // 2 倍する
  twice(hoge);
}

これをコンパイルして逆アセンブルすると、実行コード部分は以下のアセンブリになる。これを見るとわかるが、変数名や関数名、型情報、コメント、といった情報が存在しない

]# gcc main.c -o main
]# objdump -d main
[...]
0000000000400546 <main>:
  400546:       55                      push   %rbp
  400547:       48 89 e5                mov    %rsp,%rbp
  40054a:       48 83 ec 10             sub    $0x10,%rsp
  40054e:       c7 45 fc 09 00 00 00    movl   $0x9,-0x4(%rbp)
  400555:       8b 45 fc                mov    -0x4(%rbp),%eax
  400558:       89 c7                   mov    %eax,%edi
  40055a:       e8 d7 ff ff ff          callq  400536 <twice>
  40055f:       b8 00 00 00 00          mov    $0x0,%eax
  400564:       c9                      leaveq
  400565:       c3                      retq
  400566:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40056d:       00 00 00
[...]

そのため、このままでは実行ファイルと元のプログラムとの対応がわからない。そこで、デバッグ情報として実行ファイルと元のプログラムとの対応を保持させる。主なデバッグ情報の規格は DWARF
参考 デバッグ情報の歩き方
参考 DWARF Debugging Information Format Version 5

補足

変数名や関数名の一部は名前が表示されている。この理由は、一部はシンボルテーブルで解決できるためだと思う。

]# readelf -s main | grep FUNC
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
    46: 0000000000400490     0 FUNC    LOCAL  DEFAULT   11 deregister_tm_clones
    47: 00000000004004c0     0 FUNC    LOCAL  DEFAULT   11 register_tm_clones
    48: 0000000000400500     0 FUNC    LOCAL  DEFAULT   11 __do_global_dtors_aux
    51: 0000000000400530     0 FUNC    LOCAL  DEFAULT   11 frame_dummy
    62: 00000000004005e0     5 FUNC    GLOBAL DEFAULT   11 __libc_csu_fini
    66: 00000000004005e8     0 FUNC    GLOBAL HIDDEN    12 _fini
    // アドレス値と名前があるので解決可能
    67: 0000000000400536    16 FUNC    GLOBAL DEFAULT   11 twice
    68: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    73: 0000000000400570   101 FUNC    GLOBAL DEFAULT   11 __libc_csu_init
    75: 0000000000400480     5 FUNC    GLOBAL HIDDEN    11 _dl_relocate_static_pie
    76: 0000000000400450    47 FUNC    GLOBAL DEFAULT   11 _start
    // アドレス値と名前があるので解決可能
    78: 0000000000400546    32 FUNC    GLOBAL DEFAULT   11 main
    81: 0000000000400428     0 FUNC    GLOBAL HIDDEN    10 _init

試しに strip コマンドでシンボルテーブルを削除すると、変数名や関数名の表示が消えた。やはりシンボルテーブルが影響していると思う。

]# strip -R symtab main
]# objdump -d main
...
  400546:       55                      push   %rbp
  400547:       48 89 e5                mov    %rsp,%rbp
  40054a:       48 83 ec 10             sub    $0x10,%rsp
  40054e:       c7 45 fc 09 00 00 00    movl   $0x9,-0x4(%rbp)
  400555:       8b 45 fc                mov    -0x4(%rbp),%eax
  400558:       89 c7                   mov    %eax,%edi
  40055a:       e8 d7 ff ff ff          callq  0x400536
  40055f:       b8 00 00 00 00          mov    $0x0,%eax
  400564:       c9                      leaveq
  400565:       c3                      retq
  400566:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40056d:       00 00 00
...

デバッグ情報の付与のしかた

gcc の場合、 -g オプションでデバッグ情報を付与できる。付与する情報量ごとに-g0 から -g3 まである。詳細は他の記事(GDBでデバッグするなら-g3オプション)や、次のオンラインマニュアル gdb(3) を参照してください。

       -glevel
       -ggdblevel
       -gstabslevel
       -gxcofflevel
       -gvmslevel
           Request debugging information and also use level to specify how much information.  The default level is 2.

           Level 0 produces no debug information at all.  Thus, -g0 negates -g.

           Level 1 produces minimal information, enough for making backtraces in parts of the program that you don't plan to debug.  This includes
           descriptions of functions and external variables, and line number tables, but no information about local variables.

           Level 3 includes extra information, such as all the macro definitions present in the program.  Some debuggers support macro expansion when you
           use -g3.

           -gdwarf does not accept a concatenated debug level, to avoid confusion with -gdwarf-level.  Instead use an additional -glevel option to change
           the debug level for DWARF.

デバッグ情報の効果を確かめる

先ほどの C 言語のプログラムを再度コンパイルし、-g オプションの有無で情報量に差があることを示す。 -g オプションのあり・なしでコンパイルし、それぞれの実行ファイルに対して objdump を -d -S -l オプションで実行する。-S は対応するソースコードを表示するオプション。 -l は対応する行数を表示するオプション。

デバッグ情報を付与していない場合は、ソースコードが表示できない。

]# gcc main.c -o main
]# objdump -d main -S -l
[...]
0000000000400546 <main>:
main():
  400546:       55                      push   %rbp
  400547:       48 89 e5                mov    %rsp,%rbp
  40054a:       48 83 ec 10             sub    $0x10,%rsp
  40054e:       c7 45 fc 09 00 00 00    movl   $0x9,-0x4(%rbp)
  400555:       8b 45 fc                mov    -0x4(%rbp),%eax
  400558:       89 c7                   mov    %eax,%edi
  40055a:       e8 d7 ff ff ff          callq  400536 <twice>
  40055f:       b8 00 00 00 00          mov    $0x0,%eax
  400564:       c9                      leaveq
  400565:       c3                      retq
  400566:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40056d:       00 00 00
[...]

デバッグ情報を付与した場合は、ソースコードが表示できる。

]# gcc main.c -o main -g
]# objdump -d main -S -l
...
0000000000400546 <main>:
main():
/root/work/clang/main.c:7

int main(void) {
  400546:       55                      push   %rbp
  400547:       48 89 e5                mov    %rsp,%rbp
  40054a:       48 83 ec 10             sub    $0x10,%rsp
/root/work/clang/main.c:8
  int hoge = 9;
  40054e:       c7 45 fc 09 00 00 00    movl   $0x9,-0x4(%rbp)
/root/work/clang/main.c:10
  // 2 倍する
  twice(hoge);
  400555:       8b 45 fc                mov    -0x4(%rbp),%eax
  400558:       89 c7                   mov    %eax,%edi
  40055a:       e8 d7 ff ff ff          callq  400536 <twice>
  40055f:       b8 00 00 00 00          mov    $0x0,%eax
/root/work/clang/main.c:11
}
  400564:       c9                      leaveq
  400565:       c3                      retq
  400566:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40056d:       00 00 00
...

デバッグ情報の中身を見てみる

次に、-g オプションの有無で実行ファイルがどのように変わるかを確認する。readelf を -S オプションで実行し、セクション情報を確認する。

-g オプションを付けない場合、 27 セクション存在する。

// 
]# gcc main.c -o main
]# readelf -S main
...
  [23] .comment          PROGBITS         0000000000000000  0000101c
       0000000000000058  0000000000000001  MS       0     0     1
  [24] .gnu.build.attrib NOTE             0000000000a01020  00001074
       0000000000000654  0000000000000000           0     0     4
  [25] .symtab           SYMTAB           0000000000000000  000016c8
       00000000000007b0  0000000000000018          26    62     8
  [26] .strtab           STRTAB           0000000000000000  00001e78
       0000000000000414  0000000000000000           0     0     1
  [27] .shstrtab         STRTAB           0000000000000000  0000228c
       000000000000010f  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
...

-g オプションを付けた場合、デバッグ用のセクションが増えている。

]# gcc -g3  main.c -o main
]# readelf -S main
...
  [23] .comment          PROGBITS         0000000000000000  0000101c
       0000000000000058  0000000000000001  MS       0     0     1
  [24] .gnu.build.attrib NOTE             0000000000a01020  00001074
       0000000000000654  0000000000000000           0     0     4
  [25] .debug_aranges    PROGBITS         0000000000000000  000016c8
       0000000000000030  0000000000000000           0     0     1
  [26] .debug_info       PROGBITS         0000000000000000  000016f8
       0000000000000341  0000000000000000           0     0     1
  [27] .debug_abbrev     PROGBITS         0000000000000000  00001a39
       000000000000010e  0000000000000000           0     0     1
  [28] .debug_line       PROGBITS         0000000000000000  00001b47
       00000000000000fc  0000000000000000           0     0     1
  [29] .debug_str        PROGBITS         0000000000000000  00001c43
       0000000000000258  0000000000000001  MS       0     0     1
  [30] .symtab           SYMTAB           0000000000000000  00001ea0
       0000000000000828  0000000000000018          31    67     8
  [31] .strtab           STRTAB           0000000000000000  000026c8
       0000000000000414  0000000000000000           0     0     1
  [32] .shstrtab         STRTAB           0000000000000000  00002adc
       000000000000014f  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
...

デバッグ情報の詳細は readelf の -w オプションで表示できる。例えば、変数 hoge の情報を見てみると、実際にソースコードとの対応が取れそうな情報が表示された。

]# gcc main.c -o main -g
]# readelf -w -W main
...
  // 型の定義
 <1><65>: 省略番号: 5 (DW_TAG_base_type)
    <66>   DW_AT_byte_size   : 4
    <67>   DW_AT_encoding    : 5        (signed)
    <68>   DW_AT_name        : int
...
 <2><304>: 省略番号: 17 (DW_TAG_variable)
    <305>   DW_AT_name        : (indirect string, offset: 0x123): hoge // 変数名
    <309>   DW_AT_decl_file   : 1 // ファイル名
    <30a>   DW_AT_decl_line   : 8 // 行
    <30b>   DW_AT_decl_column : 7 // 列
    <30c>   DW_AT_type        : <0x65> // 型情報(前述の型定義のアドレスっぽい)
    <310>   DW_AT_location    : 2 byte block: 91 6c     (DW_OP_fbreg: -20) // 実行ファイルとの対応
 <2><313>: Abbrev Number: 0
...
 The File Name Table (offset 0x7e):
  Entry Dir     時刻    サイズ  名前
  1     0       0       0       main.c
  2     1       0       0       stddef.h
...

DW_AT_location だけ難しい。DW_AT_location はリファレンスに、次のように記載されている。

  1. DW_OP_fbreg The DW_OP_fbreg operation provides a signed LEB128 offset from the address specified by the location description in the DW_AT_frame_base attribute of the current function. This is typically a stack pointer register plus or minus some offset.

先述のアセンブラ(次に再掲)からもわかるが、ローカル変数はスタック領域に確保される。

0000000000400546 <main>:
  400546:       55                      push   %rbp
  400547:       48 89 e5                mov    %rsp,%rbp
  40054a:       48 83 ec 10             sub    $0x10,%rsp
  40054e:       c7 45 fc 09 00 00 00    movl   $0x9,-0x4(%rbp)

そのため、デバッグ情報側もスタック領域からのオフセットで位置を指定しているんだと思う。(-0x4(%rbp) がローカル変数の位置。関数呼出の CALL で sp = main -8。push %rbp で sp = main - 16。mov %rsp,%rbp で rbp = sp。 movl $0x9,-0x4(%rbp) で 0x4(%rbp) = main - 20。これが DW_OP_fbreg: -20 だと思われる。)

デバッグ情報の問題点

デバッグ情報を付与した分、ファイルサイズが大きくなる

]# gcc -o main-g0 main.c -g0
]# gcc -o main-g1 main.c -g1
]# gcc -o main-g2 main.c -g2
]# gcc -o main-g3 main.c -g3
# ls -al
合計 80
drwxr-xr-x. 2 root root    80  123 21:51 .
drwxr-xr-x. 4 root root   152  123 21:49 ..
-rwxr-xr-x. 1 root root 10912  123 21:51 main-g0
-rwxr-xr-x. 1 root root 11816  123 21:51 main-g1
-rwxr-xr-x. 1 root root 13424  123 21:51 main-g2
-rwxr-xr-x. 1 root root 35136  123 21:51 main-g3

debuginfo

ようやく本題。先述の通り、デバッグ情報を含めるとファイルサイズが膨らむ。デバッグ情報は使わなければ無駄なので、 CentOS のリポジトリで提供されているパッケージはだいたいが最低限の情報(MiniDebugInfo)だけを設定している。(というか-g3でコンパイルしてるものを見たことがないけど、公式に書いてなかったのでこう書いた。)
参考 Red Hat パート II. デバッグアプリケーション

MiniDebugInfo はバックトレースができる位の最低限の情報が入っていて、.gnu_debugdata セクションに保存されている。
参考 gdb manual > 18.4 Debugging information in a special section

]# readelf -W -S /usr/bin/ls
...
  [29] .gnu_debugdata    PROGBITS        0000000000000000 02726c 000ec4 00      0   0  1

これだけだとデバッグできたもんじゃないので、 gdb や systemtap がデバッグ情報を必要としたときに備えてデバッグ情報を提供している。これが debuginfo。

gdb での使い方

gdb で利用するときのインストール方法は単純で、表示されたコマンドをそのまま打ち込むだけ。例えば ls を gdb で起動したときは、次のように gdb のログ(Missing separate debuginfos, use: dnf debuginfo-install coreutils-8.30-6.el8.x86_64)が表示される。

]# gdb ls
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-6.el8
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ls...Reading symbols from .gnu_debugdata for /usr/bin/ls...(no debugging symbols found)...done.
(no debugging symbols found)...done.
Missing separate debuginfos, use: yum debuginfo-install coreutils-8.30-6.el8.x86_64

デバッグ情報をインストールして gdb を再起動すると、 debuginfo が読み込まれる。(Reading symbols from ls...Reading symbols from /usr/lib/debug/usr/bin/ls-8.30-6.el8.x86_64.debug...done.)

]# yum debuginfo-install coreutils-8.30-6.el8.x86_64
...
インストール済み:
  coreutils-debuginfo-8.30-6.el8.x86_64

完了しました!
]# gdb ls
...
Reading symbols from ls...Reading symbols from /usr/lib/debug/usr/bin/ls-8.30-6.el8.x86_64.debug...done.
done.

gdb はどのようにデバッグ情報を扱っているか?

gdb には、デバッグ情報を実行ファイルから分離できる仕組みがある。
参考 gdb manual > 18.3 Debugging Information in Separate Files

実行ファイルにデバッグ情報を検索するキーのようなものが埋まっているので、それを基にファイルを読み込む。

方法は build-id と debuglink の 2 種類ある。

build-id

build-id の情報は .note.gnu.build-id セクションに含まれている。

]# readelf -x .note.gnu.build-id /usr/bin/ls

セクション '.note.gnu.build-id' の 十六進数ダンプ:
  0x000002d0 04000000 14000000 03000000 474e5500 ............GNU.
  0x000002e0 93770896 4f0f7e36 73465d77 49d6cf6a .w..O.~6sF]wI..j
  0x000002f0 2601dea2                            &...

構造の説明は省略するが、937708964f0f7e3673465d7749d6cf6a2601dea2が build-id。(構造の詳細は 【ELF形式】.note.gnu.build-id セクション などを参照してください。)

gdb は、 build-id に一致するデバッグファイルを <デバッグディレクトリ>/.build-id ディレクトリから検索する。

debuglink の情報は .gnu_debuglink セクションに含まれている。

]# readelf -p .gnu_debuglink /usr/bin/ls

セクション '.gnu_debuglink' の文字列ダンプ:
  [     0]  ls-8.30-6.el8.x86_64.debug
  [    1c]  3舖E

gdb は、このファイルを <デバッグディレクトリ>/<コマンドが配置されたディレクトリのパス。今回だと usr/bin/ls なので /usr/bin> から探す。

gdb のデバッグディレクトリ

デフォルトのデバッグディレクトリは/usr/lib/debug。(spec ファイルに記述されてた)

...
../configure                                                    \
        --prefix=%{_prefix}                                     \
        --libdir=%{_libdir}                                     \
        --sysconfdir=%{_sysconfdir}                             \
        --mandir=%{_mandir}                                     \
        --infodir=%{_infodir}                                   \
        --with-system-gdbinit=%{_sysconfdir}/gdbinit            \
        --with-gdb-datadir=%{_datadir}/gdb                      \
        --enable-gdb-build-warnings=,-Wno-unused                \
        --enable-build-with-cxx                                 \
%ifnarch %{ix86} alpha ppc s390 s390x x86_64 ppc64 ppc64le sparc sparcv9 sparc64 %{arm} aarch64
        --disable-werror                                        \
%else
        --enable-werror                                         \
%endif
        --with-separate-debug-dir=/usr/lib/debug
...

ls の debuginfo でインストールされる資材を確認すると、 build-id の検索対象や debuglink の検索対象が存在するのがわかる。(build-id は環境によってはサポートしていないので、どっちもあるっぽい。)

]# rpm -ql coreutils-debuginfo | grep 93/7708964f0f7e3673465d7749d6cf6a2601dea2
/usr/lib/debug/.build-id/93/7708964f0f7e3673465d7749d6cf6a2601dea2
/usr/lib/debug/.build-id/93/7708964f0f7e3673465d7749d6cf6a2601dea2.debug

]# rpm -qil coreutils-debuginfo | grep "ls-8.30-6.el8.x86_64.debug"
/usr/lib/debug/usr/bin/ls-8.30-6.el8.x86_64.debug

build-id を先に試して、ダメなら debuglink が使われる。

まあでも、シンボリックリンクなので読み込まれるファイルはどっちでも同じ。

]# ls -al /usr/lib/debug/.build-id/93/7708964f0f7e3673465d7749d6cf6a2601dea2.debug
lrwxrwxrwx. 1 root root 63  512  2019 /usr/lib/debug/.build-id/93/7708964f0f7e3673465d7749d6cf6a2601dea2.debug -> ../../../../../usr/lib/debug/usr/bin/ls-8.30-6.el8.x86_64.debug

ソースコードは?

debuginfo にはソースコードが含まれてないため、 gdb で表示されない。

(gdb) b main
Breakpoint 1 at 0x43b0: file ../src/ls.c, line 1449.
(gdb) r
Starting program: /usr/bin/ls
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, main (argc=1, argv=0x7fffffffe528) at ../src/ls.c:1449
1449    ../src/ls.c: そのようなファイルやディレクトリはありません.

ソースコードは debugsource パッケージで配布される。
参考7.3. DEBUGINFO パッケージおよび DEBUGSOURCE パッケージ

試しに ls のソースコードを落としてみると、 gdb でソースコードが表示される。

]# yum install coreutils-debugsource

]# gdb ls
...
(gdb) b main
Breakpoint 1 at 0x43b0: file ../src/ls.c, line 1449.
(gdb) r
Starting program: /usr/bin/ls
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, main (argc=1, argv=0x7fffffffe528) at ../src/ls.c:1449
1449    {