LoginSignup
10
10

More than 5 years have passed since last update.

rawメモリダンプをgdbでオフラインデバッグする方法

Last updated at Posted at 2019-01-12

やりたいこと

組込み開発をしていると、問題発生時のrawメモリダンプから解析をする必要があったりする。
その場合、対応する実行ファイルのシンボルから、おかしそうな領域に対応するダンプデータを見ることになる。
ただ、意外とシンボルのアドレスや型の確認とそれに合わせたダンプの内容確認が面倒くさい。

  1. 見たい領域のシンボルのアドレスを確認する
  2. 見たい領域の型を確認する
  3. rawメモリダンプに含まれるメモリのアドレスから、見たい領域のダンプ中のオフセットを算出する
  4. 領域の型に応じて、rawメモリダンプをパースする

もちろん上記を手順を一度通せば、同じシンボルの内容を確認するのはある程度プログラム化できる。
とはいえ、確認しなければ変数・型・アドレスは当然毎回違うので、結局大体都度上記の手順を踏むこととなる。

そこをgdb上でメモリの内容をシンボルから参照したい、というのが今回の記事のやりたいことである。

なお、ここで想定しているのは、linuxのように素敵なELFフォーマットのコアダンプが取得できる環境ではない。
本当にただメモリの生データをそのままファイルに落としただけのものが手に入る状況とする。

やること(概要)

ざっくり言えば、今回の目的を達成するために必要なことは
「rawメモリダンプと実行ファイルをリンクして、gdbに読み込む」
である。

やること(詳細)

下記のようなプログラムとメモリダンプを例に、詳細な手順を示す。
時間がない人は、「解析手順(改善後)」まで飛ばしてください。

メモリダンプを解析するプログラムの例

main.c
#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. 見たい領域の型を確認する

これも今回の例では簡単。

main.cから抜粋
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)

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10