ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

SCR1の解析 - riscv-tests処理内容の確認

スポンサーリンク

昨日の記事でVivadoシミュレータ上で実行したriscv-testsのaddiテストが正常に終了しない件についての調査を行い、テストベンチ上の$finishに辿り着く条件を満たしていないことがわかった。

www.tech-diningyo.info

結構SCR1の中身を真面目に追っていかないとわからない感じがしたが、そもそもこのriscv-testsで何が実行されているのかをちゃんとは把握していないのでriscv-testsの中身の確認をしていく。ということもありタイトルもそれっぽく変更。

addi.Sの中身の確認

現在実行中のaddi命令の試験についての大まかな流れを確認していく。

現在Vivadoシミュレータ上にインスタンスされているSCR1は32bitCPUなのでrv32uiのディレクトリの下のコードがビルドされているが、中身はrv64uiへのインクルードパスが記載されており実際にコードとしては使用されるのはrv64uiのaddiテストになる。

# See LICENSE for license details.

#*****************************************************************************
# addi.S
#-----------------------------------------------------------------------------
#
# Test addi instruction.
#

#include "riscv_test.h"
#include "test_macros.h"

RVTEST_RV64U
RVTEST_CODE_BEGIN

  #-------------------------------------------------------------
  # Arithmetic tests
  #-------------------------------------------------------------

  TEST_IMM_OP( 2,  addi, 0x00000000, 0x00000000, 0x000 );
  TEST_IMM_OP( 3,  addi, 0x00000002, 0x00000001, 0x001 );
  TEST_IMM_OP( 4,  addi, 0x0000000a, 0x00000003, 0x007 );

  TEST_IMM_OP( 5,  addi, 0xfffffffffffff800, 0x0000000000000000, 0x800 );
  TEST_IMM_OP( 6,  addi, 0xffffffff80000000, 0xffffffff80000000, 0x000 );
  TEST_IMM_OP( 7,  addi, 0xffffffff7ffff800, 0xffffffff80000000, 0x800 );

  TEST_IMM_OP( 8,  addi, 0x00000000000007ff, 0x00000000, 0x7ff );
  TEST_IMM_OP( 9,  addi, 0x000000007fffffff, 0x7fffffff, 0x000 );
  TEST_IMM_OP( 10, addi, 0x00000000800007fe, 0x7fffffff, 0x7ff );

  TEST_IMM_OP( 11, addi, 0xffffffff800007ff, 0xffffffff80000000, 0x7ff );
  TEST_IMM_OP( 12, addi, 0x000000007ffff7ff, 0x000000007fffffff, 0x800 );

  TEST_IMM_OP( 13, addi, 0xffffffffffffffff, 0x0000000000000000, 0xfff );
  TEST_IMM_OP( 14, addi, 0x0000000000000000, 0xffffffffffffffff, 0x001 );
  TEST_IMM_OP( 15, addi, 0xfffffffffffffffe, 0xffffffffffffffff, 0xfff );

  TEST_IMM_OP( 16, addi, 0x0000000080000000, 0x7fffffff, 0x001 );

  #-------------------------------------------------------------
  # Source/Destination tests
  #-------------------------------------------------------------

  TEST_IMM_SRC1_EQ_DEST( 17, addi, 24, 13, 11 );

  #-------------------------------------------------------------
  # Bypassing tests
  #-------------------------------------------------------------

  TEST_IMM_DEST_BYPASS( 18, 0, addi, 24, 13, 11 );
  TEST_IMM_DEST_BYPASS( 19, 1, addi, 23, 13, 10 );
  TEST_IMM_DEST_BYPASS( 20, 2, addi, 22, 13,  9 );

  TEST_IMM_SRC1_BYPASS( 21, 0, addi, 24, 13, 11 );
  TEST_IMM_SRC1_BYPASS( 22, 1, addi, 23, 13, 10 );
  TEST_IMM_SRC1_BYPASS( 23, 2, addi, 22, 13,  9 );

  TEST_IMM_ZEROSRC1( 24, addi, 32, 32 );
  TEST_IMM_ZERODEST( 25, addi, 33, 50 );

  TEST_PASSFAIL

RVTEST_CODE_END

  .data
RVTEST_DATA_BEGIN

  TEST_DATA

RVTEST_DATA_END

上記のようにRVTEST_CODE_BEGINで各種初期化を実行した後、addi命令の基本的な処理をTEST_IMM_OPTEST_IMM_DEST_BYPASSといったマクロに渡して命令のテストを行っていく。

RVTEST_CODE_BEGINで各種初期化

まずはRVTEST_CODE_BEGINから。オリジナルのriscv-testsのgithubではこのマクロはsubmoduleとして登録されているriscv-envに登録されているriscv_test.hに実装されているが、SCR1の環境ではこのファイルに実装されている各種テスト用マクロと、riscv-tests/macros/scalar/test_macros.hに登録されている各テスト実行用のマクロをまとめたファイルがtests/common/riscv_macros.hに用意されている。

#define RVTEST_CODE_BEGIN                                               \
        .section .text.init;                                            \
        .org 0xC0, 0x00;                                                \
        .align  6;                                                      \
        .weak stvec_handler;                                            \
        .weak mtvec_handler;                                            \
trap_vector:                                                            \
        /* test whether the test came from pass/fail */                 \
        csrr t5, mcause;                                                \
        li t6, CAUSE_USER_ECALL;                                        \
        beq t5, t6, _report;                                       \
        li t6, CAUSE_SUPERVISOR_ECALL;                                  \
        beq t5, t6, _report;                                       \
        li t6, CAUSE_MACHINE_ECALL;                                     \
        beq t5, t6, _report;                                       \
        /* if an mtvec_handler is defined, jump to it */                \
        la t5, mtvec_handler;                                           \
        beqz t5, 1f;                                                    \
        jr t5;                                                          \
        /* was it an interrupt or an exception? */                      \
1:      csrr t5, mcause;                                                \
        bgez t5, handle_exception;                                      \
        INTERRUPT_HANDLER;                                              \
handle_exception:                                                       \
        /* we don't know how to handle whatever the exception was */    \
other_exception:                                                        \
        /* some unhandlable exception occurred */                       \
        li   a0, 0x1;                                                   \
_report:                                                                \
        j sc_exit;                                                      \
        .align  6;                                                      \
        .globl _start;                                                  \
_start:                                                                 \
        RISCV_MULTICORE_DISABLE;                                        \
        /*INIT_SPTBR;*/                                                 \
        /*INIT_PMP;*/                                                   \
        DELEGATE_NO_TRAPS;                                              \
        li TESTNUM, 0;                                                  \
        la t0, trap_vector;                                             \
        csrw mtvec, t0;                                                 \
        CHECK_XLEN;                                                     \
        /* if an stvec_handler is defined, delegate exceptions to it */ \
        la t0, stvec_handler;                                           \
        beqz t0, 1f;                                                    \
        csrw stvec, t0;                                                 \
        li t0, (1 << CAUSE_LOAD_PAGE_FAULT) |                           \
               (1 << CAUSE_STORE_PAGE_FAULT) |                          \
               (1 << CAUSE_FETCH_PAGE_FAULT) |                          \
               (1 << CAUSE_MISALIGNED_FETCH) |                          \
               (1 << CAUSE_USER_ECALL) |                                \
               (1 << CAUSE_BREAKPOINT);                                 \
        csrw medeleg, t0;                                               \
        csrr t1, medeleg;                                               \
        bne t0, t1, other_exception;                                    \
1:      csrwi mstatus, 0;                                               \
        init;                                                           \
        EXTRA_INIT;                                                     \
        EXTRA_INIT_TIMER;                                               \
        la t0, _run_test;                                               \
        csrw mepc, t0;                                                  \
        csrr a0, mhartid;                                               \
        mret;                                                           \
        .section .text;                                                 \
_run_test:

やってることをざっくり書くと以下の通り。

  • trap_vectorの実装
    • .org 0xc0, 0x00に従いtrap_vector`は0x1c0を先頭に配置される。(0x100はリンカスクリプトの指定)
    • 各モードごとにエントリが定義されており、ecall実行時のモードに対応したエントリが実行される
    • _reportではsc_exitへジャンプして処理終了。
    • sc_exit自体はCRTに定義されているが、アドレス自体はが前回の記事で書いたシミュレーション終了条件となるアドレスに一致する。
  • _startの宣言と実装

因みに上記のアセンブラコード中で使用されているa0やらt0やらはRISC-Vの持っているレジスタ(x0-x31とか)を指しており、コンパイラでどのように使用するかによって名前が割り振られている。最初、何も知らずにアセンブラみて混乱したので一応。

対応は以下の通りで、ここではSCR1に関係しているx0-x31のみ記載(この表自体はuser-level ISAのchapter 20に記載されている)。

Register ABI Name(※) Description Saver
x0 zero Hard-wired zero -
x1 ra Return address Caller
x2 sp Stack pointer Callee
x3 gp Global pointer -
x4 tp Thread pointer -
x5 t0 Temporary/alternate link register Caller
x6-7 t1-2 Temporaries Caller
x8 s0/fp Saved register/frame pointer Callee
x9 s1 Saved register Callee
x10-11 a0-1 Function arguments/return values Caller
x12-17 a2-7 Function arguments Caller
x18-27 s2-11 Saved registers Callee
x28-31 t3-6 Temporaries Caller

※ABI(Application Binary Interface)の略らしい。

因みに以下にこの辺のアセンブラのことがまとめてられているのでこちらも参照するとわかりやすいはず。

github.com

各命令のテスト

tests/common/riscv_macros.hに定義されているテスト用マクロを使って命令のテストを実行する。

一例としてTEST_IMM_OPの中身は以下のようになっている。(ファイルはsim/tests/common/riscv_macros.h

#define TEST_IMM_OP( testnum, inst, result, val1, imm ) \
    TEST_CASE( testnum, x30, result, \
      li  x1, MASK_XLEN(val1); \
      inst x30, x1, SEXT_IMM(imm); \
)

マクロの中ではTEST_CASEマクロを呼んでおり、この中でテストが実行される↓

#define TEST_CASE( testnum, testreg, correctval, code... ) \
test_ ## testnum: \
    code; \
    li  x29, MASK_XLEN(correctval); \
    li  TESTNUM, testnum; \
bne testreg, x29, fail;

実際の処理としてはTEST_IMM_OPinst引数に設定した命令(今回はaddi)を実行した結果を格納したx30と期待値が格納されているx29を比較して値が不一致の場合にfailラベルにジャンプする。

テストのPASS/FAIL判定

このfailラベルは各テストの最後に実行されるTEST_PASSFAILマクロに定義されている。

#define TEST_PASSFAIL \
        bne x0, TESTNUM, pass; \
fail: \
        RVTEST_FAIL; \
pass: \
        RVTEST_PASS \

fail/passラベルで実行されるのはriscv_test.hにて定義されるRVTEST_FAIL/RVTEST_PASSマクロでa1レジスタに最後に実行された試験番号(TEST_NUM)、a0レジスタにテスト結果(0x0:PASS/0x1:FAIL)を格納して対象命令の試験が終了となる。

#define RVTEST_PASS                                                     \
        fence;                                                          \
        mv a1, TESTNUM;                                                 \
        li  a0, 0x0;                                                    \
        ecall

#define TESTNUM x28
#define RVTEST_FAIL                                                     \
        fence;                                                          \
        mv a1, TESTNUM;                                                 \
        li  a0, 0x1;                                                    \
        ecall

ということで流れをまとめると

  1. RVTEST_CODE_BEGINでテストの準備
  2. TEST_IMM_OP等のテスト用マクロを使ってテストを実行
    1. 各テスト結果がPASSならそのまま次のテストへ
    2. FAILの場合には、failラベルにジャンプして試験終了
  3. テストの結果自体は最終状態におけるa0レジスタの値をチェックすればわかるようになっている。
  4. PASS or FAILにかかわらず最終的にはecallが発行される

これで大体内容は把握できたかな。やっぱり流れ的には最後のRVTEST_PASS/RVTEST_FAILからtrap_handlerへジャンプした後にsc_exitを経て最終アドレスな気がする。

ということはecall命令がうまく処理されていないことになるが、そもそもecall命令って何??な状態なので次回はecall命令の中身を把握するところからやっていきたい。