특권 명령어 집합

이제 FemtoRV32를 단계적으로 확장하여 Linux를 구동할 수 있게 만들고자 합니다. 계획은 다음과 같습니다.

특권 명령어 집합을 어떻게 구현하는지 살펴봅시다. 안타깝게도 RISC-V 매뉴얼 제2권(특권 명령어 집합)은 제 생각에 제1권만큼 잘 쓰여 있지 않습니다. 제1권을 무릎에 올려 두고서는 며칠 만에 RV32I용 Verilog를 작성할 수 있었습니다. 제2권은 사정이 다릅니다. 제2권은 모든 CSR 레지스터를 나열할 뿐, 무엇이 어떤 역할을 하는지, 무엇이 중요하고 무엇이 선택 사항인지에 대한 체계적인 관점을 제공하지 않기 때문입니다.

활용할 수 있는 또 다른 정보원이 있습니다. @ultraembedded가 RV32(및 RV64)를 포함한 여러 프로세서용 소프트웨어 에뮬레이터를 개발했으며, 여기에서 볼 수 있습니다. 흥미롭게도 이 에뮬레이터는 사이클 단위로 구동되므로, 특권 명령어 집합을 좀 더 계통적으로 바라보기 위한 좋은 출발점이 됩니다. 그래서 RISC-V 제2권을 무릎에 올려 두고, rv32.h와 rv32.cpp를 두 개의 탭에 열어 둔 채로 계속 진행하겠습니다.

가장 먼저 할 일은 rv32.h에서 어떤 CSR이 사용되는지 적어 두고, 이를 RISC-V 제2권에서 찾아보는 것입니다. 목록은 다음과 같습니다.

CSR 레지스터

이름 설명
mepc 머신 예외 프로그램 카운터
mcause 머신 트랩 원인
msr 머신 상태 레지스터
mpriv 현재 특권 수준(비표준)
mtvec 머신 트랩 핸들러 기본 주소(mevec이라고도 함)
mtval 머신 오류 주소 또는 명령어
mie 머신 인터럽트 활성화
mip 머신 인터럽트 대기
mtime 머신 타이머 레지스터
mtimecmp 머신 타이머 레지스터
mtime_ie (bool) 타이머 인터럽트 활성화?
mscratch 머신 트랩 핸들러용 스크래치 레지스터
mideleg 머신 인터럽트 위임 레지스터
medeleg 머신 예외 위임 레지스터
sepc 슈퍼바이저 예외 프로그램 카운터
stvec 슈퍼바이저 트랩 핸들러 기준 주소(sevec이라고도 함)
scause 슈퍼바이저 트랩 원인
stval 슈퍼바이저 모드의 잘못된 주소 또는 명령어
satp 슈퍼바이저 주소 변환 및 보호
sscratch 슈퍼바이저 트랩 핸들러용 스크래치 레지스터

이렇게 해서 머신 모드 CSR과 슈퍼바이저 모드 CSR을 확인했습니다. 프로세서가 Linux를 실행할 때(@ultraembedded의 에뮬레이터는 이것이 가능합니다) 이것들이 어떻게 사용되는지 궁금합니다. 여기에는 사용자 모드 CSR이 보이지 않는데, 아마 사용되지 않기 때문일 것입니다. Volume II를 보면 사용자 모드 CSR은 트랩 상태와 트랩 처리로 구성되어 있는데, 아마 Linux에는 사용자 모드 트랩이 없는 것 같습니다. FPU용 CSR(나중에 살펴보겠습니다)과 카운터/타이머용 CSR(일부는 이미 구현했습니다)도 있습니다. 지금은 1단계에 필요한 머신 모드 CSR만 다루겠습니다.

이제 rv32.cpp를 살펴봅시다. 파일이 그리 길지 않고(2308줄) 주석도 어느 정도 있으며, 코드가 매우 잘 작성된 것 같습니다. 좋은 자료를 찾은 것 같습니다. 소스 파일을 훑어보면서 목차를 작성해 봅시다:

이제 volume II를 다시 살펴봅니다. 우리가 알고 싶은 내용 대부분은 머신 상태 레지스터인 msr의 설명에 있습니다. 1단계에서는 머신 모드만 있는 단순한 구성을 사용합니다. 여기서 더 단순하게 만들어, 인터럽트 없이 예외만 다룹니다. 그러면 다음 CSR만 있으면 됩니다:

이름 설명
mepc 머신 예외 프로그램 카운터
mcause 머신 트랩 원인
msr 머신 상태 레지스터
mtvec 머신 트랩 핸들러 베이스 주소(mevec이라고도 부름)
mtval 머신 잘못된 주소 또는 명령어

(그 밖에 예외 핸들러가 사용할 수 있는 스크래치 레지스터인 mscratch도 있습니다).

이제 755번째 줄에 있는, 예외를 처리하는 코드를 살펴봅시다. 머신 모드만 있으므로 곧바로 800번째 줄로 점프합니다:

        uint32_t s = m_csr_msr;

        // Interrupt save and disable
        s &= ~SR_MPIE;
        s |= (s & SR_MIE) ? SR_MPIE : 0;
        s &= ~SR_MIE;

        // Record previous priviledge level
        s &= ~SR_MPP;
        s |= (m_csr_mpriv << SR_MPP_SHIFT);

        // Raise priviledge to machine level
        m_csr_mpriv  = PRIV_MACHINE;

        m_csr_msr    = s;
        m_csr_mepc   = pc;
        m_csr_mcause = cause;
        m_csr_mtval  = badaddr;

        log_exception(pc, m_csr_mevec, cause);

        // Set new PC
        m_pc = m_csr_mevec;

요약하면, 예외를 호출한다는 것은 머신 상태 레지스터로 무언가를 하는 것(나중에 살펴보겠습니다)이고, 그다음 mepc(현재 명령어로 설정할까요, 다음 명령어로 설정할까요? 나중에 살펴보겠습니다), mcause, mtval을 설정한 뒤 mevec으로 점프하는 것을 의미합니다(특권 수준도 변경하지만, 1단계에서는 그렇게 하지 않습니다).

그리고 1363번째 줄에서 MRET을 처리하는 방식을 살펴봅니다:

        DPRINTF(LOG_INST,("%08x: mret\n", pc));
        INST_STAT(ENUM_INST_MRET);

        assert(m_csr_mpriv == PRIV_MACHINE);

        uint32_t s        = m_csr_msr;
        uint32_t prev_prv = SR_GET_MPP(m_csr_msr);

        // Interrupt enable pop
        s &= ~SR_MIE;
        s |= (s & SR_MPIE) ? SR_MIE : 0;
        s |= SR_MPIE;

        // Set next MPP to user mode
        s &= ~SR_MPP;
        s |=  SR_MPP_U;

        // Set privilege level to previous MPP
        m_csr_mpriv   = prev_prv;
        m_csr_msr     = s;

        // Return to EPC
        pc = m_csr_mepc;
    }

좋습니다, 대략 예상했던 대로입니다. 즉 머신 상태 레지스터로 했던 작업을 되돌리고, mepc에 이전에 저장해 둔 주소로 점프합니다(특권 수준도 변경하지만 1단계에서는 그럴 필요가 없습니다).

아직 답해야 할 질문이 두 가지 남아 있습니다:

두 가지입니다:

  1. 머신 상태 레지스터에 저장되는 "인터럽트 활성화 스택"(깊이 3, 실제로는 2)입니다. MIE : Machine Interrupt Enable(머신 인터럽트 활성화) MPIE: Machine Previous Interrupt Enable(머신 이전 인터럽트 활성화)
  2. MPP(Machine Privilege P?, 머신 특권 P?)일까요?

참고 자료