RISC-V를 분석하면 가장 이해하기 어려운 콤포넌트는 opensbi이다. opensbi는 슈퍼바이저 모드와 
머신 모드의 인터페이스라고 할 수 있으며, 리눅스 커널의 가장 낮은 소프트웨어 레이어라고 할 수 있다.

리눅스 커널에서 RISC-V에 dependent한 동작을 정확히 이해하려면, 역시나 opensbi를 이해해야 한다.

이번 포스트에서는 opensbi 스펙 중에 marchid를 중심으로 소프트웨어의 실행 흐름을 정리한다.
ChatGPT이 이런 내용을 알려주지 않으니 많은 개발자에게 도움이 됐으면 좋겠다.

RISC-V 단체에서 기술한 opensbi 스펙 문서를 보자.

4.6. Function: Get machine architecture ID (FID #5)
struct sbiret sbi_get_marchid(void);

https://lists.riscv.org/g/tech-brs/attachment/361/0/riscv-sbi.pdf#:~:text=Function:%20Firmware%20Features%20Set%20(FID%20%230)%20struct,for%20which%20per%20feature%20supported%20values%20are

marchid를 읽어오는 인터페이스이다. 리눅스 커널에서 marchid에 대한 opensbi는 어떻게 구현됐을까?

다음 함수를 보자.

rch/riscv/kernel/sbi.c
long sbi_get_marchid(void)
{
return __sbi_base_ecall(SBI_EXT_BASE_GET_MARCHID);
}

어셈블리 명령어로 보면 sbi_get_marchid 함수의 구현부는 아래와 같다:

      SP:FFFFFFFF8000B3BA|sbi_get_marchid:   c.addi     sp,-0x10      ; sp,-16
      SP:FFFFFFFF8000B3BC|                   c.sdsp     s0,0x8(sp)    ; s0,8(sp)
      SP:FFFFFFFF8000B3BE|                   c.addi4spn s0,0x10       ; s0,16
      SP:FFFFFFFF8000B3C0|                   c.li       a0,0x0
      SP:FFFFFFFF8000B3C2|                   c.li       a1,0x0
      SP:FFFFFFFF8000B3C4|                   c.li       a2,0x0
      SP:FFFFFFFF8000B3C6|                   c.li       a3,0x0
      SP:FFFFFFFF8000B3C8|                   c.li       a4,0x0
      SP:FFFFFFFF8000B3CA|                   c.li       a5,0x0
      SP:FFFFFFFF8000B3CC|                   c.li       a6,0x5
      SP:FFFFFFFF8000B3CE|                   c.li       a7,0x10       ; a7,16
      SP:FFFFFFFF8000B3D0|                   ecall

a7 레지스터에 0x10을 로딩한 다음에 ecall 명령어를 실행한다. 이 명령어를 실행하면 바로
머신 모드로 트랩이 유발된다.

이제부터는 opensbi 코드 분석이다.

00000000000003c8 <_trap_handler>:
     3c8:       34021273                csrrw   tp,mscratch,tp
     3cc:       06523023                sd      t0,96(tp) # 60 <_try_lottery+0x36>
     3d0:       300022f3                csrr    t0,mstatus
[...]
     468:       3002b073                csrc    mstatus,t0
     46c:       00010533                add     a0,sp,zero
     470:       16e0c0ef                jal     ra,c5de <sbi_trap_handler>

리눅스 커널에서 ecall 명령어를 실행하면, 트랩이 유발되면서 _trap_handler 레이블의 시작 주소로 점프한다.
그 다음에 sbi_trap_handler 함수로 분기한다.

참고로 opensbi에서 트랩 핸들러를 설정하는 루틴은 아래와 같다:

opensbi/firmware/fw_base.S
        /* Setup trap handler */
        lla     a4, _trap_handler
        csrr    a5, CSR_MISA
        srli    a5, a5, ('H' - 'A')
        andi    a5, a5, 0x1
        beq     a5, zero, _skip_trap_handler_hyp
        lla     a4, _trap_handler_hyp
_skip_trap_handler_hyp:
        csrw    CSR_MTVEC, a4

'csrw    CSR_MTVEC, a4'이 핵심 명령어이다. 


다시 원래 주제로 돌아와서 sbi_trap_handler() 함수를 분석하자.
함수의 분석 내용은 주석을 참고하자.

struct sbi_trap_context *sbi_trap_handler(struct sbi_trap_context *tcntx)
{
        int rc = SBI_ENOTSUPP;
        const char *msg = "trap handler failed";
        struct sbi_scratch *scratch = sbi_scratch_thishart_ptr();
        const struct sbi_trap_info *trap = &tcntx->trap;
        struct sbi_trap_regs *regs = &tcntx->regs;
        ulong mcause = tcntx->trap.cause;

        /* Update trap context pointer */
        tcntx->prev_context = sbi_trap_get_context(scratch);
        sbi_trap_set_context(scratch, tcntx);

        /* Austin: mcause 레지스터의 최상위 비트가 1인지를 체크한다. 만약 true이면 트렙의
                       종류는 인터럽트이다. */
        if (mcause & MCAUSE_IRQ_MASK) {
                if (sbi_hart_has_extension(sbi_scratch_thishart_ptr(),
                                           SBI_HART_EXT_SMAIA))
                        rc = sbi_trap_aia_irq();
                else
                        rc = sbi_trap_nonaia_irq(mcause & ~MCAUSE_IRQ_MASK);
                msg = "unhandled local interrupt";
                goto trap_done;
        }

        /* Austin: mcause 레지스터는 익셉션 코드 정보를 저장한다. 이 값에 따라 다른 방식으로
                      트랩을 처리한다. mcause는 Armv8-A의 esr_el3에 대응된다(개념적으로)  */
       switch (mcause) {
        case CAUSE_ILLEGAL_INSTRUCTION:
                rc  = sbi_illegal_insn_handler(tcntx);
                msg = "illegal instruction handler failed";
                break;
        case CAUSE_MISALIGNED_LOAD:
                sbi_pmu_ctr_incr_fw(SBI_PMU_FW_MISALIGNED_LOAD);
                rc  = sbi_misaligned_load_handler(tcntx);
                msg = "misaligned load handler failed";
                break;
        case CAUSE_MISALIGNED_STORE:
                sbi_pmu_ctr_incr_fw(SBI_PMU_FW_MISALIGNED_STORE);
                rc  = sbi_misaligned_store_handler(tcntx);
                msg = "misaligned store handler failed";
                break;
        case CAUSE_SUPERVISOR_ECALL:
        case CAUSE_MACHINE_ECALL:
               */ Austin: 리눅스 커널에서 ecall을 실행했으니, 이 루틴으로 분기한다 */
                rc  = sbi_ecall_handler(tcntx);
                msg = "ecall handler failed";
                break;
        case CAUSE_LOAD_ACCESS:
                sbi_pmu_ctr_incr_fw(SBI_PMU_FW_ACCESS_LOAD);
                rc  = sbi_load_access_handler(tcntx);
                msg = "load fault handler failed";
                break;

sbi_ecall_handler() 함수를 분석하자. 분석 내용은 주석을 참고하자.

lib/sbi/sbi_ecall.c
int sbi_ecall_handler(struct sbi_trap_context *tcntx)
{
        int ret = 0;
        struct sbi_trap_regs *regs = &tcntx->regs;
        struct sbi_ecall_extension *ext;
        unsigned long extension_id = regs->a7;
        unsigned long func_id = regs->a6;
        struct sbi_ecall_return out = {0};
        bool is_0_1_spec = 0;

        ext = sbi_ecall_find_extension(extension_id);

위 루틴에서 regs->a7은 0x10이고 regs->a6이다. 왜냐면, 리눅스 커널에서 ecall 명령어를 실행하기 직전에
a6와 a7 레지스터를 다음과 같이 지정했기 때문이다.

      SP:FFFFFFFF8000B3BA|sbi_get_marchid:   c.addi     sp,-0x10      ; sp,-16
      [...]
      SP:FFFFFFFF8000B3CC|                   c.li       a6,0x5
      SP:FFFFFFFF8000B3CE|                   c.li       a7,0x10       ; a7,16

아래 코드를 보면 extension_id는 base(0x10)이고 func_id는 0x5라는 사실을 알 수 있다. 

opensbi/include/sbi/sbi_ecall_interface.h
/* SBI function IDs for BASE extension*/
#define SBI_EXT_BASE_GET_SPEC_VERSION           0x0
#define SBI_EXT_BASE_GET_IMP_ID                 0x1
#define SBI_EXT_BASE_GET_IMP_VERSION            0x2
#define SBI_EXT_BASE_PROBE_EXT                  0x3
#define SBI_EXT_BASE_GET_MVENDORID              0x4
#define SBI_EXT_BASE_GET_MARCHID                0x5

sbi_ecall_handler() 함수의 아랫 부분 코드를 더 분석하자.

lib/sbi/sbi_ecall.c
int sbi_ecall_handler(struct sbi_trap_context *tcntx)
{
        int ret = 0;
        struct sbi_trap_regs *regs = &tcntx->regs;
        struct sbi_ecall_extension *ext;
        unsigned long extension_id = regs->a7;
        unsigned long func_id = regs->a6;
        struct sbi_ecall_return out = {0};
        bool is_0_1_spec = 0;

        ext = sbi_ecall_find_extension(extension_id);
        if (ext && ext->handle) {
*/ Austin: 'ext->handle' 구문에서 sbi_ecall_base_handler 함수가 호출된다. */
                ret = ext->handle(extension_id, func_id, regs, &out);
                if (extension_id >= SBI_EXT_0_1_SET_TIMER &&
                    extension_id <= SBI_EXT_0_1_SHUTDOWN)
                        is_0_1_spec = 1;
        } else {
                ret = SBI_ENOTSUPP;
        }

그 이유는 extension_id가 0x10이면 sbi_ecall_base_handler 함수가 호출되도록 등록했기 때문이다.
sbi_ecall_base_handler 함수가 호출되는 세세한 구현 방식은 나중에 설명하자.

opensbi/lib/sbi/sbi_ecall_base.c
static int sbi_ecall_base_register_extensions(void)
{
        return sbi_ecall_register_extension(&ecall_base);
}

struct sbi_ecall_extension ecall_base = {
        .name                   = "base",
        .extid_start            = SBI_EXT_BASE,
        .extid_end              = SBI_EXT_BASE,
        .register_extensions    = sbi_ecall_base_register_extensions,
        .handle                 = sbi_ecall_base_handler,
};

sbi_ecall_base_handler 함수이다.

opensbi/lib/sbi/sbi_ecall_base.c
static int sbi_ecall_base_handler(unsigned long extid, unsigned long funcid,
                                  struct sbi_trap_regs *regs,
                                  struct sbi_ecall_return *out)
{
        int ret = 0;

        switch (funcid) {
[...]
       case SBI_EXT_BASE_GET_MARCHID:
                out->value = csr_read(CSR_MARCHID);
                break;

'csr_read(CSR_MARCHID);' 매크로 함수의 리턴 값을 out-value에 저장한다.
'csr_read(CSR_MARCHID);'  구문의 정체는 무엇일까? 바로 marchid CSRs 레지스터이다.

   126f8:       f12027f3                csrr    a5,marchid
   126fc:       4501                    li      a0,0
   126fe:       e69c                    sd      a5,8(a3)

a3이 out의 주소를 저장하니, 'sd      a5,8(a3)' 명령어를 실행하면 marchid 레지스터의 값이 저장된다.

marchid 레지스터의 값이 어떻게 저장되는지 확인하자. sbi_ecall_handler 함수의 구현부이다.

int sbi_ecall_handler(struct sbi_trap_context *tcntx)
{
        int ret = 0;
        struct sbi_trap_regs *regs = &tcntx->regs;
[...]
       ext = sbi_ecall_find_extension(extension_id);
        if (ext && ext->handle) {
                ret = ext->handle(extension_id, func_id, regs, &out);
[...]
               if (!is_0_1_spec)
                        regs->a1 = out.value;

marchid CSR은 a1 레지스터를 통해서 저장된다. 'regs->a1' 가장 마지막 라인을 보자.


머신 모드에서 슈퍼바이저 모드(리눅스 커널)로 리턴하는 동작

이제 머신 모드에서 슈퍼바이저 모드로 리턴하는 동작이다. 즉 opensbi -> 리눅스 커널이다.

00000000000003c8 <_trap_handler>:
     3c8:       34021273                csrrw   tp,mscratch,tp
     3cc:       06523023                sd      t0,96(tp) # 60 <_try_lottery+0x36>
     3d0:       300022f3                csrr    t0,mstatus
[...]
    46c:       00010533                add     a0,sp,zero
     470:       16e0c0ef                jal     ra,c5de <sbi_trap_handler>

     ; sbi_trap_handler 함수 실행이 마무리되면 아래 어셈블리 명령어가 실행된다.
     ; 스택에 푸시한 (리눅스 커널에서 ecall을 실행하기 직전의) 레지스터를 로딩한다.
     ; 슈퍼바이저 모드(리눅스 커널)로 돌아가기 위해서이다.  
     474:       00853083                ld      ra,8(a0)
     478:       01053103                ld      sp,16(a0)
     47c:       01853183                ld      gp,24(a0)
     480:       02053203                ld      tp,32(a0)
     484:       03053303                ld      t1,48(a0)
     488:       03853383                ld      t2,56(a0)
     48c:       6120                    ld      s0,64(a0)
     48e:       6524                    ld      s1,72(a0)
     490:       6d2c                    ld      a1,88(a0)
     492:       7130                    ld      a2,96(a0)
[...]
     4de:       30029073                csrw    mstatus,t0 ; Austin: 되돌아갈 privilege 모드를 mstatus 레지스터 설정
     4e2:       10053283                ld      t0,256(a0)
     4e6:       34129073                csrw    mepc,t0  ; Austin: 되돌아갈 리눅스 커널의 주소 - ecall을 실행한 다음 주소
     4ea:       02853283                ld      t0,40(a0)
     4ea:       02853283                ld      t0,40(a0)
     4ee:       6928                    ld      a0,80(a0)
     4f0:       30200073                mret     ; Austin: 리눅스 커널로 리턴(슈퍼바이저 모드) 


      SP:FFFFFFFF8000B3BA|sbi_get_marchid:   c.addi     sp,-0x10      ; sp,-16
      SP:FFFFFFFF8000B3BC|                   c.sdsp     s0,0x8(sp)    ; s0,8(sp)
      SP:FFFFFFFF8000B3BE|                   c.addi4spn s0,0x10       ; s0,16
      SP:FFFFFFFF8000B3C0|                   c.li       a0,0x0
      SP:FFFFFFFF8000B3C2|                   c.li       a1,0x0
      SP:FFFFFFFF8000B3C4|                   c.li       a2,0x0
      SP:FFFFFFFF8000B3C6|                   c.li       a3,0x0
      SP:FFFFFFFF8000B3C8|                   c.li       a4,0x0
      SP:FFFFFFFF8000B3CA|                   c.li       a5,0x0
      SP:FFFFFFFF8000B3CC|                   c.li       a6,0x5
      SP:FFFFFFFF8000B3CE|                   c.li       a7,0x10       ; a7,16
      SP:FFFFFFFF8000B3D0|                   ecall
      SP:FFFFFFFF8000B3D4|                   c.bnez     a0,0xFFFFFFFF8000B3DE
      SP:FFFFFFFF8000B3D6|                   c.mv       a0,a1 ; Austin: 리턴값을 a0 레지스터에 복사한다. 그 이유는: RISC-V에서 리턴값은
                                                                               ; a0에 저장하기 때문.
      SP:FFFFFFFF8000B3D8|                   c.ldsp     s0,0x8(sp)    ; s0,8(sp)
      SP:FFFFFFFF8000B3DA|                   c.addi     sp,0x10       ; sp,16
      SP:FFFFFFFF8000B3DC|                   c.jr       ra ; Austin: caller 함수로 리턴한다.


아래는 위 어셈블리 명령어에 대응되는 커널 코드이다. 

arch/riscv/kernel/sbi_ecall.c
long __sbi_base_ecall(int fid)
{
struct sbiret ret;

ret = sbi_ecall(SBI_EXT_BASE, fid, 0, 0, 0, 0, 0, 0);
if (!ret.error)
return ret.value;
else
return sbi_err_map_linux_errno(ret.error);
}
EXPORT_SYMBOL(__sbi_base_ecall);

정리

콜 스택을 정리하자:

[리눅스 커널] - supervisor mode
sbi_get_marchid
 - __sbi_base_ecall
  -  ecall 
--------------------------
[opensbi]
  - _trap_handler
    -  sbi_trap_handler 
      - sbi_ecall_handler 
       - sbi_ecall_base_handler
  - a1에 리턴 값 저장    
 - mret     

PS: * 글이 도움이 됐으면 '좋아요'를 눌러주시면 좋겠습니다.

+ Recent posts