はじめに
linux kernel configの一つ、CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
について掘り下げてみる。
主に参考にしたlinux kernelは、v4.15。
TL;DR
CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
はdefconfigで結構無効にされてしまっているけど、使えるgdb
のバージョンが6.7以降なら有効にしておこう。
CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERSとは何か
core dump生成時、Disk I/Oやストレージ容量への負荷を減らすため、テキスト領域のように変更されないメモリはデフォルトで保存しない。
デバッグ時に、必要に応じて実行ファイルを読みこめばいいからだ。
実行ファイルのELFヘッダもその部類に入る。
つまりメモリにロードされてから変更されることは基本的にない。
ただ、CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
が有効な場合は、デフォルトでクラッシュしたプログラムのELFヘッダもcoreファイル中に保存するようになる。
ELFヘッダにはビルドID等の情報が含まれるため、解析時に対応させるべきデバッグシンボルを特定する際などに有益な場合があるからだ。
CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
はあくまでもデフォルト値を変更するだけで、起動後にprocfs経由で変更することもできる。
/proc/PID/coredump_filter
の"(bit 4) ELF header pages in file-backed private memory areas"を立てればよい。
実装
CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
が使われているのは下記の一箇所。
有効なときはMMF_DUMP_FILTER_DEFAULTにMMF_DUMP_ELF_HEADERSビットが立つ。
#define MMF_DUMP_FILTER_DEFAULT \
((1 << MMF_DUMP_ANON_PRIVATE) | (1 << MMF_DUMP_ANON_SHARED) |\
(1 << MMF_DUMP_HUGETLB_PRIVATE) | MMF_DUMP_MASK_DEFAULT_ELF)
#ifdef CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
# define MMF_DUMP_MASK_DEFAULT_ELF (1 << MMF_DUMP_ELF_HEADERS)
#else
# define MMF_DUMP_MASK_DEFAULT_ELF 0
#endif
MMF_DUMP_ELF_HEADERS
をチェックしているのは、vma_dump_size
関数。
マクロを使っているため単純なgrepではヒットしないので注意。
その名のとおり、core dump生成時vmaごとにダンプすべきサイズを算出している。
vmaの先頭がELFヘッダを示すマジックワード(0x7f, 'E' ,'L', 'F')と一致したら、PAGE_SIZEを返す。
(=ELFヘッダが含まれると見られる領域を1ページ分ダンプする)
static unsigned long vma_dump_size(struct vm_area_struct *vma,
unsigned long mm_flags)
{
#define FILTER(type) (mm_flags & (1UL << MMF_DUMP_##type))
(中略)
if (FILTER(ELF_HEADERS) &&
vma->vm_pgoff == 0 && (vma->vm_flags & VM_READ)) {
u32 __user *header = (u32 __user *) vma->vm_start;
u32 word;
mm_segment_t fs = get_fs();
/*
* Doing it this way gets the constant folded by GCC.
*/
union {
u32 cmp;
char elfmag[SELFMAG];
} magic;
BUILD_BUG_ON(SELFMAG != sizeof word);
magic.elfmag[EI_MAG0] = ELFMAG0;
magic.elfmag[EI_MAG1] = ELFMAG1;
magic.elfmag[EI_MAG2] = ELFMAG2;
magic.elfmag[EI_MAG3] = ELFMAG3;
/*
* Switch to the user "segment" for get_user(),
* then put back what elf_core_dump() had in place.
*/
set_fs(USER_DS);
if (unlikely(get_user(word, header)))
word = 0;
set_fs(fs);
if (word == magic.cmp)
return PAGE_SIZE;
}
#undef FILTER
return 0;
実験
ELFヘッダありのcoreダンプとELFヘッダなしのcoreダンプを実際に取得して比較してみる。
準備
事前にcoreファイルを出力できるよう、resoure limitをなくしておく。
また、出力先もわかりやすくするため、core_patternも変更しておく。
$ ulimit -c unlimited
$ sudo su
# echo 'core.%p' > /proc/sys/kernel/core_pattern
# exit
ELFヘッダなし
coredump_filter
で"(bit 4) ELF header pages in file-backed private memory areas"を落とした状態でcoreファイルを生成する。
$ echo 0x23 > /proc/self/coredump_filter
$ sleep 100 &
$ kill -SEGV $!
$ mv core.$! core.no_elf_header
ELFヘッダあり
今度は"(bit 4) ELF header pages in file-backed private memory areas"を立てた状態でcoreファイルを生成する。
$ echo 0x33 > /proc/self/coredump_filter
$ sleep 100 &
$ kill -SEGV $!
$ mv core.$! core.with_elf_header
比較
それぞれのcoreファイルのセクション情報を比較して、PIDの違いやオフセットの違いを除いたものが下記。
load1
セクションがload1a
とload1b
に、分かれていることがわかる。
同様にload6
とload11
も分かれている。
load1
はSizeが0だったのに対して、load1a
は00001000(=PAGE_SIZE)となっている。
確かに追加でデータが保存されてそうだ。
$ objdump -h core.no_elf_header > sections.no_elf_header
$ objdump -h core.with_elf_header > sections.with_elf_header
$ diff -u sections.no_elf_header sections.with_elf_header
--- sections.no_elf_header 2019-03-30 11:11:08.051431648 +0900
+++ sections.with_elf_header 2019-03-30 11:11:28.923522648 +0900
Sections:
Idx Name Size VMA LMA File off Algn
(中略)
- 12 load1 00000000 0000000000400000 0000000000000000 00002000 2**12
+ 12 load1a 00001000 0000000000400000 0000000000000000 00002000 2**12
+ CONTENTS, ALLOC, LOAD, READONLY, CODE
+ 13 load1b 00000000 0000000000401000 0000000000001000 00003000 2**12
ALLOC, READONLY, CODE
(中略)
- 17 load6 00000000 00007f136b94f000 0000000000000000 00025000 2**12
+ 18 load6a 00001000 00007fae7829d000 0000000000000000 00026000 2**12
+ CONTENTS, ALLOC, LOAD, READONLY, CODE
+ 19 load6b 00000000 00007fae7829e000 0000000000001000 00027000 2**12
ALLOC, READONLY, CODE
(中略)
- 22 load11 00000000 00007f136bd19000 0000000000000000 0002f000 2**12
+ 24 load11a 00001000 00007fae78667000 0000000000000000 00031000 2**12
+ CONTENTS, ALLOC, LOAD, READONLY, CODE
+ 25 load11b 00000000 00007fae78668000 0000000000001000 00032000 2**12
ALLOC, READONLY, CODE
追加された領域の中身を見てみる
追加されたセクションのオフセット、サイズはわかったので、中身を確認してみる。
load1a
セクションを取り出し、readelf
コマンドに食わせると、1ページ分しかないためか少しエラーが出たが、ちゃんとELFとして解釈し、BUILD_IDも取り出せている。
また、最初のテキスト領域ということで実行ファイルそのもの(今回はsleep
コマンド)の先頭1ページと一致することも確認できた。
(本当はobjcopy
コマンドでスマートに取り出したかったが、coreファイルだからかエラーになってしまったので、dd
コマンドで取り出している。)
$ dd if=core.with_elf_header of=core.with_elf_header.load1a bs=4096 skip=2 count=1
$ readelf --notes core.with_elf_header.load1a
readelf: Error: Reading 0x740 bytes extends past end of file for section headers
readelf: Error: the dynamic segment offset + size exceeds the size of the file
Displaying notes found at file offset 0x00000254 with length 0x00000044:
Owner Data size Description
GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
OS: Linux, ABI: 2.6.32
GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
Build ID: 5ff67ba1f7dc0370081f7f6aad5c173d1825c087
$ dd if=/bin/sleep of=sleep.head bs=4096 count=1
$ diff sleep.head core.with_elf_header.load1a
では、同様にELFヘッダが追加されたload6
, load11
は何者か?
gdb
でinfo sharelibrary
を見てみるとload6a
の直後に/lib/x86_64-linux-gnu/libc.so.6
が、load11a
の直後に/lib64/ld-linux-x86-64.so.2
がロードされている。
これらのsharedlibrayもELFファイルなので、そのヘッダが保存されたのだろう。
ただ、ローダが書き換えているのか、ライブラリそのもののヘッダとは中身が異なっていた。
そのためこれらのELFヘッダがどういうときに役に立つのかはわからなかった。
(ご存知の方はぜひ教えてください。)
(gdb) target core core.with_elf_header
[New LWP 12138]
Core was generated by `sleep 100'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00007fae783692f0 in ?? ()
(gdb) info files
Local core dump file:
`/home/kondo/work/190330_linux_kernel/core.with_elf_header', file type elf64-x86-64.
0x0000000000400000 - 0x0000000000401000 is load1a
0x0000000000401000 - 0x0000000000401000 is load1b
0x0000000000606000 - 0x0000000000607000 is load2
0x0000000000607000 - 0x0000000000608000 is load3
0x0000000000ab4000 - 0x0000000000ad5000 is load4
0x00007fae77ec8000 - 0x00007fae77ec8000 is load5
0x00007fae7829d000 - 0x00007fae7829e000 is load6a
0x00007fae7829e000 - 0x00007fae7829e000 is load6b
0x00007fae7845d000 - 0x00007fae7845d000 is load7
0x00007fae7865d000 - 0x00007fae78661000 is load8
0x00007fae78661000 - 0x00007fae78663000 is load9
0x00007fae78663000 - 0x00007fae78667000 is load10
0x00007fae78667000 - 0x00007fae78668000 is load11a
0x00007fae78668000 - 0x00007fae78668000 is load11b
0x00007fae78869000 - 0x00007fae7886c000 is load12
0x00007fae7888c000 - 0x00007fae7888d000 is load13
0x00007fae7888d000 - 0x00007fae7888e000 is load14
0x00007fae7888e000 - 0x00007fae7888f000 is load15
0x00007fff0efcf000 - 0x00007fff0eff0000 is load16
0x00007fff0eff7000 - 0x00007fff0effa000 is load17
0x00007fff0effa000 - 0x00007fff0effc000 is load18
0xffffffffff600000 - 0xffffffffff601000 is load19
Local exec file:
<no file loaded>
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
0x00007fae782bc8b0 0x00007fae7840fb04 No /lib/x86_64-linux-gnu/libc.so.6
0x00007fae78667ac0 0x00007fae78685850 No /lib64/ld-linux-x86-64.so.2
CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
の歴史
通常、coreファイル自体もELFフォーマットで出力されるので少し混乱するかも知れないが、この場合coreファイルの中にさらにELFヘッダがあることになる。
実際、このことにより6.7より古いgdbでは、ELFの解釈に失敗してしまったらしい。
そのことはCONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
の説明にも書かれている。
config CORE_DUMP_DEFAULT_ELF_HEADERS
bool "Write ELF core dumps with partial segments"
default y
depends on BINFMT_ELF && ELF_CORE
help
ELF core dump files describe each memory mapping of the crashed
process, and can contain or omit the memory contents of each one.
The contents of an unmodified text mapping are omitted by default.
For an unmodified text mapping of an ELF object, including just
the first page of the file in a core dump makes it possible to
identify the build ID bits in the file, without paying the i/o
cost and disk space to dump all the text. However, versions of
GDB before 6.7 are confused by ELF core dump files in this format.
The core dump behavior can be controlled per process using
the /proc/PID/coredump_filter pseudo-file; this setting is
inherited. See Documentation/filesystems/proc.txt for details.
This config option changes the default setting of coredump_filter
seen at boot time. If unsure, say Y.
実はこのgdbでただしく解釈できないという課題があったため、2008年の導入当初(v2.6.28)CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS
はデフォルト無効だった。
ただ、環境が追いついたこともあり、2010年デフォルト有効にするパッチが投稿され、v2.6.37でマージされたようだ。
ここからは推測だが、そのデフォルト無効時に作成されたdefconfigがそのままだったり参考にされたりして、結構無効になっているarchがある。
coreファイルのサイズも数ページ分しか増えないし、最悪ツールが対応していなかったとしてもELFから該当のセクションを取り除けばいいだけなので、特別な理由がない限り有効にしておきたい。
参考
proc (Linux Documentation)
default CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS=y (LKML)