やりたいこと
組込み開発をしていると、問題発生時のrawメモリダンプから解析をする必要があったりする。
その場合、対応する実行ファイルのシンボルから、おかしそうな領域に対応するダンプデータを見ることになる。
ただ、意外とシンボルのアドレスや型の確認とそれに合わせたダンプの内容確認が面倒くさい。
- 見たい領域のシンボルのアドレスを確認する
- 見たい領域の型を確認する
- rawメモリダンプに含まれるメモリのアドレスから、見たい領域のダンプ中のオフセットを算出する
- 領域の型に応じて、rawメモリダンプをパースする
もちろん上記を手順を一度通せば、同じシンボルの内容を確認するのはある程度プログラム化できる。
とはいえ、確認しなければ変数・型・アドレスは当然毎回違うので、結局大体都度上記の手順を踏むこととなる。
そこをgdb
上でメモリの内容をシンボルから参照したい、というのが今回の記事のやりたいことである。
なお、ここで想定しているのは、linuxのように素敵なELFフォーマットのコアダンプが取得できる環境ではない。
本当にただメモリの生データをそのままファイルに落としただけのものが手に入る状況とする。
やること(概要)
ざっくり言えば、今回の目的を達成するために必要なことは
「rawメモリダンプと実行ファイルをリンクして、gdb
に読み込む」
である。
やること(詳細)
下記のようなプログラムとメモリダンプを例に、詳細な手順を示す。
時間がない人は、「解析手順(改善後)」まで飛ばしてください。
メモリダンプを解析するプログラムの例
# include <stdlib.h>
# include <stdio.h>
# include <math.h>
extern const void * __data_start;
extern const void * __bss_start;
# define DATA_SECTION_ADDR ((void *)&__data_start)
# define DATA_SECTION_SIZE ((long)&__bss_start - (long)&__data_start)
# define BSS_SECTION_ADDR ((void *)&__bss_start)
# define BSS_SECTION_SIZE ((long)0x3898)
typedef struct _pos_t {
float x;
float y;
} pos_t;
static int count;
static pos_t pos_history[1024];
static int
move_point (pos_t* pos)
{
count++;
pos->x = cosf ((float)count / 100. * M_PI);
pos->y = sinf ((float)count / 100. * M_PI);
/* for debug */
pos_history[count] = *pos;
/* error !*/
if (count > (int)(sizeof(pos_history)/ sizeof(pos_t)))
return -1;
return 0;
}
static void
dump_data_and_bss ()
{
FILE *fptr = fopen ("data_and_bss.dump", "w");
if (fptr == NULL)
{
perror ("fopen");
return;
}
int ret = fwrite (DATA_SECTION_ADDR, DATA_SECTION_SIZE, 1, fptr);
if (ret != 1)
{
fprintf (stderr, "DATA_SECTION_ADDR = %p (0x%lx bytes)\n", DATA_SECTION_ADDR, DATA_SECTION_SIZE);
perror ("fwrite data section");
goto END;
}
ret = fwrite (BSS_SECTION_ADDR, BSS_SECTION_SIZE, 1, fptr);
if (ret != 1)
{
fprintf (stderr, "BSS_SECTION_ADDR = %p (0x%lx bytes)\n", BSS_SECTION_ADDR, BSS_SECTION_SIZE);
perror ("fwrite bss section");
goto END;
}
END:
fclose (fptr);
}
int
main (void)
{
while (1)
{
pos_t pos;
int ret = move_point (&pos);
if (ret < 0)
goto ERROR;
}
exit (0);
ERROR:
dump_data_and_bss ();
exit (1);
}
このプログラムでは、メインループが動くごとに同心円上を回転する点の座標を返す、というなんの意味もないプログラムである。
今回はこの中に不具合を仕込んでいる。
エラーが発生した時(といっても毎回エラーになるのだが)に、DataセクションとBSSセクションの中身をファイルにダンプするようにしている。
さらにメモリダンプから経緯が解析できるよう、pos_history
というグローバル変数に座標の履歴を残している。
$ gcc -g -W -Wall -Werror -o draw_circle main.c -lm -static
$ ./draw_circle
$ ls -l data_and_bss.dump
-rw-rw-r-- 1 user user 21384 1月 12 17:15 data_and_bss.dump
このpos_history
に保存された値を、メモリダンプ(data_and_bss.dump)から取り出して、不具合の解析をしなければならない状況とする。
解析手順(改善前)
1. 見たい領域のシンボルのアドレスを確認する
これは簡単。
nm
コマンドで、pos_history
のアドレスは0x6ccbe0
であることがわかる。
$ nm draw_circle | grep pos_history
00000000006ccbe0 b pos_history
2. 見たい領域の型を確認する
これも今回の例では簡単。
typedef struct _pos_t {
float x;
float y;
} pos_t;
static pos_t pos_history[1028];
3. rawメモリダンプに含まれるメモリのアドレスから、見たい領域のダンプ中のオフセットを算出する
readelf
コマンドにより、.data
セクションの開始アドレスが0x6cb080
であることがわかる。
さらに、pos_history
のアドレスと比較することで、ダンプファイル中のオフセットが0x1B60
と算出できる。
$ readelf -S draw_circle | grep '\.data '
[25] .data PROGBITS 00000000006cb080 000cb080
$ echo 'obase=16;ibase=16;6CCBE0-6CB080' | bc
1B60
4. 領域の型に応じて、rawメモリダンプをパースする
ここが領域の型によっては、結構面倒。
float
(4byte)が2個でひとつのpos_t
なので、4byteずつダンプしてみる。
すると下記のような結果を得られる。
ただhexを見たら浮動小数点デコードできる方ならこれで良いのかもしれないが、私にはわかってもゼロのときと符号ぐらいである。
なんとなく雰囲気はつかめても、正しい値が入っているか、まで読み取るのは厳しい。
ここらへんで、「あーパーサ作るかー。ってかなんでfloatやねん。。。」となる。
$ hexdump -v -s 0x1B60 -n 8192 -e '"%07_ax " 4/4 "%08X " "\n"' data_and_bss.dump
0001b60 00000000 00000000 3F7FDFA9 3D00A892
0001b70 3F7F7EAE 3D809851 3F7EDD26 3DC0BBDC
0001b80 3F7DFB3B 3E00575B 3F7CD925 3E20305C
0001b90 3F7B772D 3E3FE0E3 3F79D5AE 3E5F60F1
0001ba0 3F77F511 3E7EA891 3F75D5CF 3E8ED7EC
(中略)
0003b40 3F4F1BAD 3F16792E 3F4A479C 3F1CE776
0003b50 3F45404A 3F232E4D 3F40074A 3F294BB9
解析手順(改善後)
ここからが本題。
1. デバッグセクションを取り出す
gdb
でシンボル参照するために、デバッグセクションは必要。
objcopy
コマンドで取り出しておく。
readelf
コマンドは結果を確認してるだけで、必須ではない。
$ objcopy --only-section='.debug_*' draw_circle draw_circle.debug
$ readelf -S draw_circle.debug
There are 10 section headers, starting at offset 0xbe8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .debug_aranges PROGBITS 0000000000000000 00000158
0000000000000030 0000000000000000 0 0 1
[ 2] .debug_info PROGBITS 0000000000000000 00000188
0000000000000412 0000000000000000 0 0 1
[ 3] .debug_abbrev PROGBITS 0000000000000000 0000059a
0000000000000159 0000000000000000 0 0 1
[ 4] .debug_line PROGBITS 0000000000000000 000006f3
00000000000000ed 0000000000000000 0 0 1
[ 5] .debug_str PROGBITS 0000000000000000 000007e0
00000000000002c0 0000000000000001 MS 0 0 1
[ 6] .debug_ranges PROGBITS 0000000000000000 00000aa0
0000000000000030 0000000000000000 0 0 1
[ 7] .shstrtab STRTAB 0000000000000000 00000b79
0000000000000069 0000000000000000 0 0 1
[ 8] .symtab SYMTAB 0000000000000000 00000ad0
00000000000000a8 0000000000000018 9 7 8
[ 9] .strtab STRTAB 0000000000000000 00000b78
0000000000000001 0000000000000000 0 0 1
2. .textセクションを取り出す
必須ではないが、メモリダンプを見るようなときは逆アセンブルを見たくなることも多いので、.text
セクションもあったほうが良い。
これもobjcopy
コマンドで取り出しておく。
このときstripしてシンボルテーブルを取り除かないと、あとでリンク時にエラーになるので注意。
readelf
コマンドは結果を確認してるだけで、必須ではない。
$ objcopy --only-section='.text' --strip-all draw_circle draw_circle.text
$ readelf -S draw_circle.text
There are 3 section headers, starting at offset 0x9f5c8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000400390 00000390
000000000009f224 0000000000000000 AX 0 0 16
[ 2] .shstrtab STRTAB 0000000000000000 0009f5b4
0000000000000011 0000000000000000 0 0 1
3. メモリダンプをエルフ形式にする
今回のメイン。
後でリンクできるよう、ld
コマンドでエルフ形式に変換する。
これもobjcopy
コマンドで取り出しておく。
このときstripしてシンボルテーブルを取り除かないと、あとでリンク時にエラーになるので注意。
readelf
コマンドは結果を確認してるだけで、必須では(以下ry
$ ld --format=binary --relocatable --output=data_and_bss.elf data_and_bss.dump
$ readelf -S data_and_bss.elf
There are 5 section headers, starting at offset 0x54a0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .data PROGBITS 0000000000000000 00000040
0000000000005368 0000000000000000 WA 0 0 1
[ 2] .shstrtab STRTAB 0000000000000000 0000547e
0000000000000021 0000000000000000 0 0 1
[ 3] .symtab SYMTAB 0000000000000000 000053a8
0000000000000078 0000000000000018 4 2 8
[ 4] .strtab STRTAB 0000000000000000 00005420
000000000000005e 0000000000000000 0 0 1
このとき、rawメモリダンプは.data
セクションに格納されていることがわかる。
今回のメモリダンプは.data
セクションのダンプなのでこれでいい。
しかしもし他のセクションをダンプしたものの場合はobjcopy
コマンドでセクション名を変更する必要がある。
$ objcopy --strip-all --rename-section .data=.another_section data_and_bss.elf another_section.elf
$ readelf -S another_section.elf
There are 3 section headers, starting at offset 0x53c8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .another_section PROGBITS 0000000000000000 00000040
0000000000005368 0000000000000000 WA 0 0 1
[ 2] .shstrtab STRTAB 0000000000000000 000053a8
000000000000001c 0000000000000000 0 0 1
4. 1-3のelfファイルをリンクする
いよいよ.text
セクションと.data
セクションと.debug_*
セクションをリンクする。
このとき、.text
セクションの開始アドレスを-Ttext
オプションで、.data
セクションの開始アドレスを-Tdata
オプションで指定できる。
もしその他のセクションとしてリンクする場合は、より汎用的な--section-start
オプションで指定すればいい。
もちろんリンカスクリプトで記述することもできる。
readelf
コマンドの結果から、欲しいセクションのデータが正しいアドレスに格納できていることがわかる。
$ ld -Ttext=0x00400390 -Tdata=0x006cb080 draw_circle.debug draw_circle.text data_and_bss.elf -o draw_circle.gdb
$ readelf -S draw_circle.gdb
There are 12 section headers, starting at offset 0xd0fc8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000400390 00000390
000000000009f224 0000000000000000 AX 0 0 16
[ 2] .data PROGBITS 00000000006cb080 000cb080
0000000000005368 0000000000000000 WA 0 0 1
[ 3] .debug_aranges PROGBITS 0000000000000000 000d03e8
0000000000000030 0000000000000000 0 0 1
[ 4] .debug_info PROGBITS 0000000000000000 000d0418
0000000000000412 0000000000000000 0 0 1
[ 5] .debug_abbrev PROGBITS 0000000000000000 000d082a
0000000000000159 0000000000000000 0 0 1
[ 6] .debug_line PROGBITS 0000000000000000 000d0983
00000000000000ed 0000000000000000 0 0 1
[ 7] .debug_str PROGBITS 0000000000000000 000d0a70
00000000000002c0 0000000000000001 MS 0 0 1
[ 8] .debug_ranges PROGBITS 0000000000000000 000d0d30
0000000000000030 0000000000000000 0 0 1
[ 9] .shstrtab STRTAB 0000000000000000 000d0f51
0000000000000075 0000000000000000 0 0 1
[10] .symtab SYMTAB 0000000000000000 000d0d60
0000000000000180 0000000000000018 11 9 8
[11] .strtab STRTAB 0000000000000000 000d0ee0
0000000000000071 0000000000000000 0 0 1
5. gdb
で読み込む
$ gdb draw_circle.gdb
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
(中略)
Reading symbols from draw_circle.gdb...done.
(gdb) set print pretty on
(gdb) p pos_history
$1 = {{
x = 0,
y = 0
}, {
x = 0.999506533,
y = 0.0314107612
}, {
x = 0.998026729,
y = 0.0627905205
}, {
x = 0.995561957,
y = 0.0941083133
(中略)
(gdb) p pos_history[1023]
$3 = {
x = 0.750111222,
y = 0.661311686
}
(gdb) p pos_history[1024]
$4 = {
x = 0.728967488, <-------- overrun!!!
y = 0.684548318 <-------- overrun!!!
}
(gdb) p pos_history[1025]
$5 = {
x = 0.707106948, <-------- overrun!!!
y = 0.70710659 <-------- overrun!!!
}
(gdb) p pos_history[1026]
$6 = {
x = 0,
y = 0
}
(gdb) ptype pos_history
type = struct _pos_t {
float x;
float y;
} [1024]
gdbがデバッグセクションからシンボル情報を読み取ってくれるので、普段のデバッグと変わらない気軽さで変数の値を参照できる。
このように見れれば、不具合(今回の場合だとメモリオーバーラン)も見つけやすいんじゃないだろうか。
ステップ数が増えているから複雑に感じるかもしれないが、次節のようにMakefileを作っておけばほぼ自動化できるうえに、
一度このgdb用ファイルを作ってしまえば見たいところを型によらずに解析できるので楽ちん。
rawメモリダンプを手渡されたときにはぜひ。
Makefile
下記Makefile含め、今回のコードはこちらに置いている。
EXEC=draw_circle
DUMP=data_and_bss
$(EXEC): main.c
gcc -g -W -Wall -Werror -o $@ $^ -lm -static
$(DUMP).dump: $(EXEC)
./$(EXEC); true
$(DUMP).elf: $(DUMP).dump
${LD} --format=binary --relocatable --output=$@ $^
$(EXEC).debug: $(EXEC)
objcopy --only-section='.debug_*' $< $@
$(EXEC).text: $(EXEC)
objcopy --only-section='.text' --strip-all $< $@
$(EXEC).gdb: $(EXEC).debug $(EXEC).text $(DUMP).elf
# ld -T dump.ld $^ -o $@
ld -Ttext=0x00400390 -Tdata=0x006cb080 $^ -o $@
clean:
rm -f $(EXEC) *.o *~ *.dump *.text *.debug *.gdb *.elf
.PHONY: clean
補足
ちなみにPartnerJet2では、rd
コマンドを使ってオフラインで生メモリダンプをロードできる。
PartnerJet2のライセンスが十分にあり、操作にも習熟してる方はそちらの方法を使えば良いと思う。
自分はgdbのほうが慣れているので同様の機能を探したが、見つからなかったので今回のこの調査を行った。
もしgdbのそのような機能をご存知の方がいたら、ぜひ教えてください。
参考
Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification
Anatomy of an ELF core file
A brief look into core dumps
Linux Effective Coredump
objcopy(1)
ld(1)