Chapter 17

특권 모드와 CSR

응용 프로그램은 운영체제를, 운영체제는 펌웨어를 함부로 건드려서는 안 된다. 누구나 실수하고, 누군가는 악의를 품기 때문이다. RISC-V는 하트가 지금 얼마만큼의 권한으로 명령을 실행하는지를 특권 모드로 나누고, 하트의 동작을 조절하는 스위치와 계기판을 CSR(제어·상태 레지스터)이라는 4096칸짜리 별도 주소 공간에 모아 두었다. 이 장에서는 M·S·U 세 모드가 왜 필요한지, CSR 주소 자체에 접근 권한이 어떻게 새겨져 있는지, 그리고 misa·mstatus 같은 핵심 레지스터를 필드 단위로 해부한다.

왜 특권 수준이 필요한가

한 대의 컴퓨터 위에서 웹 브라우저, 운영체제 커널, 부팅 펌웨어가 함께 돈다. 브라우저의 버그가 커널 메모리를 덮어쓰거나, 악성 앱이 디스크 컨트롤러를 직접 조작할 수 있다면 시스템 전체가 위험해진다. 그래서 하드웨어는 “지금 실행 중인 코드가 무엇을 할 수 있는가”를 구분해야 한다. 이 구분이 특권 수준(privilege level)이다. 규격은 특권 수준을 소프트웨어 스택의 구성 요소 사이를 보호하는 수단으로 정의하고, 현재 모드에서 허락되지 않은 일을 하려 하면 예외가 발생한다고 정한다.

RISC-V 하트는 언제나 어느 한 특권 수준에서 실행되며, 그 수준은 2비트로 표현된다. 지금 정의된 수준은 셋이다.

레벨인코딩이름약자주로 실행하는 것
000User/ApplicationU응용 프로그램
101SupervisorS운영체제 커널
210Reserved—예약 (모드로는 쓰지 않음)
311MachineM펌웨어, SBI 구현, 보안 모니터
특권 높음 ▲ 특권 낮음 ▼ D 모드 (디버그) — 선택 사항 외부 디버거가 하트를 멈췄을 때만 들어가며, M보다도 많은 접근 권한을 가진다 11 M 모드 · Machine 펌웨어 · SBI 구현(OpenSBI 등) · 보안 모니터 모든 하트에 반드시 있는 유일한 모드 01 S 모드 · Supervisor 운영체제 커널 · 가상 메모리 관리 H 확장이 있으면 HS 모드로 하이퍼바이저도 실행 (21장) 00 U 모드 · User 응용 프로그램 — 가장 적은 권한 CSR은 카운터·부동소수점 상태 등 일부만 접근 트랩 ecall · 예외 · 인터럽트 트랩 S의 ecall, 위임 안 된 것 sret mret
그림 17-1. 특권 모드 사다리. 금색 점은 트랩(ecall, 예외, 인터럽트)이 더 높은 특권으로 올라가는 길, 남색 점은 sret·mret으로 내려오는 길이다. 모드를 바꾸는 방법은 이 두 가지뿐이다. 왼쪽 칩의 숫자는 각 모드의 2비트 인코딩이다.

규격은 M 모드를 가장 높은 권한을 가진, 유일한 필수 모드로 정한다. M 모드 코드는 기계의 구현 세부에 낮은 수준으로 접근할 수 있으므로 본질적으로 신뢰받는 코드다. 그 위에 얼마나 많은 모드를 둘지는 구현이 고른다. 모드가 적을수록 하드웨어는 싸지지만 격리는 약해진다.

모드 수지원 모드규격이 드는 용도예
1M단순한 임베디드 시스템센서 노드의 펌웨어 하나만 도는 MCU
2M, U보안이 필요한 임베디드 시스템RTOS 커널(M) + 격리된 태스크(U)
3M, S, U유닉스 계열 운영체제를 돌리는 시스템OpenSBI(M) + 리눅스(S) + 앱(U)
M 모드만 있어도 보호는 가능하다

모드가 M 하나뿐이면 잘못된 응용 코드로부터 아무것도 지킬 수 없다. 그래도 PMP(물리 메모리 보호)의 잠금(L) 비트를 쓰면 M 모드 자신에게도 접근 제한을 걸 수 있어, 부트 ROM 같은 영역을 제한적으로나마 보호할 수 있다(20장).

“수준”과 “모드”는 다르다

규격은 코드가 어느 수준을 위해 작성되었는가와 어느 모드에서 실제로 실행되는가를 구분한다. 보통은 같지만, 고전적인 가상 머신 모니터는 S 수준용으로 작성된 게스트 운영체제를 U 모드에서 돌린다. 이때 게스트가 S 수준 명령이나 CSR을 쓰면 모두 트랩이 걸려 더 높은 모드의 감시자가 대신 흉내 낸다. 같은 바이너리가 두 환경에서 모두 돌 수 있는 것은 운영체제가 하드웨어가 아니라 SBI라는 약속에 맞춰 작성되기 때문이다.

네 번째 모드, 디버그 모드

구현은 칩 밖 디버깅과 제조 시험을 위해 D 모드를 둘 수 있다. M보다도 접근 권한이 많은 추가 특권 모드로 볼 수 있으며, CSR 주소 몇 개(0x7B0–0x7BF)를 D 모드 전용으로 예약한다. 자세한 동작은 별도의 디버그 규격이 정한다(22장).

하트는 한 번에 한 모드: 실행 환경과 모드 전환

특권 규격은 소프트웨어 스택을 “위의 소프트웨어가 아래와 대화하는 인터페이스”와 “그 인터페이스를 구현하는 실행 환경”의 짝으로 설명한다. 응용 프로그램은 ABI(Application Binary Interface)로 응용 실행 환경(AEE)과 대화하고, 운영체제는 SBI(Supervisor Binary Interface)로 감독자 실행 환경(SEE)과 대화한다. 하이퍼바이저가 있으면 그 아래에 HBI와 HEE가 하나 더 생긴다.

① 응용 하나 응용 프로그램 ABI AEE ② 운영체제 응용 응용 ABI ABI 운영체제 (AEE 역할) SBI SEE (펌웨어 등) ③ 하이퍼바이저 응용 · ABI 응용 · ABI OS OS SBI SBI 하이퍼바이저 (SEE 역할) HBI HEE 짙은 상자 = 추상 인터페이스(약속), 옅은 상자 = 그 약속을 구현하는 구체적인 소프트웨어 · 하드웨어
그림 17-2. 규격 서론의 세 가지 소프트웨어 스택. 규격처럼 인터페이스는 짙은 상자로, 실제 구성 요소는 옅은 상자로 그렸다. 운영체제가 SBI에만 맞춰 작성되면 아래의 SEE가 간단한 부트로더든 하이퍼바이저가 만든 가상 머신이든 같은 바이너리가 그대로 돈다.

이 스택을 하트 하나의 시간 축으로 바꿔 보면 특권 아키텍처의 기본 규칙이 드러난다. 하트는 어느 순간에도 정확히 하나의 특권 모드에서 실행된다. 그리고 모드는 다음 두 가지 방법으로만 바뀐다.

  1. 트랩 — ecall, 예외, 인터럽트가 일어나면 하트는 처리기로 뛰면서 같거나 더 높은 특권 모드로 들어간다. 특권이 올라가는 트랩을 수직 트랩(vertical trap), 같은 수준에 머무는 트랩을 수평 트랩(horizontal trap)이라 한다. 규격은 “트랩은 결코 더 특권 높은 모드에서 더 낮은 모드로 옮겨 가지 않는다”고 못 박는다.
  2. xRET — 처리기가 끝나면 mret이나 sret이 트랩 때 저장해 둔 이전 모드(mstatus.MPP·SPP)로 돌아간다. 특권을 낮추는 유일한 정규 경로다.

트랩 때 무엇이 어떻게 저장되고 복원되는지는 18장에서 한 단계씩 따라간다. 여기서는 모드가 시간에 따라 오르내리는 모습만 눈에 익혀 두자.

ANIMATION

하트 한 개의 특권 모드 타임라인

재생을 누르면 하트가 응용 프로그램(U)에서 출발한다.
재생
현재 모드U
마지막 전환—
Sstc 확장이 없는 전통적인 시스템에서 타이머 인터럽트가 어떻게 M 모드를 거쳐 S 모드로 전달되는지 보자. 금색 화살표(트랩)는 언제나 위로, 남색 화살표(xRET)는 언제나 아래로 간다.

4096칸의 CSR 주소 공간

CSR(Control and Status Register)은 정수 레지스터 x0–x31과 별개인 저장 공간이다. CSR 명령의 12비트 csr 필드(명령어 비트 31–20)로 번지를 고르므로 최대 4096개까지 둘 수 있다. 특권 아키텍처 전체가 이 공간 위에 지어진다. 트랩 처리기 주소(mtvec), 인터럽트 허용 비트(mie), 페이지 테이블 위치(satp)가 모두 CSR이다.

RISC-V가 영리한 점은 주소 자체에 접근 규칙을 새겨 넣었다는 것이다. 주소의 위쪽 2비트 csr[11:10]는 읽기·쓰기 가능(00, 01, 10)인지 읽기 전용(11)인지를, 그다음 2비트 csr[9:8]는 그 CSR에 접근할 수 있는 가장 낮은 특권을 나타낸다. 하드웨어는 표를 뒤질 필요 없이 비트 4개만 보고 1차 검사를 끝낼 수 있다.

1110 98 70 [11:10] [9:8] [7:0] 같은 구역 안의 번호 읽기/쓰기 00·01·10 = 읽기·쓰기 11 = 읽기 전용 최소 특권 00 U · 01 S 10 HS · 11 M [7:4]의 일부 패턴은 표준이 아닌 “사용자 정의(custom)”용 예: 0x7C0–0x7FF, 0xBC0–0xBFF 는 M 수준 custom 읽기·쓰기 예 0x300 mstatus 00 11 0000 0000 읽기·쓰기, M 모드 이상 0x100 sstatus 00 01 0000 0000 읽기·쓰기, S 모드 이상 0xC00 cycle 11 00 0000 0000 읽기 전용, U부터 (카운터 문 필요) 0xF14 mhartid 11 11 0001 0100 읽기 전용, M 모드 전용
그림 17-3. CSR 주소 12비트의 구조. 위쪽 4비트만으로 “누가 읽을 수 있고 누가 쓸 수 있는가”가 결정된다. 덕분에 하드웨어의 권한 검사가 단순해지고, 대신 CSR을 주소 공간에 배치하는 방식에는 제약이 생긴다.
규격 원문에서
“The top two bits (csr[11:10]) indicate whether the register is read/write (00,01, or 10) or read-only (11). The next two bits (csr[9:8]) encode the lowest privilege level that can access the CSR, with the pattern 10 representing hypervisor CSRs.”

Privileged Vol. II, §1.1.1 CSR Address Mapping Conventions — 특권 인코딩에서는 예약된 10이 CSR 주소에서는 하이퍼바이저(HS) 수준을 뜻한다는 점에 주의하자.

규격은 이 규칙 위에 표준과 사용자 정의(custom) 영역을 나눠 둔 할당표를 정한다. custom 영역에 정의된 CSR은 앞으로 표준 확장이 다시 정의하지 않겠다고 약속되어 있어, 칩 회사가 자기만의 CSR을 안심하고 둘 수 있다.

최소 특권 [9:8]표준 읽기·쓰기custom 읽기·쓰기표준 읽기 전용custom 읽기 전용
U (00)0x000–0x0FF 0x400–0x4FF0x800–0x8FF0xC00–0xCBF0xCC0–0xCFF
S (01)0x100–0x1FF 0x500–0x5BF 0x900–0x9BF0x5C0–0x5FF 0x9C0–0x9FF0xD00–0xDBF0xDC0–0xDFF
HS (10)0x200–0x2FF 0x600–0x6BF 0xA00–0xABF0x6C0–0x6FF 0xAC0–0xAFF0xE00–0xEBF0xEC0–0xEFF
M (11)0x300–0x3FF 0x700–0x79F 0xB00–0xBBF
디버그용 0x7A0–0x7AF
0x7C0–0x7FF 0xBC0–0xBFF0xF00–0xFBF0xFC0–0xFFF

0x7B0–0x7BF는 D 모드에서만 보이는 디버그 전용 CSR(dcsr, dpc 등)이며, 규격은 M 모드가 이곳에 접근하면 illegal-instruction 예외를 내야 한다고 권한다. 아래 그림은 4096칸 전체를 16칸씩 묶어 색으로 칠한 지도다. 칸을 누르면 아래 해독기로 그 번지가 넘어간다.

그림 17-4. CSR 주소 공간 지도. 한 칸이 CSR 16개다. 세로 줄은 위쪽 4비트(csr[11:8]), 가로 칸은 csr[7:4]다. 같은 줄 안에서는 권한이 같다는 점, 그리고 각 줄의 오른쪽 끝([7:6]=11)이 대개 custom 영역이라는 점이 한눈에 보인다.
카운터만 가진 “그림자” CSR

규격은 낮은 수준에서 읽기 전용인 CSR을 높은 수준에서 읽기·쓰기로 쓰고 싶을 때 주소를 따로 둔다. 예를 들어 cycle(0xC00, U 읽기 전용)은 mcycle(0xB00, M 읽기·쓰기)의 그림자다. 이렇게 하면 허락된 읽기는 트랩 없이 바로 실행되고, 금지된 쓰기만 걸러진다. 현재 이런 그림자 CSR을 가진 것은 카운터뿐이다.

SIMULATOR

CSR 주소 해독기

XLEN
이름mstatus
[11:10] 접근읽기·쓰기
[9:8] 최소 특권M
할당 구역표준
비트를 눌러 0x300(mstatus)을 0xF14(mhartid)로 바꿔 보자. 위쪽 두 비트가 11이 되는 순간 모든 “쓰기” 칸이 빨갛게 변한다. 또 0xC80(cycleh)을 고른 뒤 XLEN을 RV64로 바꾸면 어떻게 되는지 확인하자. 표의 “읽기”는 csrr(=csrrs rd, csr, x0), “쓰기”는 csrw처럼 실제로 CSR에 값을 쓰는 명령을 뜻한다.

CSR 명령과 illegal-instruction 예외

CSR은 Zicsr 확장의 여섯 명령으로만 읽고 쓴다(16장). 여섯 명령은 모두 “읽고-고치고-쓰기”를 원자적으로 하지만, 피연산자에 따라 읽기나 쓰기를 아예 하지 않을 수 있다. 이 차이가 권한 검사에 그대로 영향을 준다. 읽기 전용 CSR을 읽기만 하는 명령은 괜찮지만, 같은 CSR에 쓰기를 “시도”하는 명령은 값이 0이어도 예외가 난다.

명령조건CSR 읽기CSR 쓰기흔한 의사 명령
csrrwrd = x0안 함함csrw csr, rs
rd ≠ x0함함csrrw rd, csr, rs
csrrs · csrrcrs1 = x0함안 함csrr rd, csr
rs1 ≠ x0함함 (레지스터 값이 0이어도)csrs · csrc
csrrwird = x0안 함함csrwi
csrrsi · csrrciuimm = 0함안 함—

규격이 정한 CSR 접근 실패의 경우는 다음과 같다.

여기에 더해 주소 규칙만으로는 표현할 수 없는 “두 번째 문지기”가 있다. mstatus.TVM이 1이면 S 모드의 satp 접근이, mcounteren 비트가 0이면 S 모드의 카운터 읽기가, mstateen 비트가 0이면 새 확장의 상태 접근이 막힌다. RV64에서는 RV32 전용 상위 절반 CSR(cycleh 등)의 주소가 예약되어 있어 접근하면 보통 illegal-instruction 예외가 난다.

CSR 명령 csr = 12비트 ① 존재하나? 구현된 CSR인가 RV64의 …h는 예약 ② 특권 충분? 현재 모드 ≥ csr[9:8] 0x7B0–0x7BF는 D 전용 ③ 쓰기 허용? 쓰기를 시도하는데 csr[11:10]=11 이면 ✗ ④ 추가 문 TVM · counteren stateen · STCE 통과 읽기·쓰기 수행 아니오아니오아니오닫힘 illegal-instruction 예외 (mcause = 2) mtval에 명령어 비트가 담길 수 있다 · 가상화 상황의 일부는 virtual-instruction 예외 통과한 쓰기라도, 읽기·쓰기 CSR 안의 읽기 전용 비트에 대한 쓰기는 예외 없이 무시된다
그림 17-5. CSR 접근이 성공하기까지의 관문. ①–③은 주소만 보고 판정할 수 있고, ④는 다른 CSR의 상태에 따라 달라진다. 어느 관문에서 막히든 결과는 같은 illegal-instruction 예외이며, 그 처리는 18장의 트랩 메커니즘이 맡는다.
csrr t0, mhartid # 0xF14: 최소 특권 M → illegal-instruction csrr t1, cycle # 0xC00: U 읽기 가능. mcounteren.CY·scounteren.CY 가 열려 있어야 성공 csrw cycle, t1 # 0xC00 은 읽기 전용([11:10]=11) → 어느 모드에서든 illegal-instruction csrrs t2, fflags, x0 # rs1=x0 이므로 쓰기 없음. 부동소수점 상태(mstatus.FS)가 Off 가 아니면 성공 csrs sstatus, t3 # 0x100: 최소 특권 S → illegal-instruction
“권한 위반”과 “특권 명령”을 구별하지 않는다

규격은 특권 명령 예외를 illegal-instruction 예외와 따로 두지 않았다. 아키텍처가 단순해지고, 어떤 상위 특권 명령이 구현되어 있는지도 숨길 수 있기 때문이다. 둘을 구별해야 한다면 트랩을 받은 상위 모드의 소프트웨어가 정책을 정한다.

하트의 신분증: misa와 ID 레지스터

부팅 직후의 펌웨어가 가장 먼저 알아야 할 것은 “나는 어떤 하트 위에서 돌고 있는가”다. 규격은 이를 위해 M 모드 CSR 몇 개를 반드시 읽을 수 있게 해 두었다. 그 중심에 있는 것이 misa(Machine ISA register, 0x301)다. misa는 WARL 읽기·쓰기 레지스터로, 맨 위 두 비트에 기본 정수 폭을, 맨 아래 26비트에 알파벳 한 글자짜리 확장의 유무를 담는다.

MXLEN-1 MXLEN-2 MXLEN-3 26 25 0 MXL WARL · 읽기 전용 0 MXLEN-28 비트, 0으로 읽힘 Extensions[25:0] WARL · 글자마다 1비트 정의된 확장 예약 잠정 예약(N, P) X = 비표준 확장 있음
그림 17-6. misa의 배치. 비트 0이 A, 비트 25가 Z다. I·E는 베이스 ISA를, S·U는 해당 모드의 존재를 나타낸다. Zba·Zicsr처럼 여러 글자로 된 확장은 misa에 비트가 없다. 그런 확장은 장치 트리 같은 별도 수단으로 알린다.
MXLXLEN비고
132MXLEN = 2MXL+4. misa가 0이 아니면 MXL은 M 모드의 실효 XLEN(MXLEN)이다. 덜 특권적인 모드의 XLEN은 MXLEN보다 작을 수는 있어도 클 수는 없다.
264
3예약

MXL이 맨 위에 있는 데에는 이유가 있다. misa를 읽은 값의 부호(최상위 비트)로 한 번 분기하고, 왼쪽으로 한 칸 민 뒤 다시 부호로 분기하면 레지스터 폭을 모르는 코드로도 RV32인지 RV64인지 알아낼 수 있다. 규칙 몇 가지를 더 정리하면 다음과 같다.

SIMULATOR

misa 만들기

MXL
미리 정한 구성
misa (16진수)—
ISA 문자열 (한 글자 확장만)—
켜진 글자 수—
“리눅스” 구성을 고른 뒤 F를 꺼 보자. 규격의 의존 규칙에 따라 D가 함께 꺼진다(빨간 점선 테두리). Q나 V가 켜져 있었다면 그것들도 D에 의존하므로 같이 꺼진다. U를 끄면 S와 H도 꺼진다. MXL을 1로 바꾸면 16진수 값이 32비트로 줄고 맨 앞 두 비트만 01로 바뀐다는 점도 확인하자. I를 끄면 읽기 전용인 E가 저절로 켜진다.

누가 만든 어떤 하트인가: 네 개의 ID 레지스터

CSR주소내용
mvendorid0xF1132비트. JEDEC 제조사 ID를 Bank(연속 코드 0x7f의 개수, 비트 31:7)와 Offset(마지막 바이트에서 패리티를 뺀 7비트, 비트 6:0)으로 담는다. 0x7f를 열두 번 이어 쓴 뒤 0x8a로 끝나는 ID는 0x60a가 된다. 0이면 미구현 또는 비상업 구현.
marchid0xF12MXLEN 비트. 마이크로아키텍처의 ID. 오픈 소스 프로젝트는 RISC-V International이 최상위 비트 0인 번호를 배정하고, 상업 ID는 최상위 비트가 1이다. mvendorid와 함께 하트의 종류를 유일하게 식별해야 한다.
mimpid0xF13구현(설계 버전)의 번호. 형식은 제공자가 정한다.
mhartid0xF14지금 코드를 실행하는 하트의 번호. 실행 환경 안에서 유일해야 하고, 연속일 필요는 없지만 하트 하나는 반드시 0번이어야 한다. 리셋 직후 “딱 한 하트만” 초기화를 하게 만들 때 쓴다.
mconfigptr0xF15하트와 플랫폼을 설명하는 설정 자료 구조가 있는 곳의 주소.
낮은 모드는 misa를 읽지 않는다

misa는 M 모드 전용이다. 규격은 낮은 특권 수준이 CPU 레지스터를 직접 읽는 대신 환경 호출(ecall)로 실행 환경에 물어보라고 요구한다. 그래야 가상화 계층이 각 수준이 보는 ISA를 자유롭게 바꿀 수 있고, 하드웨어에 부담을 주지 않으면서 훨씬 풍부한 정보를 줄 수 있다. 리눅스는 부팅 때 펌웨어가 넘겨준 장치 트리의 riscv,isa 문자열을 읽는다.

mstatus: 하트의 조종판

mstatus(Machine Status, 0x300)는 하트의 현재 동작 상태를 기록하고 조절하는 MXLEN 비트 읽기·쓰기 레지스터다. 인터럽트를 켜고 끄는 스위치, 트랩 직전의 모드를 기억하는 칸, 부동소수점·벡터 상태가 더러워졌는지 알려 주는 표시등, 메모리 접근 권한을 비트는 레버가 한 레지스터에 모여 있다. RV32에서는 64비트 분량이 다 들어가지 않으므로, RV64 mstatus의 비트 62:36에 해당하는 필드를 mstatush(0x310)의 비트 30:4에 둔다. 단 SD, SXL, UXL은 mstatush에 없다.

무리필드 (RV64 비트)역할
트랩 스택SIE(1) MIE(3) SPIE(5) MPIE(7) SPP(8) MPP(12:11)모드별 전역 인터럽트 허용, 트랩 직전의 허용 값과 모드를 한 단계 저장
확장 상태VS(10:9) FS(14:13) XS(16:15) SD(63)벡터·부동소수점·기타 확장 상태가 Off/Initial/Clean/Dirty 중 무엇인지
메모리 권한MPRV(17) SUM(18) MXR(19)M 모드가 MPP의 권한으로 메모리를 읽게 하거나, 페이지 권한 해석을 바꿈
가상화 지원TVM(20) TW(21) TSR(22)S 모드의 satp·SFENCE.VMA, WFI, SRET을 M 모드로 가로챔
XLEN · 엔디언UXL(33:32) SXL(35:34) UBE(6) SBE(36) MBE(37)U·S 모드의 XLEN과, 모드별 데이터 접근 바이트 순서
H · CFI · 이중 트랩GVA(38) MPV(39) SPELP(23) MPELP(41) SDT(24) MDT(42)하이퍼바이저 확장, 랜딩 패드(Zicfilp), 이중 트랩(Ssdbltrp·Smdbltrp) 지원
EXPLORER

mstatus 필드 탐색기 (RV64)

보기
무리 강조
“sstatus로 보기”를 누르면 S 모드가 볼 수 있는 필드만 남는다. MIE, MPIE, MPP, MPRV, TVM, TW, TSR처럼 M 모드의 비밀은 모두 사라지고, SIE·SPIE·SPP·SUM·MXR과 FS·VS·XS·SD는 그대로 남는다. 회색 칸(WPRI)도 하나 눌러 보자.

sstatus는 mstatus를 들여다보는 창이다

S 모드 소프트웨어는 sstatus(0x100)를 읽고 쓴다. sstatus는 별도의 레지스터가 아니라 mstatus의 제한된 보기다. 규격의 표현대로 단순한 구현에서는 sstatus의 어떤 필드를 읽거나 쓰는 것이 mstatus의 같은 이름 필드를 읽거나 쓰는 것과 같다. 같은 관계가 sie·sip와 mie·mip 사이에도 있다.

그림 17-7. mstatus의 비트 24–0과 sstatus의 같은 위치. 점선으로 이어진 필드는 같은 저장소를 두 이름으로 보는 것이고, sstatus 쪽의 빈 회색 칸은 S 모드에게 WPRI(0으로 보임)다. 그림 아래에 정리한 것처럼 위쪽 비트 중에서는 UXL과 SD만 sstatus에 보인다.

트랩 스택: MIE · MPIE · MPP

MIE와 SIE는 각 모드의 전역 인터럽트 허용 비트다. 하트가 모드 x에서 실행 중일 때 xIE=1이면 그 모드의 인터럽트가 전역으로 허용된다. 낮은 모드의 인터럽트는 그 모드의 wIE와 상관없이 항상 막혀 있고, 높은 모드의 인터럽트는 yIE와 상관없이 항상 허용된다. 그래서 U 모드에서 돌던 하트는 MIE가 0이어도 M 모드 타이머 인터럽트를 받는다.

트랩이 중첩될 수 있도록 각 모드는 두 칸짜리 스택을 가진다. 모드 y에서 모드 x로 트랩이 들어가면 xPIE ← xIE, xIE ← 0, xPP ← y가 된다. xRET은 반대로 xIE ← xPIE, 모드 ← xPP, xPIE ← 1, xPP ← 지원하는 가장 낮은 모드를 수행하고, 새 모드가 M이 아니면 MPRV도 0으로 만든다. MPP는 U·S·M을 모두 담아야 하므로 2비트, SPP는 U·S만 담으면 되므로 1비트다.

모드 MIE MPIE MPP ① U 모드 실행 중 U 1 ? ? ② M 모드로 트랩 M 0 1 00 (U) ③ mret 이후 U 1 1 00 (U) 트랩: MPIE←MIE, MIE←0, MPP←U mret: MIE←MPIE, 모드←MPP, MPIE←1, MPP←U
그림 17-8. 두 칸짜리 트랩 스택. 트랩이 들어오면 “지금” 값이 “직전(P)” 칸으로 밀려 내려가고 인터럽트가 꺼진다. mret은 직전 칸을 다시 끌어올린다. mret 뒤의 MPP가 U인 것은 규격이 xRET 때 xPP를 지원하는 가장 낮은 모드로 바꾸게 했기 때문이다. 스택 관리 버그를 찾기 쉽게 하려는 장치다.

확장 상태: FS · VS · XS와 SD

부동소수점 레지스터 32개와 벡터 레지스터 32개를 문맥 전환 때마다 모두 저장하면 비싸다. 그래서 mstatus는 각 확장 상태가 Off(0), Initial(1), Clean(2), Dirty(3) 중 무엇인지 기록한다. 운영체제는 Dirty인 상태만 메모리에 저장하고 Clean으로 바꿔 두면 된다. SD는 FS·VS·XS 중 하나라도 Dirty이면 1이 되는 읽기 전용 요약 비트로, 최상위 비트에 있어서 부호 검사 한 번으로 “저장할 것이 있는가”를 판정할 수 있다.

0 · Off접근하면 illegal 1 · Initial초기 상수 값 2 · Clean마지막 저장본과 같음 3 · Dirty저장 후 바뀌었을 수 있음 OS가 켬 (새 프로세스는 0으로 채운 Initial) 상태를 바꾸는 명령 실행 → Dirty 문맥 저장 후 OS가 Clean으로
그림 17-9. FS·VS 필드의 네 상태. 실제 하드웨어가 하는 일은 “상태를 바꾸는 명령이 실행되면 Dirty로 표시”뿐이고, 나머지 전이는 운영체제가 mstatus·sstatus에 직접 써서 만든다. Off 상태는 확장을 아예 잠가 두는 용도로도 쓰인다. 리눅스는 프로세스가 처음 벡터 명령을 쓸 때 일어나는 illegal-instruction 트랩을 보고 그제야 VS를 켠다.

나머지 필드들

필드뜻
MPRV1이면 M 모드의 load·store가 MPP 모드인 것처럼 주소 변환·보호를 받는다. 명령어 인출은 영향받지 않는다. 오정렬 접근을 흉내 내는 M 모드 루틴이 소프트웨어 주소 변환 없이 사용자 메모리를 읽을 수 있게 한다. U 모드가 없으면 읽기 전용 0.
SUM0이면 S 모드가 U 페이지(PTE의 U=1)에 접근할 때 폴트가 난다. 커널이 실수로 사용자 포인터를 따라가는 일을 막는다(19장).
MXR1이면 실행 전용(X=1) 페이지도 load로 읽을 수 있다. 명령어를 읽어 흉내 내야 하는 처리기용이다.
TVM1이면 S 모드의 satp 접근과 SFENCE.VMA·SINVAL.VMA가 illegal-instruction이 된다. 섀도 페이지 테이블로 게스트를 돌릴 때 쓴다.
TW1이면 M보다 낮은 모드의 WFI가 구현이 정한 제한 시간 안에 끝나지 않을 때 illegal-instruction이 된다. 놀고 있는 게스트 대신 다른 게스트로 전환하는 데 쓴다.
TSR1이면 S 모드의 SRET이 illegal-instruction이 된다. H 확장이 없는 하드웨어에서 하이퍼바이저를 흉내 낼 때 필요하다.
UXL · SXLRV64에서 U·S 모드의 XLEN을 고르는 WARL 필드. 인코딩은 misa.MXL과 같다. 읽기 전용으로 만들어 항상 64로 둘 수도 있다.
UBE · SBE · MBE명령어 인출을 뺀 메모리 접근의 바이트 순서. 0이 리틀 엔디언, 1이 빅 엔디언이다. 명령어 인출은 언제나 리틀 엔디언이다.

WPRI · WLRL · WARL: 필드에 쓰는 약속

CSR의 모든 비트가 모든 값을 받아 주지는 않는다. 어떤 필드는 미래를 위해 비워 두었고, 어떤 필드는 일부 값만 뜻이 있다. 규격은 이런 필드를 세 가지 이름으로 분류하고, 소프트웨어와 하드웨어가 각각 무엇을 지켜야 하는지 정했다.

WPRI Reserved Writes Preserve, Reads Ignore 미래를 위한 예약 필드 SW: 읽은 값은 무시하고, 다른 필드를 쓸 때 원래 값 보존 HW: 구현하지 않았으면 읽기 전용 0 WLRL Write/Read Only Legal Values 예: mcause의 Exception Code SW: 합법 값만 써야 한다 불법 값을 쓰면… HW: 예외를 내도 되고, 아무 비트 패턴을 돌려줘도 됨 WARL Write Any Values, Reads Legal Values 예: misa, mtvec, MPP, pmpaddr SW: 아무 값이나 써도 된다 예외는 절대 나지 않는다 HW: 읽으면 언제나 합법 값 → 써 보고 읽어서 탐지
그림 17-10. 세 가지 필드 규칙. WARL은 “써 보고 다시 읽기”로 하드웨어가 무엇을 지원하는지 알아낼 수 있다는 점이 핵심이다. WLRL에 불법 값을 쓴 뒤 읽은 값은 임의일 수 있지만, 규격은 그 값이 쓴 값과 이전 값에 따라 결정적으로 정해져야 한다고 권한다.
규칙소프트웨어가 지킬 것하드웨어가 지킬 것대표 예
WPRI읽은 값을 무시하고, 다른 필드를 고칠 때 이 필드의 값을 그대로 다시 쓴다필드를 갖추지 않았다면 읽기 전용 0mstatus 비트 0·2·4·31:25
WLRL합법 값만 쓴다. 불법 값을 쓴 뒤 읽은 값을 믿지 않는다지원 값만 구별할 만큼의 비트를 두되, 읽으면 전체 인코딩을 돌려준다. 불법 값 쓰기에 예외를 내도 된다(필수 아님)mcause·scause의 Exception Code
WARL아무 값이나 쓸 수 있다예외 없이 받아들이고, 읽으면 항상 합법 값. 불법 값을 쓴 뒤에는 쓴 값과 하트 상태에 따라 결정적으로 정해지는 합법 값misa, mtvec, MPP, SXL·UXL, pmpaddr, medeleg

WARL 덕분에 펌웨어는 별도의 기능 목록 없이도 하드웨어를 탐지할 수 있다. 규격 비고에 나오는 예가 MPP다. MPP는 구현된 모드만 담을 수 있는 WARL 필드이므로, S(01)를 써 보고 다시 읽어서 01이 남아 있으면 S 모드가 있는 것이다. U와 M만 있는 하트라면 MPP를 담는 데 하드웨어 저장 비트가 하나면 충분하다(00 또는 11).

li t0, 0x1800 # MPP 필드 마스크 (mstatus 비트 12:11) csrc mstatus, t0 # MPP = 00 li t1, 0x0800 # MPP = 01 (S) 을 써 본다 csrs mstatus, t1 csrr t2, mstatus and t2, t2, t0 bne t2, t1, no_s_mode # 01 이 남아 있지 않으면 S 모드가 없다
다른 CSR이 합법 값을 바꿀 때

한 CSR에 쓴 값이 다른 CSR 필드의 합법 값 집합을 바꾸는 경우가 있다. 규격은 따로 정하지 않은 한, 두 번째 필드가 새 합법 값 중 UNSPECIFIED한 값을 즉시 갖는다고 정한다. 이전 값이 여전히 합법이어도 바뀔 수 있다. 이런 변화는 그 CSR에 대한 “쓰기”가 아니므로 쓰기에 딸린 부수 효과도 일으키지 않는다.

카운터와 접근 문: mcounteren · scounteren · mcountinhibit

성능 측정과 시간 측정을 위해 M 모드에는 64비트 카운터가 있다. mcycle(0xB00)은 하트가 돌고 있는 코어의 클럭 사이클을, minstret(0xB02)은 완료(retire)한 명령 수를 센다. 여기에 이벤트를 고를 수 있는 mhpmcounter3–mhpmcounter31(0xB03–0xB1F)과 그 이벤트 선택기 mhpmevent3–mhpmevent31(0x323–0x33F)이 있다. RV32에서는 위쪽 32비트를 …h CSR로 따로 읽는다.

낮은 모드는 같은 값을 읽기 전용 그림자로 본다. cycle(0xC00), instret(0xC02), hpmcounter3–31(0xC03–0xC1F)이 그것이고, time(0xC01)은 메모리 사상 레지스터 mtime의 그림자다. 그런데 카운터는 부채널 공격의 재료가 될 수도 있고, 가상화 환경에서는 값을 가공해 보여 줘야 할 수도 있다. 그래서 규격은 카운터마다 문을 달았다.

카운터 본체 mcycle · minstret mhpmcounter3–31 (+ 메모리의 mtime) mcountinhibit ← 브레이크: 증가 멈춤 mcounteren M이 연다 S 모드 cycle · time · instret 읽기 scounteren S가 연다 U 모드 두 문이 모두 열려 있어야 읽기 가능 비트 배치(두 문 공통): CY = 0 · TM = 1 · IR = 2 · HPM3–31 = 3–31
그림 17-11. 카운터 접근 문. mcounteren은 “다음으로 구현된 낮은 모드”(S가 있으면 S, 없으면 U)의 읽기를, scounteren은 U 모드의 읽기를 허락한다. 문이 닫혀 있으면 읽기가 illegal-instruction 예외가 되고, M 모드는 그 트랩을 받아 값을 흉내 내 줄 수 있다. mcountinhibit는 문이 아니라 카운터 자체의 브레이크다.
SIMULATOR

카운터 문 열고 닫기

mcounteren (M이 설정)

scounteren (S가 설정)

mcountinhibit (M이 설정)

TM 비트는 없다 (항상 0)
처음 설정에서 U 모드의 rdtime은 성공하지만 rdcycle은 실패한다. mcounteren.CY는 열려 있어도 scounteren.CY가 닫혀 있기 때문이다. 반대로 IR은 S가 열어 주었는데도 M이 막고 있어 U도 S도 읽지 못한다. 리눅스는 보안 때문에 사용자 공간의 cycle 직접 읽기를 막아 두는 경우가 있다.

새 상태 지키기와 간접 접근: Smstateen · Smcsrind

Smstateen: 운영체제가 모르는 상태를 막는 문

새 확장이 하트에 레지스터를 추가했는데, 오래된 운영체제나 하이퍼바이저는 그 존재를 모른다고 하자. 문맥 전환 때 그 레지스터를 저장·복원하지 않으므로, 한 사용자 스레드나 게스트 OS가 써 둔 값을 다른 스레드나 게스트가 읽을 수 있다. 이것이 은닉 채널(covert channel)이다. 부동소수점과 벡터는 mstatus의 FS·VS 필드로 막을 수 있지만, 확장마다 sstatus에 필드를 하나씩 줄 수는 없다. 그래서 Smstateen/Ssstateen 확장은 “상태 허용” 비트를 모아 둘 전용 CSR을 만들었다.

mstateen0 비트 63 …… 48 (M 이하 수준용 상태) 2 1 0 (U 수준까지 보이는 상태) mstateen0 (M) 비트 = 0 → 아래 전부 막힘 hstateen0 (HS) VS·VU 모드 접근 관리 sstateen0 (S) U·VU 모드 접근 관리 SE0(비트 63)는 아래 수준의 stateen CSR 자체에 대한 접근을 연다
그림 17-12. mstateen0의 정의된 비트. C(비트 0)는 모든 custom 상태, FCSR(1)은 Zfinx 계열에서의 fcsr, JVT(2)는 Zcmt의 jvt, 위쪽의 CTR·SRMCFG·P1P13·CONTEXT·IMSIC·AIA·CSRIND·ENVCFG·SE0는 각각 Smctr, Ssqosid의 srmcfg, hedelegh, Sdtrig의 context, AIA, Smcsrind, envcfg CSR, 그리고 하위 stateen 자체에 대한 접근을 관리한다. 위 수준에서 0인 비트는 아래 수준에서 읽기 전용 0이다.

Smcsrind: 창 하나로 레지스터 배열 읽기

인터럽트 우선순위 표나 성능 카운터 설정처럼 비슷한 레지스터가 수십 개씩 늘어선 상태를 모두 CSR 주소에 하나씩 배정하면 4096칸이 금방 바닥난다. 게다가 번호로 순회하려면 CSR 번호가 명령어에 박혀 있으므로 switch 문이 필요하다. Smcsrind/Sscsrind는 AIA 확장에서 시작된 간접 접근 방식을 일반화했다. 선택 레지스터에 번호를 쓰고, 별칭 레지스터를 읽고 쓰면 그 번호의 레지스터에 접근한다.

수준선택 CSR별칭 CSR
Mmiselect 0x350mireg 0x351, mireg2 0x352, mireg3 0x353, mireg4–6 0x355–0x357
Ssiselect 0x150sireg 0x151, sireg2 0x152, sireg3 0x153, sireg4–6 0x155–0x157
VSvsiselect 0x250vsireg 0x251 … vsireg6 0x257 (0x254 제외)

별칭 번호가 0x354를 건너뛰는 것은 그 자리가 AIA의 miph에 이미 배정되어 있기 때문이다. 선택 값의 최상위 비트(XLEN-1)가 1이면 custom 용도이고, 0이면 표준 확장에 배정된다. 지금 배정된 값으로는 AIA의 iprio0–15(0x30–0x3F), eidelivery(0x70), eithreshold(0x72), eip0–63(0x80–0xBF), eie0–63(0xC0–0xFF)가 있고, S 수준에서는 카운터 위임용 0x40–0x5F와 제어 전송 기록(Smctr)용 0x200–0x2FF가 더 있다.

miselect = 0x72 “몇 번 레지스터를 볼까” mireg 읽기·쓰기가 선택된 곳으로 선택 값이 가리키는 레지스터 (AIA IMSIC) 0x30–0x3Fiprio0 – iprio15 0x70eidelivery 0x710 (예약) 0x72eithreshold 0x80–0xBFeip0 – eip63 0xC0–0xFFeie0 – eie63
그림 17-13. 간접 CSR 접근. csrw miselect, 0x72 다음 csrr t0, mireg를 실행하면 CSR 주소를 하나도 더 쓰지 않고 IMSIC의 eithreshold를 읽는다. 지원하지 않는 선택 값으로 mireg에 접근했을 때의 동작은 UNSPECIFIED이며, 구현은 보통 illegal-instruction 예외를 낼 것으로 기대된다.
li t0, 0x30 # iprio0 의 선택 값 li t1, 0x40 # iprio15 다음 값 loop: csrw miselect, t0 # 번호만 바꾸면 되므로 switch 문이 필요 없다 csrw mireg, zero # 선택된 iprio 레지스터를 0 으로 addi t0, t0, 1 bne t0, t1, loop
이 장에서 다루지 않은 CSR

트랩 처리 CSR(mtvec, mepc, mcause, mtval, mip, mie, medeleg, mideleg)은 18장, satp와 Sv39는 19장, pmpcfg·pmpaddr·mseccfg는 20장, hstatus·hgatp 같은 하이퍼바이저 CSR은 21장에서 다룬다.

핵심 정리

  1. RISC-V의 특권 수준은 U(00), S(01), M(11) 셋이며 10은 예약이다. M 모드만 필수이고, 구현은 M / M·U / M·S·U 중 하나를 고른다.
  2. 하트는 언제나 한 모드에서만 실행되며, 모드는 트랩(같거나 더 높은 모드로)과 xRET(저장해 둔 모드로)으로만 바뀐다.
  3. CSR 주소 12비트 중 [11:10]=11이면 읽기 전용, [9:8]은 접근 가능한 최소 특권(00 U, 01 S, 10 HS, 11 M)이다. 특권 부족이나 읽기 전용 CSR 쓰기는 illegal-instruction 예외다.
  4. csrr(=csrrs rd, csr, x0)은 쓰기를 하지 않으므로 읽기 전용 CSR에도 안전하지만, csrw는 값과 상관없이 쓰기를 시도한다.
  5. misa는 MXL(XLEN)과 A–Z 확장 비트를 담는 WARL 레지스터다. 의존 관계를 어기는 쓰기는 관련 기능을 모두 끈다.
  6. mstatus는 인터럽트 허용·트랩 스택(MIE·MPIE·MPP), 확장 상태(FS·VS·XS·SD), 메모리 권한(MPRV·SUM·MXR), 가상화 가로채기(TVM·TW·TSR)를 모은 조종판이며, sstatus는 그 제한된 보기다.
  7. WPRI는 보존·무시, WLRL은 합법 값만 쓰기, WARL은 무엇이든 쓰고 합법 값만 읽기다. WARL은 “써 보고 읽기”로 하드웨어 기능을 탐지하게 해 준다.
  8. 카운터 읽기는 mcounteren(→S)과 scounteren(→U)이 문을 열어야 가능하고, mcountinhibit은 증가만 멈춘다. Smstateen은 새 확장 상태를 막는 문, Smcsrind는 선택·별칭 CSR로 레지스터 배열에 접근하는 창이다.

확인 퀴즈

모든 RISC-V 하드웨어 구현이 반드시 갖춰야 하는 특권 모드는?

M 모드는 기계 전체에 제한 없이 접근할 수 있는 유일한 모드라서 모든 구현에 필수다. 가장 단순한 구현은 M 모드만 가질 수 있다.

CSR 주소 0xC01(time)에 대해 주소만 보고 말할 수 있는 것은?

0xC01 = 11 00 0000 0001. [11:10]=11이라 읽기 전용, [9:8]=00이라 U 수준이다. 단, 실제로 U에서 읽으려면 mcounteren.TM과 scounteren.TM이 열려 있어야 한다.

M 모드에서 csrw mhartid, t0를 실행하면?

0xF14는 [11:10]=11인 읽기 전용 CSR이다. 읽기 전용 레지스터에 쓰기를 시도하면 특권과 상관없이 illegal-instruction 예외가 난다. (읽기·쓰기 CSR 안의 읽기 전용 비트라면 무시된다.)

M·S·U가 있는 하트에서 mcounteren.CY=1, scounteren.CY=0일 때 rdcycle의 결과는?

mcounteren은 S의 접근을, scounteren은 U의 접근을 연다. U는 두 레지스터의 같은 비트가 모두 1이어야 한다.

misa의 MXL 필드가 2라면?

MXLEN = 2MXL+4이므로 MXL=1은 32, MXL=2는 64다. MXL=3은 예약이다. misa 전체가 0이면 misa를 구현하지 않았다는 뜻이다.

WARL 필드에 대한 설명으로 옳은 것은?

첫째는 WLRL(예외는 선택 사항), 둘째는 WPRI에 대한 설명이다. WARL은 “써 보고 다시 읽기”로 지원 값을 알아낼 수 있다.

S 모드 소프트웨어가 sstatus를 읽었을 때 볼 수 없는 필드는?

sstatus는 mstatus의 제한된 보기다. MIE·MPIE·MPP·MPRV·TVM·TW·TSR 같은 M 모드 필드는 sstatus에서 보이지 않는다.

Smstateen 확장이 해결하려는 문제는?

새 확장의 상태를 낮은 수준이 쓰지 못하게 막아 두면, 그 상태를 저장·복원하지 않는 오래된 소프트웨어 아래에서도 정보가 새지 않는다. CSR 주소 절약은 Smcsrind의 목표다.

더 읽을거리