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 はリファレンスに、次のように記載されている。
- 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 1月 23 21:51 .
drwxr-xr-x. 4 root root 152 1月 23 21:49 ..
-rwxr-xr-x. 1 root root 10912 1月 23 21:51 main-g0
-rwxr-xr-x. 1 root root 11816 1月 23 21:51 main-g1
-rwxr-xr-x. 1 root root 13424 1月 23 21:51 main-g2
-rwxr-xr-x. 1 root root 35136 1月 23 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
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 が使われる。
- /usr/lib/debug/.build-id/ab/cdef1234.debug
- /usr/bin/ls.debug
- /usr/bin/.debug/ls.debug
- /usr/lib/debug/usr/bin/ls.debug.
引用元: 参考 gdb manual > 18.3 Debugging Information in Separate Files
まあでも、シンボリックリンクなので読み込まれるファイルはどっちでも同じ。
]# ls -al /usr/lib/debug/.build-id/93/7708964f0f7e3673465d7749d6cf6a2601dea2.debug
lrwxrwxrwx. 1 root root 63 5月 12 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 {