FPGA開発日記

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

関数の呼び出し関係をトレースとして表示する機能を追加

ISSに関数呼出のトレースを表示する機能を追加した。

    --trace_hier  : Generate Trace Hierachy tree
    --trace_out <log> : output log filename for trace hierarchy mode
                        default is same as --out <filename>

--trace_hier オプションを有効化すると、以下のような関数呼出の関係を示すトレース情報が出力されるようになった。 実はこれがずっと前から欲しかった機能で、新しいベンチマークなどを解析する場合に非常に重要になる機能だ。

github.com

          <Return: calc_func>
          <FunctionCall 53570: calc_func(0x80000058)>
            <FunctionCall 53594: crcu16(0x800015c0)>
            <Return: crcu16>
          <Return: calc_func>
        <Return: cmp_complex>
        <FunctionCall 53828: cmp_complex(0x80000180)>
          <FunctionCall 53837: calc_func(0x80000058)>
            <FunctionCall 53860: core_bench_matrix(0x80000eec)>
              <FunctionCall 53875: matrix_test(0x80000d04)>
                <FunctionCall 55136: matrix_sum(0x80000a74)>
                <Return: matrix_sum>
                <FunctionCall 56328: crc16(0x8000169c)>
                  <FunctionCall 56331: crcu16(0x800015c0)>
                <Return: crc16>
                <FunctionCall 56506: matrix_mul_vect(0x80000ba8)>
                <Return: matrix_mul_vect>
                <FunctionCall 57307: matrix_sum(0x80000a74)>
                <Return: matrix_sum>
                <FunctionCall 58418: crc16(0x8000169c)>
                  <FunctionCall 58421: crcu16(0x800015c0)>
                <Return: crc16>
                <FunctionCall 58581: matrix_mul_matrix(0x80000bfc)>
                <Return: matrix_mul_matrix>
                <FunctionCall 65775: matrix_sum(0x80000a74)>
                <Return: matrix_sum>
                <FunctionCall 66891: crc16(0x8000169c)>
                  <FunctionCall 66894: crcu16(0x800015c0)>
                <Return: crc16>
                <FunctionCall 67062: matrix_mul_matrix_bitextract(0x80000c74)>
                <Return: matrix_mul_matrix_bitextract>
                <FunctionCall 78630: matrix_sum(0x80000a74)>
                <Return: matrix_sum>
                <FunctionCall 79822: crc16(0x8000169c)>
                  <FunctionCall 79825: crcu16(0x800015c0)>
                <Return: crc16>
              <Return: matrix_test>
              <FunctionCall 80539: crc16(0x8000169c)>
                <FunctionCall 80542: crcu16(0x800015c0)>
            <Return: core_bench_matrix>
            <FunctionCall 80715: crcu16(0x800015c0)>
            <Return: crcu16>
          <Return: calc_func>
          <FunctionCall 80900: calc_func(0x80000058)>
            <FunctionCall 80923: core_bench_matrix(0x80000eec)>
              <FunctionCall 80938: matrix_test(0x80000d04)>
                <FunctionCall 82199: matrix_sum(0x80000a74)>
                <Return: matrix_sum>
                <FunctionCall 83391: crc16(0x8000169c)>
                  <FunctionCall 83394: crcu16(0x800015c0)>

基本的に、シミュレーション中に自分のPCアドレスを検索しながら、ジャンプ先のPCが関数の先頭ならば、関数コールされたと判定する。 関数の飛び先について、スタックに保存しておき、関数から戻ってくるときは、戻ってきた先のPCと、スタックに保存していたPCが同一ならば、関数から戻ってきたと判定する。

void traceInfo::HierFunctionCall (Addr_t fetch_pc)
{
    // Jump to Function

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

    std::string func_symbol;
    if (is_find_jump_pc &&
        m_env->FindSymbol (jump_pc, &func_symbol) == true) {
        for (int i = 0; i < GetHierDepth (); i++) {
            fprintf (GetTraceHierFp(), "  ");
        }
        fprintf (GetTraceHierFp(), "<FunctionCall %d: %s(0x%08x)>\n",
                 GetStep (),
                 func_symbol.c_str(), jump_pc, fetch_pc + 8);
        SetHierDepth (GetHierDepth()+1);
        PairStackTrace *stack_trace = new PairStackTrace();
        Addr_t next_pc;
#ifdef ARCH_MIPS
        next_pc = fetch_pc + 8;
#else  // ARCH_MIPS
#ifdef ARCH_RISCV
        next_pc = fetch_pc + 4;
#else  // ARCH_RISCV
#endif // ARCH_RISCV
#endif // ARCH_MIPS
        *stack_trace = std::make_pair (next_pc, func_symbol);
        m_hier_stack.push_back (stack_trace);
    }
}

飛び先のPC(jump_pc)を検索し、シンボルテーブルの中にそのPCが存在していれば、関数にジャンプしたと判定する。 一方で、m_hier_stackには、ペアの情報として、戻ってくるPC(MIPSの場合は遅延分岐があるのでPC+8、RISC-Vの場合はPC+4)と、関数の名前を保存している。

void traceInfo::HierReturn (Addr_t fetch_pc)
{
    // Return from Function

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

同様に、関数の戻りについても判定しており、関数のスタックのトップから順に検索していき、自分のPC(fetch_pc)と一致すれば、関数から戻ってきたと判定する。

ここで、単純にスタックを利用していないのには理由がある。 例えば、

void funcA() {
   funcB ();
}

というような関数が存在した場合、スタック的には、

[Top]
funcB
funcA
[Bottom]

と格納されているが、実際にコンパイルされたときに、戻りアドレスが省略されることにより、

  • 理想
funcAの呼出元 --> funcA --> funcB --> funcA --> funcAの呼出元

となれば良いのだが、実際には最適化により、

funcAの呼出元-->funcA-->funcB-->funcAの呼出元

となり、funcBの戻りアドレスがfuncAでない時がある。こうするとスタックの一番上だけ検索していてば、戻りアドレスがいつまでも検索できないことになってしまう。

そこで、スタックの先頭から順にアドレスを探索していき、スタックの中で該当アドレスに引掛った場合、そこまで呼出スタックを戻す。 下記が、そのスタックの一番上からアドレスが見付かるまで探索しているコードだ。

        while (trace_it != m_hier_stack.begin()) {
            if ((*trace_it)->first == fetch_pc) {
                is_found_pc = true;
                break;
            }
            trace_it--;
            pop_count++;
        }

これにより、gccにより最適化がされて、プログラム的な呼出関係が崩れたとしても、正しくトレースを続けることができるようになった。