FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Elfファイルからシンボルを取り出してシミュレータでトレースを表示する機能の実装

RISC-Vシミュレータにはelfファイルを読み込ませているのだが、elfファイルにはいろんな情報が取り込まれており、例えば

  • テキスト領域の関数のヘッダアドレス
  • グローバルデータが格納されているアドレス

などの情報が格納されている。

シミュレータは、テキスト領域の命令と、データ領域の初期値をロードするのだが、それ以外にも、関数の先頭アドレスや、シンボルのアドレスをロードすることによってより多くのデバッグ情報を出力することができる。 今回、RISC-Vシミュレータに追加したのは、関数が配置されているアドレス一覧を読み込んで、プログラムがどのような順番で関数をジャンプしていったのかをトレースすることができる「サブルーチントレース」(自称)の機能だ。

f:id:msyksphinz:20180612022439p:plain
図. サブルーチントレースの実行結果。関数がどのようにしてジャンプし、どこから戻ってきたかをトレースする。このプログラムでは外部I/Oの問題でプログラムが最終的にクラッシュしている...

どのようにしてサブルーチントレースを追加するか

サブルーチントレースの機能は、主に2つに分けられている。

  • 関数ジャンプを検出する部分
  • 関数から戻ってきたことを検出する部分

関数ジャンプの検出

まず、関数ジャンプをする部分の検出は2つの手法が考えられる。

  1. ジャンプ命令を検出する。
  2. ラベル一覧と比較し、新たなラベルに到達するとそれを関数ジャンプとみなす。

  3. の方法は、アーキテクチャ毎に、どの命令が関数ジャンプなのかを登録する必要がある。これはアーキテクチャ毎の項目が多くなり汎用的ではない。 一方で2. の方法は、命令がラベル付きのアドレスに到達するとそれは関数ジャンプとみなす。命令に限定しないので、より汎用的だといえる。 一方で2. の問題点は、関数コールのつもりはないのに、ラベルを踏むとそれだけで関数コールと判定されてしまう可能性がある。 したがって、ある程度のフィルタをかけなければならず。RISCプロセッサの場合は対外関数コールに使われる命令というのは、

  4. プログラムカウンタをアップデートする。

  5. プログラムカウンタ以外のレジスタをアップデータする(戻り先アドレスの保存)

となるので、この条件を満たし、さらにラベルを踏んだ命令は関数ジャンプとみなす。

  • 実装した関数ジャンプ検出のコード。
void TraceInfo::HierFunctionCall (Addr_t fetch_pc)
{
  if (m_hier_func_in_skip == true) {
    return;
  }

  // Jump to Function

  Addr_t jump_pc;
  bool is_find_jump_pc = FindPCUpdate (&jump_pc);

  std::string func_symbol;
  if (is_find_jump_pc) {
    if (m_pe_thread->FindSymbol (jump_pc, &func_symbol) == true) {
      for (uint32_t i = 0; i < GetHierDepth (); i++) {
        fprintf (GetTraceHierFp(), "  ");
      }
      std::stringstream str;
      str << "<FunctionCall " << GetStep() << " " << func_symbol << "(0x"
          << std::hex << std::setw(8) << std::setfill('0') << jump_pc << ")";
      fprintf (GetTraceHierFp(), "%s", str.str().c_str());
      SetHierDepth (GetHierDepth()+1);
      PairStackTrace *stack_trace = new PairStackTrace();
#ifdef ARCH_RISCV
      Addr_t next_pc = fetch_pc + RiscvDec::GetInstLength (GetInstIdx()) / 8;
#endif // ARCH_RISCV
      *stack_trace = std::make_pair (next_pc, func_symbol);
      m_hier_stack.push_back (stack_trace);

      // find skip list
      for (auto it = m_hier_skip_func.begin ();
           it != m_hier_skip_func.end ();
           it++) {
        if ((*it)->first == func_symbol) {
          m_hier_func_in_skip = true;
          if ((*it)->second == InstSkip_t::InstSkip) {
            m_hier_debug_in_skip = true;
          }
          fprintf (GetTraceHierFp(), " ...");
          break;
        }
        it++;
      }
      fprintf (GetTraceHierFp(), ">\n");
    }
  }
}

関数から戻ってきたことを検出する

これは、関数ジャンプ検出時にヒントを挿入しておく。関数から戻ってくるときは、そのジャンプ命令の次の命令に戻ってくることが大半だ(そうでない場合もあるが...)。

そこで、関数ジャンプが発生した際に、「関数ジャンプが実行された命令のプログラムアドレス+関数ジャンプ命令の命令長」戻り先の候補アドレスになり、それを戻り先候補アドレスとして登録する。 そしてシミュレーション上で、戻り先候補アドレスを踏むとそれを関数から戻ってきたこととみなす。

これらの情報をもとに、トレースファイルを作成すれば、上記のように関数のジャンプ履歴を取得することができる。

  • 実装した関数から戻ってくることを検出するコード
void TraceInfo::HierReturn (Addr_t fetch_pc)
{
  // Return from Function

  if (!m_hier_stack.empty ()) {
    bool is_found_pc = false;
    int32_t pop_count = 0;
    auto trace_it = m_hier_stack.end();
    trace_it--;
    for (; trace_it != m_hier_stack.begin(); trace_it--) {
      if ((*trace_it)->first == fetch_pc) {
        is_found_pc = true;
        break;
      }
      pop_count++;
    }
    if (is_found_pc == true) {
      while (pop_count-- >= 0) {
        m_hier_stack.pop_back();
        SetHierDepth (GetHierDepth()-1);
      }
      for (uint32_t i = 0; i < GetHierDepth (); i++) {
        fprintf (GetTraceHierFp(), "  ");
      }
      fprintf (GetTraceHierFp(), "<Return: %s>\n", (*trace_it)->second.c_str());

      m_hier_func_in_skip  = false;
      m_hier_debug_in_skip = false;
    }
  }
}