특권 명령어 집합
이제 FemtoRV32를 단계적으로 확장하여 Linux를 구동할 수 있게 만들고자 합니다. 계획은 다음과 같습니다.
첫 번째 단계는 예외, 인터럽트,
ECALL명령어를 구현하고, 예외 핸들러에서 시스템 콜을 처리하는 것입니다. 이 단계에서는 여전히 단일 특권 수준(머신 모드)만 사용합니다. 첫 번째 단계를 마치면 gcc와 그 표준 라이브러리로 컴파일한 ELF 실행 파일을 실행할 수 있게 됩니다.두 번째 단계는 두 가지 특권 수준을 구현하고, 머신 모드에서 사용하는 메모리를 보호하는 것입니다.
세 번째 단계는 MMU와 가상 주소 지정을 구현하는 것입니다.
특권 명령어 집합을 어떻게 구현하는지 살펴봅시다. 안타깝게도 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줄) 주석도 어느 정도 있으며, 코드가 매우 잘 작성된 것 같습니다. 좋은 자료를 찾은 것 같습니다. 소스 파일을 훑어보면서 목차를 작성해 봅시다:
- 1~173행: CSR용 저수준 get/set,
reset(), 명령어 인출 - 174~433행: MMU(나중에 살펴볼 것이며, 1단계에서는 필요하지 않습니다. 이 부분이 있다는 것만 기억해 둡니다)
- 437~555행: 로드와 스토어(MMU와의 인터페이스 포함)
- 558~751행: 예외 발생을 포함한 CSR용 고수준 get/set
- 755~827행: 예외와 인터럽트를 처리하는 함수, 반드시 이해해야 할 부분
- 831~2188행:
execute단계를 구현하는 거대한 함수- 1344~1411행:
ECALL,EBREAK,MRET,SRET, 반드시 이해해야 할 부분 - 2155~2185행: 예외와 인터럽트 받아들이기, 반드시 이해해야 할 부분
- 1344~1411행:
- 2192~2229행: 한 클록 사이클을 시뮬레이션하는 함수
- 2233~2281행: 인터럽트 설정/해제, 반드시 이해해야 할 부분
- 2282~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단계에서는 그럴 필요가 없습니다).
아직 답해야 할 질문이 두 가지 남아 있습니다:
mepc는 예외를 일으킨 명령어로 설정됩니까, 아니면 다음 명령어로 설정됩니까?MRET이 하는 일을 보면 다음 명령어인 것 같지만, 확실히 해 둘 필요가 있습니다. 예외 핸들러 안에 압축 모드 명령어를 심어 두었다고 생각해 보십시오. 그러면 예외를 일으킨 명령어가mepc-4였는지mepc-2였는지 판별해야 하는데, 골치 아픈 일입니다! (하지만 실제로 그런 것 같습니다)- 머신 상태 레지스터로 하고 또 되돌리는 작업이란 무엇입니까?
SR_MIE와SR_MPIE를 사용한 비트 조작이 몇 가지 있습니다(아마 machine interrupt enable(머신 인터럽트 활성화)과 machine pending interrupt enable(머신 보류 인터럽트 활성화) 같은 것을 뜻하는 것 같습니다). 주로ECALL을 구현하려는 것이므로 지금은 이것들을 무시해도 됩니다. 인터럽트는 나중에 살펴보겠습니다. 그다음MPP와 관련된 것이 있는데, 이것은 무엇입니까?rv32_isa.h에 정의되어 있지만 주석이 없으므로(저자는 독자가 이미 익숙하다고 가정하는데, 저는 그렇지 않습니다!), 제2권을 파고들어 봅시다. 20페이지의mstatus부분에 나옵니다. 이것은 특권 수준을 추적하는 데 쓰이는 스택입니다(다시 말하지만, 1단계에서는 이를 무시해도 됩니다).
두 가지입니다:
- 머신 상태 레지스터에 저장되는 "인터럽트 활성화 스택"(깊이 3, 실제로는 2)입니다. MIE : Machine Interrupt Enable(머신 인터럽트 활성화) MPIE: Machine Previous Interrupt Enable(머신 이전 인터럽트 활성화)
- MPP(Machine Privilege P?, 머신 특권 P?)일까요?
참고 자료
- RISC-V 매뉴얼, volume II (특권 아키텍처)
- @ultraembedded의 exactstep