Chapter 21

H 확장: 하이퍼바이저

클라우드 서버 한 대에는 수십 개의 운영체제가 동시에 돈다. 그 운영체제들은 저마다 자기가 컴퓨터의 주인이라고 믿고 페이지 테이블을 바꾸고, 인터럽트를 끄고, 특권 명령을 실행한다. H 확장은 이런 게스트 운영체제를 대부분 트랩 없이 하드웨어에서 직접 돌리게 해 주는 특권 구조의 확장이다. 핵심은 두 가지다. 게스트 전용의 가짜 S 모드(VS 모드)와 그 모드가 쓰는 복제 CSR, 그리고 게스트 물리 주소를 한 번 더 바꾸는 2단계 주소 변환이다.

왜 하드웨어 가상화가 필요한가

가상 머신을 만드는 가장 오래된 방법은 트랩하고 흉내 내기(trap-and-emulate)다. 게스트 운영체제를 권한이 낮은 U 모드에서 돌리면, 게스트가 특권 명령(csrw satp, sret, sfence.vma …)을 실행할 때마다 불법 명령 예외가 나서 하이퍼바이저로 넘어온다. 하이퍼바이저는 그 명령을 해독하고, 게스트 몫의 가짜 레지스터에 결과를 반영한 뒤 게스트로 돌아간다. 1974년 Popek과 Goldberg가 정리한 “가상화 가능한 구조”의 조건이 바로 이 방식이 성립하는 조건이다.

RISC-V의 기본 특권 구조는 이 방식을 쓰기 쉽게 설계되었다. 특권 명령이 몇 개 없고 모두 확실히 트랩되기 때문이다. 하지만 트랩 한 번에 수백 사이클이 들고, 게스트 커널은 생각보다 훨씬 자주 특권 동작을 한다. 특히 가상 메모리가 문제다. 게스트가 고친 페이지 테이블을 하드웨어가 그대로 쓸 수 없으니, 하이퍼바이저는 게스트 페이지 테이블을 감시하며 진짜 주소를 넣은 섀도 페이지 테이블(shadow page table)을 따로 만들어 둘을 계속 맞춰야 한다.

① 고전적 방식: 트랩하고 흉내 내기 게스트 앱U 게스트 OS 권한을 빼앗겨 U 모드에서 실행 U 하이퍼바이저 명령 해독·흉내 · 섀도 페이지 테이블 S 펌웨어 (M) 트랩 복귀 csrw satp · sret · 페이지 테이블 쓰기 … 모두 트랩 ② H 확장: 하드웨어가 직접 지원 게스트 앱VU 게스트 OS 자기 CSR을 직접 다룬다 VS vsatp 트랩 없음 하이퍼바이저 G-stage 테이블 · 드문 트랩만 처리 HS 펌웨어 (M) 게스트 페이지 폴트 · 가상 명령 예외 때만 트랩
그림 21-1. 두 가지 가상화 방식. 왼쪽은 게스트 OS를 U 모드로 강등시켜 특권 동작마다 트랩(빨간 화살표)을 일으킨다. 오른쪽의 H 확장에서는 게스트 OS가 VS 모드에서 자기 몫의 CSR(vsatp 등)을 트랩 없이 직접 고치고, 하이퍼바이저는 정말 필요할 때만 개입한다.
방식게스트 OS가 도는 곳메모리 가상화대표적인 비용
트랩하고 흉내 내기U 모드 (강등)섀도 페이지 테이블특권 명령·페이지 테이블 수정마다 트랩
반가상화 (para-virtualization)U 모드 + 수정된 커널하이퍼콜로 페이지 테이블 요청게스트 커널을 고쳐야 한다
H 확장 (하드웨어 지원)VS 모드2단계 주소 변환 (vsatp + hgatp)TLB 미스 때 페이지 워크가 길어진다

H 확장은 게스트 운영체제를 수정 없이 돌리면서 트랩 횟수를 크게 줄인다. 대가는 하드웨어 복잡도와 2단계 페이지 워크의 비용이다. 이 장의 뒤쪽에서 그 비용을 정확히 세어 본다. H 확장이 켜지려면 정수 레지스터가 32개인 RV32I·RV64I 베이스여야 하고(E 베이스 불가), mtval이 읽기 전용 0이면 안 되며, 페이지 기반 변환(RV32는 Sv32, RV64는 최소 Sv39)이 있어야 한다. misa의 비트 7(문자 H)이 이 확장을 나타낸다.

H 확장의 비준

H 확장 v1.0은 2021년 말 다른 여러 확장과 함께 비준되었다. 그 뒤 RVA23 프로파일이 응용 프로세서용 S 모드 프로파일(RVA23S64)에서 H를 필수로 정하면서, 리눅스를 돌리는 범용 RISC-V 칩이라면 하이퍼바이저 지원을 갖추는 것이 기본이 되었다(23장).

V 비트와 다섯 가지 모드

H 확장은 새 특권 수준을 끼워 넣지 않는다. 대신 하트에 가상화 모드(virtualization mode) 비트 V를 하나 더한다. V는 CSR로 보이는 값이 아니라 하트 내부의 상태이고, 트랩과 트랩 복귀 때만 바뀐다. V=1이면 하트는 지금 게스트를 실행하고 있으며 2단계 주소 변환이 켜진다. 원래의 S 모드는 HS 모드(hypervisor-extended supervisor)로 이름이 바뀌고, 하이퍼바이저나 KVM 같은 호스팅 기능이 있는 OS가 여기서 돈다.

M 모드 · 펌웨어 (OpenSBI 등) V = 0 · 가장 높은 권한 · 위임하지 않은 모든 트랩을 받는다 V = 0 2단계 변환 꺼짐 V = 1 2단계 변환 켜짐 HS 모드 · 하이퍼바이저 명목 권한 S · 호스트 OS(KVM 등) U 모드 · 호스트 프로세스 명목 권한 U · 예: QEMU 같은 VMM VS 모드 · 게스트 커널 명목 권한 S · vs* CSR을 쓴다 VU 모드 · 게스트 앱 명목 권한 U 트랩 → HS (V ← 0) SRET, hstatus.SPV = 1 (V ← 1) ecall (시스템 호출 · SBI 호출)
그림 21-2. H 확장의 특권 모드. 왼쪽(V=0)은 H 확장이 없을 때와 같은 M·S·U 구조이고, 오른쪽(V=1)은 게스트를 위한 VS·VU 모드다. 게스트에서 HS로 트랩하면 V가 0이 되고, 하이퍼바이저가 hstatus.SPV=1로 둔 채 sret을 실행하면 V가 1이 되어 게스트로 돌아간다.
V명목 권한약칭이름2단계 변환
0UUUser mode꺼짐
0SHSHypervisor-extended supervisor mode꺼짐
0MMMachine mode꺼짐
1UVUVirtual user mode켜짐
1SVSVirtual supervisor mode켜짐

명목 권한(nominal privilege)이라는 말에 주목하자. VS 모드의 명목 권한은 S다. 그래서 게스트 커널 안에서는 sret도, csrr a0, sstatus도 그대로 실행된다. 권한의 크기는 M > HS > VS > VU 순서이며, HS는 VS보다 높다. 이 순서가 뒤에서 볼 트랩 위임 규칙의 바탕이다.

타입 1과 타입 2 하이퍼바이저

규격은 두 가지를 모두 염두에 둔다. Xen 같은 타입 1 하이퍼바이저는 HS 모드 자체가 하이퍼바이저이고, 리눅스 KVM 같은 타입 2는 일반 커널이 HS에서 돌면서 가상 머신도 함께 돌린다. KVM은 U 모드에 QEMU 같은 가상 머신 관리 프로그램을 두고, 게스트를 실행할 때만 sret으로 V=1 세계에 들어간다. HS 모드는 S 모드와 똑같이 동작하므로 일반 S 모드 OS는 고치지 않고 HS에서도, VS 게스트로도 돈다.

하이퍼바이저 CSR과 VS CSR 바꿔치기

H 확장이 더하는 CSR은 두 무리다. 하나는 HS 모드만 쓰는 하이퍼바이저 CSR(h로 시작)으로, 게스트를 어떻게 다룰지 정한다. 다른 하나는 VS CSR(vs로 시작)로, 보통의 S CSR을 그대로 복제한 게스트 전용 사본이다. 주소를 보면 규칙이 보인다. S CSR이 0x1xx이면 짝이 되는 VS CSR은 0x2xx, 하이퍼바이저 CSR은 대부분 0x6xx다.

CSR주소하는 일
hstatus0x600게스트 실행을 제어·기록하는 상태 레지스터 (SPV, SPVP, GVA, VTSR …)
hedeleg0x602HS로 온 예외를 VS로 한 번 더 위임
hideleg0x603HS로 온 인터럽트를 VS로 한 번 더 위임
hie0x604VS 수준 인터럽트와 게스트 외부 인터럽트(SGEI)의 허용 비트
hip0x644같은 인터럽트들의 대기 비트
hvip0x645하이퍼바이저가 게스트에 가상 인터럽트를 주입
hcounteren0x606게스트가 cycle·time·instret 등을 읽을 수 있는지
htimedelta0x605게스트가 읽는 time = 실제 time + htimedelta
hgeie0x607게스트 외부 인터럽트 허용
hgeip0xE12게스트 외부 인터럽트 대기 (읽기 전용)
henvcfg0x60AV=1일 때의 실행 환경 설정 (PBMTE, ADUE, STCE, CBZE …)
htval0x643트랩 때 기록: 게스트 물리 주소 ≫ 2
htinst0x64A트랩 때 기록: 변환된 명령어 또는 의사 명령 값
hgatp0x680G-stage 변환의 MODE·VMID·루트 PPN
VS CSR주소대신하는 S CSRS CSR 주소
vsstatus0x200sstatus0x100
vsie0x204sie0x104
vstvec0x205stvec0x105
vsscratch0x240sscratch0x140
vsepc0x241sepc0x141
vscause0x242scause0x142
vstval0x243stval0x143
vsip0x244sip0x144
vstimecmp (Sstc)0x24Dstimecmp0x14D
vsatp0x280satp0x180

M 모드에도 두 CSR이 더해진다. 게스트 물리 주소를 담는 mtval2(0x34B)와 변환된 명령어를 담는 mtinst(0x34A)로, HS의 htval·htinst와 같은 역할을 M 쪽에서 한다.

바꿔치기 규칙

H 확장의 가장 영리한 부분이 여기다. V=1일 때 게스트 커널이 csrr a0, sepc(주소 0x141)를 실행하면, 하트는 sepc가 아니라 vsepc를 읽는다. 게스트 커널은 자기가 진짜 S 모드에 있다고 믿고 평소의 주소를 쓰지만, 하드웨어가 조용히 게스트 사본으로 연결해 준다. 그동안 HS의 진짜 sepc는 값을 그대로 지키며 기계 동작에 영향을 주지 않는다.

csrr a0, sepc CSR 주소 0x141 V V 비트로 선택 V = 0 V = 1 HS 수준 S CSR · V = 0 일 때 쓰인다 sstatus sie stvec sscratch sepc scause stval sip satp … VS CSR · V = 1 일 때 대신 쓰인다 vsstatus vsie vstvec vsscratch vsepc vscause vstval vsip vsatp …
그림 21-3. VS CSR 바꿔치기. 같은 명령 csrr a0, sepc라도 V=1이면 vsepc(금색)로 연결된다. 게스트는 VS CSR을 자기 주소(0x241)로 직접 건드릴 수 없고, 그렇게 하면 가상 명령 예외가 난다. VS CSR을 이름 그대로 다룰 수 있는 것은 M과 HS뿐이다.
규격 원문에서
“When V=1, the VS CSRs substitute for the corresponding supervisor CSRs, taking over all functions of the usual supervisor CSRs except as specified otherwise. Instructions that normally read or modify a supervisor CSR shall instead access the corresponding VS CSR.”

Volume II, “H” Extension — Hypervisor and Virtual Supervisor CSRs — VS 사본은 트랩이 자동으로 쓰거나 트랩 직후·SRET 직전에 곧바로 쓰이는 CSR에만 있다. 소프트웨어가 정확한 순간에 바꿔 끼울 수 없는 것들이기 때문이다.

그 이유 때문에 모든 S CSR에 짝이 있는 것은 아니다. senvcfg, scounteren, scontext는 VS 사본이 없어서 V=1에서도 같은 레지스터가 그대로 쓰인다. 하이퍼바이저는 게스트를 바꿀 때 이런 CSR을 손으로 저장·복원해야 한다.

EXPLORER

CSR 바꿔치기 탐색기

csrr a0, sepc → sepc (0x141)

HS 수준 S CSR

VS CSR

하이퍼바이저 CSR

CSR을 고르고 V를 바꿔 보자.
실행 중인 모드
하이퍼바이저 설정
실행할 명령: csrr a0, ___
명령이 가리킨 주소0x141
실제로 접근한 CSRsepc
결과정상
V=1로 바꾼 뒤 satp를 고르고 hstatus.VTVM을 켜 보자. 하이퍼바이저가 게스트의 페이지 테이블 교체를 가로채고 싶을 때 쓰는 스위치다. 또 V=1에서 hgatp나 vsstatus를 이름 그대로 읽으면 무슨 일이 생기는지, senvcfg는 왜 바뀌지 않는지도 확인하자.

hstatus 한눈에 보기

hstatus는 mstatus가 M 모드를 위해 하는 일을 게스트에 대해 한다. 트랩이 들어올 때 하드웨어가 기록하는 필드(SPV·SPVP·GVA)와, 게스트의 특정 동작을 가로채는 필드(VTSR·VTW·VTVM)가 핵심이다.

hstatus (HSXLEN = 64) · 비트 63–23 WPRI 6350 HUPMM 4948 WPRI 4734 VSXL 3332 WPRI 3123 비트 22–0 VTSR22 VTW21 VTVM20 WPRI19:18 VGEIN17:12 WPRI11:10 HU9 SPVP8 SPV7 GVA6 VSBE5 WPRI4:0 트랩 때 하드웨어가 기록 VS 모드 동작 가로채기 U 모드의 HLV/HSV 허용 인터럽트 선택 · 폭 · 마스킹
그림 21-4. RV64의 hstatus 비트 배치(규격 Figure 2). HSXLEN=32이면 VSXL과 HUPMM이 없고 나머지 위치는 같다.
필드비트뜻
SPV7HS로 트랩할 때 직전의 V를 기록. V=0에서 SRET하면 V ← SPV.
SPVP8V=1에서 트랩하면 직전의 명목 권한(VS=1, VU=0)을 기록. HLV/HSV가 어떤 권한으로 게스트 메모리를 읽을지 정한다.
GVA6트랩이 stval에 게스트 가상 주소를 썼으면 1.
VSBE5VS 모드 메모리 접근의 엔디언 (0 = 리틀).
HU91이면 U 모드에서도 HLV·HLVX·HSV를 쓸 수 있다.
VGEIN17:12VS 외부 인터럽트로 연결할 게스트 외부 인터럽트 번호(0 = 없음).
VTVM · VTW · VTSR20 · 21 · 22VS 모드의 satp 접근·SFENCE.VMA / WFI / SRET을 가상 명령 예외로 가로챈다. mstatus의 TVM·TW·TSR과 같은 구실.
VSXL33:32VS 모드의 XLEN (misa.MXL과 같은 인코딩).
HUPMM49:48U 모드 HLV/HSV의 포인터 마스킹 (Ssnpm).

2단계 주소 변환: VS-stage와 G-stage

메모리 가상화가 H 확장의 두 번째 기둥이다. V=1이면 모든 메모리 접근이 두 번 변환된다. 첫 단계 VS-stage는 게스트 커널이 vsatp로 관리하는 평범한 Sv39/Sv48 페이지 테이블이고, 게스트 가상 주소(GVA)를 게스트 물리 주소(GPA)로 바꾼다. 둘째 단계 G-stage는 하이퍼바이저가 hgatp로 관리하며, GPA를 진짜 슈퍼바이저 물리 주소(SPA)로 바꾼다. 게스트는 G-stage가 있다는 사실조차 모른다. 섀도 페이지 테이블이 필요 없어지는 이유다.

V = 1 이면 모든 메모리 접근이 두 단계를 거친다 GVA게스트 가상 주소 VS-stagevsatp가 가리키는게스트 페이지 테이블 GPA게스트 물리 주소 G-stagehgatp가 가리키는하이퍼바이저 테이블 SPA실제 물리 주소 게스트 OS가 관리 하이퍼바이저가 관리 VS 단계의 페이지 테이블 읽기(주소가 GPA)도 하나하나 G-stage를 거친다
그림 21-5. 2단계 주소 변환. 금색 점은 하나의 메모리 접근이 GVA → GPA → SPA로 바뀌는 흐름이다. 점선 화살표가 중요하다. VS-stage 페이지 테이블 자체가 게스트 물리 메모리에 있으므로, 게스트 PTE를 읽는 일조차 G-stage 변환을 거쳐야 한다.

hgatp와 Sv39x4

hgatp는 satp와 비슷하게 생겼다. MODE, 가상 머신 식별자 VMID, G-stage 루트 테이블의 PPN이 들어간다. ASID가 프로세스를 구별하듯 VMID는 가상 머신을 구별해, 게스트를 바꿀 때 TLB를 통째로 비우지 않아도 되게 한다. HSXLEN=64에서 MODE 값은 0(Bare), 8(Sv39x4), 9(Sv48x4), 10(Sv57x4)이고 나머지는 예약이다. HSXLEN=32이면 0(Bare)과 1(Sv32x4)뿐이다.

hgatp (HSXLEN = 64) MODE 0 VMID PPN 6360 5958 5744 430 0 Bare · 8 Sv39x4 9 Sv48x4 · 10 Sv57x4 가상 머신 번호 (최대 14비트) G-stage 루트 테이블의 물리 페이지 번호 16 KiB 정렬 → PPN[1:0]은 항상 0 Sv39x4 게스트 물리 주소 (41비트) VPN[2] · 11비트 VPN[1] · 9 VPN[0] · 9 page offset · 12 4030 2921 2012 110 2048 PTE × 8 B = 16 KiB 루트 512 PTE · 4 KiB 512 PTE · 4 KiB 4 KiB 페이지 안 위치
그림 21-6. 위: RV64 hgatp. 아래: Sv39x4가 나누는 41비트 게스트 물리 주소. Sv39 가상 주소와 똑같고 VPN[2]만 2비트 넓다. 그래서 루트 테이블만 4배(16 KiB)로 커진다. 비트 63:41 중 하나라도 1이면 게스트 페이지 폴트다.
규격 원문에서
“In each case, the size of the incoming address is widened by 2 bits (to 34, 41, 50, or 59 bits). To accommodate the 2 extra bits, the root page table (only) is expanded by a factor of four to be 16 KiB instead of the usual 4 KiB.”

Volume II, “H” Extension — Guest Physical Address Translation — 규격은 게스트에게 가상 주소 공간의 4배까지 물리 주소 공간을 줄 수 있게 하려고 이렇게 했다고 설명한다. 예를 들어 Sv39만 지원하는 칩도 41비트 게스트 물리 주소를 만들 수 있다.

G-stage만의 규칙

G-stage의 폴트는 게스트 페이지 폴트다

G-stage에서 문제가 생기면 보통의 페이지 폴트(12·13·15)가 아니라 게스트 페이지 폴트(20·21·23)가 난다. 이 예외는 게스트에게 위임할 수 없고 항상 HS(또는 M)가 받는다. 게스트는 G-stage를 모르니 고칠 수도 없기 때문이다. 하이퍼바이저는 보통 이 폴트를 받아 게스트에게 메모리를 실제로 할당하거나(지연 할당), MMIO 장치 접근을 흉내 낸다.

중첩 페이지 워크의 비용

TLB가 모두 빗나간 최악의 경우를 세어 보자. 게스트가 n단계 페이지 테이블(Sv39이면 3)을, 하이퍼바이저가 m단계 G-stage(Sv39x4이면 3)를 쓴다고 하자.

  1. 게스트 페이지 워크는 vsatp.PPN에서 시작한다. 이 값은 GPA다. 루트 테이블의 PTE 주소 vsatp.PPN × 4096 + VPN[n−1] × 8도 GPA이므로, 그 PTE를 읽으려면 먼저 G-stage 워크(m번 읽기)로 SPA를 구해야 한다. 그다음에야 PTE 자체를 읽는다(1번). 한 게스트 단계에 m + 1번이다.
  2. 게스트 PTE 안의 PPN이 가리키는 다음 테이블도 GPA다. 그래서 n개의 게스트 단계 모두 같은 일을 반복한다: n × (m + 1)번.
  3. 마지막 리프 PTE가 준 것은 데이터의 GPA다. 이를 SPA로 바꾸려면 G-stage 워크가 한 번 더 필요하다: + m번.

합치면 페이지 테이블 읽기는 n(m+1) + m = nm + n + m = (n+1)(m+1) − 1번이고, 실제 데이터 접근 1번이 따로 붙는다. Sv39 게스트를 Sv39x4 위에서 돌리면 3·3 + 3 + 3 = 15번, Sv48 위의 Sv48x4라면 4·4 + 4 + 4 = 24번이다. 가상화가 없을 때 각각 3번, 4번이던 것에 비하면 5–6배다. 24라는 숫자는 x86-64의 4단계 × 4단계 중첩 페이징을 분석한 Bhargava 등(ASPLOS 2008)의 결과와도 같다.

G-stage L2 G-stage L1 G-stage L0 게스트 PTE 읽기 이 줄의 읽기 VS 레벨 2 PTE VS 레벨 1 PTE VS 레벨 0 PTE 데이터의 GPA 주소 = vsatp.PPN 기준 주소 = 앞 PTE의 PPN 기준 주소 = 앞 PTE의 PPN 기준 리프 PTE가 준 GPA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 데이터 3 + 1 3 + 1 3 + 1 3 3 × (3 + 1) + 3 = 15 번의 페이지 테이블 읽기 = (3 + 1)(3 + 1) − 1 가상화가 없으면 3번 · TLB 미스 한 번의 비용이 다섯 배가 된다
그림 21-7. Sv39 게스트 × Sv39x4 G-stage의 2차원 페이지 워크. 청록 칸은 G-stage PTE 읽기, 보라 칸은 게스트 PTE 읽기, 숫자는 접근 순서다. 각 줄은 “GPA 하나를 G-stage로 변환(3번) → 그 SPA에서 게스트 PTE 읽기(1번)”이고, 마지막 줄은 데이터의 GPA 변환이다.
ANIMATION

2차원 페이지 워크 계수기

한 단계나 자동 재생을 누르면 TLB가 모두 빗나간 상황에서 메모리 접근이 하나씩 일어난다.
게스트 (vsatp)
호스트 (hgatp)
실행
페이지 테이블 읽기0 / 15
공식 n·m + n + m3·3 + 3 + 3
가상화 없을 때3
배수5.0×
호스트를 Bare로 바꾸면 G-stage가 사라져 게스트 단계 수만큼만 읽는다. 게스트와 호스트를 하나씩 키우며 표의 숫자가 어떻게 늘어나는지, 그리고 왜 하이퍼바이저가 G-stage에 큰 페이지(2 MiB, 1 GiB)를 즐겨 쓰는지 생각해 보자. 큰 페이지는 m을 줄인다.
게스트 \ G-stageBare (m=0)Sv39x4 (m=3)Sv48x4 (m=4)Sv57x4 (m=5)
Sv39 (n=3)3151923
Sv48 (n=4)4192429
Sv57 (n=5)5232935
그래서 TLB와 VMID가 중요하다

실제 칩은 이 비용을 거의 치르지 않는다. TLB가 GVA → SPA를 곧바로 기억하고, 페이지 워크 캐시가 중간 단계의 결과를 붙잡아 두기 때문이다. 규격은 hgatp.VMID와 VS 수준 ASID를 합친 것을 실질적인 ASID로 보라고 설명한다. 덕분에 여러 가상 머신의 변환이 TLB에 섞여 있어도 서로를 구별할 수 있고, 월드 스위치 때 TLB를 비울 필요가 없다. VMID 비트 수는 구현마다 다르며 Sv39x4·Sv48x4·Sv57x4에서 최대 14비트, Sv32x4에서 최대 7비트다.

게스트 페이지 폴트와 트랩 위임

H 확장은 원인 코드도 늘린다. 게스트를 위한 예외 넷과 인터럽트 넷이다. VS 모드의 ecall은 HS 모드의 ecall(9)과 다른 코드 10을 받는다. 두 ecall을 따로 위임할 수 있게 하려는 것이다.

종류코드이름언제
예외8Environment call from U-mode or VU-mode사용자·게스트 앱의 시스템 호출
예외10Environment call from VS-mode게스트 커널의 SBI 호출
예외20Instruction guest-page fault명령 인출 중 G-stage 실패
예외21Load guest-page fault읽기 중 G-stage 실패
예외22Virtual instructionV=1에서 HS라면 허용될 명령이 막힘
예외23Store/AMO guest-page fault쓰기·AMO 중 G-stage 실패
인터럽트2 · 6 · 10Virtual supervisor software / timer / externalVS 수준 인터럽트 (VSSI·VSTI·VSEI)
인터럽트12Supervisor guest external interrupt게스트 외부 인터럽트가 HS에 알림 (SGEI)

가상 명령 예외

V=1에서 어떤 명령이 막혔는데, 그 명령이 HS 모드였다면 실행될 수 있었던(HS-qualified) 경우에는 불법 명령 예외(2) 대신 가상 명령 예외(virtual instruction, 22)가 난다. 예를 들어 게스트 커널이 hgatp를 읽거나, VTSR=1일 때 sret을, VTVM=1일 때 satp에 쓰거나, HLV·HFENCE 같은 하이퍼바이저 명령을 실행하는 경우다. 규격은 M이 가상 명령 예외를 HS로 곧장 위임하고, 불법 명령 예외는 먼저 M에서 살펴보는 것이 보통이라 가상 명령 예외가 더 빨리 처리될 것이라고 설명한다.

위임은 두 단계로

예외와 인터럽트는 기본적으로 모두 M으로 간다. medeleg/mideleg이 HS로 내려 보내고, V=1에서 일어난 트랩이라면 hedeleg/hideleg가 한 번 더 VS로 내려 보낸다. 트랩은 자기보다 권한이 낮은 모드로는 가지 않으므로, HS에서 일어난 트랩이 VS로 가는 일은 없다.

트랩 발생 V = 1 (VS · VU) medeleg[원인] = 1 ? M 모드 V ← 0, MPV ← 1 아니오 위임하지 않은 트랩은 모두 M으로 예 hedeleg[원인] = 1 ? HS 모드 V ← 0, SPV ← 1 아니오 예 VS 모드 V는 1 그대로 · vs* 기록 hedeleg 읽기 전용 0 비트: 9·10·11·16·20·21·22·23 → VS로 못 간다
그림 21-8. V=1에서 일어난 동기 예외의 처리 위치. 인터럽트는 같은 구조를 mideleg·hideleg로 판단한다. VS 수준 인터럽트(2·6·10)는 H 확장이 있으면 mideleg의 해당 비트가 읽기 전용 1이라 언제나 M을 건너뛴다.
받는 모드V 변화하드웨어가 기록하는 것
MV ← 0mepc, mcause, mtval, mtval2, mtinst, mstatus의 MPV(RV64 비트 39)·MPP·GVA(비트 38)·MPIE·MIE. RV32는 MPV·GVA가 mstatush 비트 7·6.
HSV ← 0sepc, scause, stval, htval, htinst, hstatus의 SPV·SPVP(V=1에서 왔을 때만)·GVA, sstatus의 SPP·SPIE·SIE
VSV는 1 유지vsepc, vscause, vstval, vsstatus의 SPP·SPIE·SIE. hstatus와 HS의 sstatus는 그대로

게스트 페이지 폴트가 HS로 오면 stval에는 평소처럼 게스트 가상 주소가, htval에는 실패한 게스트 물리 주소를 오른쪽으로 2비트 민 값(또는 0)이 들어간다. 2비트를 미는 이유는 RV32에서도 34비트 GPA를 담기 위해서이고, PMP 주소 레지스터와 PTE가 물리 주소를 담는 방식과 같다. 폴트가 VS 페이지 테이블을 읽다가 났다면 htval은 그 PTE의 GPA이고, htinst에는 특별한 의사 명령 값 0x00003000(RV64, VS-stage 변환용 64비트 읽기)이나 0x00003020(A/D 갱신용 쓰기)이 들어가 이 상황을 알려 준다. RV32 게스트라면 0x00002000·0x00002020이다.

SIMULATOR

트랩 경로 시뮬레이터

M 모드펌웨어 · V = 0 HS 모드하이퍼바이저 · V = 0 VS 모드게스트 커널 · V = 1 VU 모드게스트 앱 · V = 1 출발 scause
사건을 고르고 트랩 발생을 눌러 보자.
위임 설정
실행
처리하는 모드—
트랩 뒤 V—
원인 레지스터—
“흔한 위임값”은 펌웨어가 게스트 관련 예외(10·20·21·22·23)를 HS로 넘기고, 하이퍼바이저가 게스트 앱의 시스템 호출과 페이지 폴트(8·12·13·15)를 게스트에게 넘기는 설정이다. 게스트 페이지 폴트에서 hedeleg를 켜려 해 보자. 왜 막혀 있을까? 타이머 인터럽트에서 hideleg[6]을 켜고 끄며 원인 코드가 6에서 5로 바뀌는 것도 확인하자.

하이퍼바이저 명령과 월드 스위치

HLV · HLVX · HSV: 게스트의 눈으로 메모리 읽기

하이퍼바이저는 종종 게스트의 가상 주소로 메모리를 읽어야 한다. 게스트가 SBI 호출로 넘긴 버퍼 주소를 따라가거나, MMIO 폴트를 일으킨 게스트 명령어를 가져와 해독할 때다. 소프트웨어로 두 단계 페이지 테이블을 걸어가는 대신 가상 머신 로드·스토어(virtual-machine load/store) 명령을 쓴다. 이 명령들은 V=0인 HS(또는 M, HU=1이면 U)에서 실행되지만, 메모리 접근만은 V=1이고 권한이 hstatus.SPVP인 것처럼 2단계 변환을 거친다.

명령인코딩 (funct7 · rs2 · funct3=100 · opcode=1110011)설명
HLV.B / HLV.BU0110000 · 00000 / 00001게스트 주소에서 바이트 읽기 (부호 / 0 확장)
HLV.H / HLV.HU0110010 · 00000 / 00001하프워드 읽기
HLVX.HU0110010 · 00011읽기 권한 대신 실행 권한으로 검사하는 하프워드 읽기
HLV.W / HLV.WU0110100 · 00000 / 00001워드 읽기 (HLV.WU는 RV64 전용)
HLVX.WU0110100 · 00011실행 권한으로 검사하는 워드 읽기 (RV32에서도 유효)
HLV.D0110110 · 00000더블워드 읽기 (RV64 전용)
HSV.B/H/W/D0110001/0110011/0110101/0110111 · rs2게스트 주소에 쓰기 (rd 자리는 0, HSV.D는 RV64 전용)

HLVX는 게스트의 명령어를 가져오는 일을 흉내 내려고 만들었다. 실행만 되고 읽기는 안 되는 페이지에서도 명령어를 읽을 수 있어야 하기 때문이다. 그래도 예외는 명령 인출 예외가 아니라 일반 로드 예외로 보고된다. V=1에서 이 명령들을 실행하면 가상 명령 예외, HU=0인 U 모드에서 실행하면 불법 명령 예외다.

HFENCE.VVMA와 HFENCE.GVMA

명령인코딩무엇을 비우나
HFENCE.VVMA rs1, rs20010001 rs2 rs1 000 00000 1110011지금 hgatp.VMID 가상 머신의 VS-stage 변환. rs1 = 게스트 가상 주소, rs2 = 게스트 ASID. “잠깐 VS 모드에 들어가 SFENCE.VMA를 한 것”과 같다.
HFENCE.GVMA rs1, rs20110001 rs2 rs1 000 00000 1110011G-stage 변환. rs1 = 게스트 물리 주소 ≫ 2, rs2 = VMID. HS에서는 mstatus.TVM=0일 때만 실행된다.

V=1에서 sfence.vma를 실행하면 그 인자는 게스트 가상 주소와 게스트 ASID로 해석되고, 현재 VMID의 VS-stage 변환에만 적용된다. 게스트 커널은 평소대로 sfence.vma를 쓰면 되고, G-stage는 건드릴 수 없다. G-stage 테이블을 고친 하이퍼바이저는 hfence.gvma로 정리해야 한다.

월드 스위치

하이퍼바이저가 게스트 A를 멈추고 게스트 B를 돌리는 일을 월드 스위치(world switch)라 한다. VS CSR 덕분에 저장할 것이 분명하다. 게스트의 정수 레지스터와 함께 vs* CSR을 메모리에 저장하고 B의 값을 넣으면 된다. 주의할 점은 vsatp와 hgatp를 한꺼번에 바꿀 방법이 없다는 것이다.

게스트 A (V = 1) vsatp · vsepc · vsstatus … hgatp.VMID = 1 게스트 B (V = 1) vsatp · vsepc · vsstatus … hgatp.VMID = 2 타이머 인터럽트 등으로 A의 시간 조각이 끝났다 트랩 sret ① 트랩 진입V ← 0, SPV ← 1 ② A 저장vs* CSR → 메모리 ③ vsatp ← 0VS 변환 끄기 ④ hgatp ← BB의 VMID · 루트 ⑤ B 복원vs* CSR, 끝으로 vsatp ⑥ sretV ← SPV = 1 HS 모드 · 하이퍼바이저의 월드 스위치 (V = 0)
그림 21-9. 월드 스위치. 금색 점이 지나가는 단계가 차례로 밝아진다. 단계 ③–⑤의 순서(vsatp를 0으로 → hgatp 교체 → 새 vsatp)는 규격이 직접 권하는 순서다. 그래야 추측 실행이 A의 VS 변환을 B의 VMID로 TLB에 넣는 일이 없다.
# ② A의 VS CSR 저장 (vsstatus, vsie, vstvec, vsscratch, vsepc, vscause, vstval, vsip, vsatp …) csrr t0, vsepc sd t0, VCPU_VSEPC(s0) # s0 = 게스트 A의 vCPU 구조체 # ③ VS-stage를 먼저 끈다 csrw vsatp, zero # ④ G-stage와 VMID를 B의 것으로 ld t1, VCPU_HGATP(s1) # s1 = 게스트 B의 vCPU 구조체 csrw hgatp, t1 # ⑤ B의 VS CSR 복원, vsatp는 맨 마지막에 ld t2, VCPU_VSATP(s1) csrw vsatp, t2 ld t3, VCPU_PC(s1) csrw sepc, t3 # B가 멈췄던 pc # ⑥ hstatus.SPV(비트 7)=1, sstatus.SPP(비트 8)=1 → sret이 VS 모드로 돌아간다 li t0, 0x80 csrs hstatus, t0 li t0, 0x100 csrs sstatus, t0 sret # V ← 1, 게스트 B 재개
규격 원문에서
“No mechanism is provided to atomically change vsatp and hgatp together. Hence, to prevent speculative execution causing one guest’s VS-stage translations to be cached under another guest’s VMID, world-switch code should zero vsatp, then swap hgatp, then finally write the new vsatp value.”

Volume II, “H” Extension — Memory-Management Fences

시간도 가상화해야 한다. 게스트가 time을 읽으면 하드웨어가 htimedelta를 더한 값을 돌려준다. 게스트를 다른 하트로 옮기거나 잠시 멈췄다 다시 돌릴 때 게스트가 보는 시간이 매끄럽게 이어지게 하려는 것이다. Sstc가 있으면 게스트 커널은 stimecmp(실제로는 vstimecmp)에 직접 써서 타이머를 맞출 수 있고, 비교 기준은 time + htimedelta다.

게스트에게 인터럽트 전달하기

게스트 커널이 보는 인터럽트는 S 수준 인터럽트(소프트웨어 1, 타이머 5, 외부 9)다. 하지만 하드웨어에서 이들은 VS 수준 인터럽트(2, 6, 10)로 존재한다. hideleg의 비트 2·6·10을 켜 두면, VS 수준 인터럽트가 게스트에게 위임되면서 자동으로 S 수준 번호로 번역된다. 게스트의 vscause에는 9·5·1이 기록된다. 수정하지 않은 커널이 자기 인터럽트를 그대로 알아보게 하려는 장치다.

VS 수준 인터럽트가 걸리는 길은 세 가지다.

패스스루 장치 IMSIC (하트 0) M 수준 파일 S 수준 파일 게스트 파일 1 게스트 파일 2 게스트 파일 3 MSI 쓰기 hgeip 3 2 1 hstatus.VGEIN = 2 hvip.VSEIP 하이퍼바이저가 직접 주입 OR hip.VSEIP VS 외부 인터럽트 대기 hideleg[10] = 1 게스트에게 넘김 게스트 커널 (VS) vscause = 인터럽트 9 하이퍼바이저를 거치지 않는 길 hgeie는 HS에 알리는 SGEI만 거른다
그림 21-10. 게스트 외부 인터럽트의 길. 장치가 MSI로 IMSIC의 게스트 인터럽트 파일 2에 쓰면 hgeip 비트 2가 켜지고, hstatus.VGEIN=2이므로 그대로 hip.VSEIP가 된다. hideleg[10]=1이면 게스트 커널은 하이퍼바이저의 개입 없이 외부 인터럽트(코드 9)를 받는다.

AIA 규격(22장)의 IMSIC는 H 확장을 가진 하트에 대해 게스트 인터럽트 파일을 둘 수 있고, 그 개수는 정확히 GEILEN개다. 게스트 커널이 vsiselect·vsireg로 접근하는 인터럽트 파일은 hstatus.VGEIN이 고른 게스트 파일이다. 그래서 게스트는 자기 인터럽트 파일을 진짜 S 수준 파일처럼 다루고, 장치는 MSI를 그 파일에 바로 쓴다. 하이퍼바이저는 가상 하트를 다른 물리 하트로 옮길 때만 VGEIN과 IOMMU의 MSI 변환을 고치면 된다. GEILEN은 구현마다 다르며 RV64에서 최대 63, RV32에서 최대 31이다. 그보다 많은 가상 하트는 hvip로 흉내 내면 된다.

인터럽트 우선순위

HS로 향하는 여러 인터럽트가 동시에 걸리면 SEI, SSI, STI, SGEI, VSEI, VSSI, VSTI, LCOFI 순으로 처리한다. VS 수준 인터럽트는 V=0(HS 실행 중)일 때도 hip에 대기 상태로 남아 있다가, 위임되어 있으면 게스트로 돌아간 뒤에 게스트에게 전달된다.

핵심 정리

  1. H 확장 v1.0은 가상화 모드 비트 V를 더해 M·HS·U(V=0)와 VS·VU(V=1)의 다섯 모드를 만든다. 일반 S 모드 OS는 고치지 않고 HS에서도, VS 게스트로도 돈다.
  2. V=1이면 S CSR 접근이 VS CSR(0x2xx)로 바꿔치기된다. 하이퍼바이저 CSR(0x6xx)과 VS CSR을 이름 그대로 쓰려 하면 가상 명령 예외(22)가 난다.
  3. 메모리는 VS-stage(vsatp)와 G-stage(hgatp) 두 번 변환된다. Sv39x4·Sv48x4·Sv57x4는 주소를 2비트 넓히고 루트 테이블만 16 KiB로 키운다. G-stage 접근은 U 모드 접근으로 검사한다.
  4. TLB가 모두 빗나가면 페이지 테이블 읽기는 n·m + n + m = (n+1)(m+1) − 1번이다. Sv39/Sv39x4는 15번, Sv48/Sv48x4는 24번. TLB와 VMID가 이 비용을 숨긴다.
  5. 게스트 페이지 폴트(20·21·23)와 가상 명령 예외(22), VS ecall(10)은 VS로 위임할 수 없다. 트랩은 medeleg → hedeleg 순으로 M → HS → VS로 내려간다. htval은 GPA ≫ 2를 담는다.
  6. HLV·HLVX·HSV는 하이퍼바이저가 게스트 주소로 메모리에 접근하게 하고, HFENCE.VVMA·HFENCE.GVMA는 각 단계의 변환 캐시를 정리한다. 월드 스위치는 vsatp=0 → hgatp → vsatp 순서로 한다.
  7. VS 수준 인터럽트(2·6·10)는 위임되면 S 수준 번호(1·5·9)로 번역된다. hvip 주입, vstimecmp, IMSIC 게스트 인터럽트 파일 + VGEIN이 게스트에 인터럽트를 전한다.

확인 퀴즈

V=1인 게스트 커널이 csrr a0, sepc를 실행하면 무엇을 읽는가?

V=1이면 S CSR 주소에 대한 접근이 짝이 되는 VS CSR로 바뀐다. 가상 명령 예외는 vsepc를 자기 주소(0x241)로 직접 읽으려 할 때 난다.

게스트 커널이 읽은 주소의 GPA가 G-stage에 매핑되어 있지 않을 때 나는 예외와 코드는?

G-stage의 실패는 게스트 페이지 폴트다. 명령 인출 20, 읽기 21, 쓰기·AMO 23이다. VS-stage의 실패라면 보통의 페이지 폴트(13)가 난다.

Sv39 게스트가 Sv39x4 G-stage 위에서 돌 때, TLB가 모두 빗나가면 데이터 접근 전에 페이지 테이블을 최대 몇 번 읽는가?

게스트 단계마다 G-stage 3번 + PTE 1번 = 4번, 세 단계면 12번. 마지막 GPA 변환에 3번을 더해 15번이다. 데이터 접근까지 세면 16번의 메모리 접근이 된다.

Sv39x4의 G-stage 루트 페이지 테이블이 16 KiB인 이유는?

41비트 GPA의 VPN[2]는 11비트라 2048개 PTE × 8바이트 = 16 KiB가 된다. 루트 테이블만 커지고 나머지 단계와 PTE 형식은 Sv39와 같다.

게스트 커널의 ecall(VS 모드, 코드 10)을 hedeleg로 게스트 자신에게 위임할 수 있는가?

게스트 커널의 ecall은 보통 SBI 호출이고, 규격은 HS 하이퍼바이저가 게스트를 위해 SBI를 구현하리라 기대한다. 그래서 VS로 위임할 수 없다.

hideleg[6]=1일 때 VS 타이머 인터럽트를 받은 게스트의 vscause에 기록되는 인터럽트 코드는?

VS 수준 인터럽트는 위임될 때 S 수준 번호로 번역된다. 10→9, 6→5, 2→1. 게스트 커널은 평소의 S 타이머 인터럽트(5)로 알아본다.

월드 스위치에서 규격이 권하는 CSR 쓰기 순서는?

두 CSR을 원자적으로 바꿀 방법이 없으므로, VS-stage를 먼저 꺼서 추측 실행이 한 게스트의 VS 변환을 다른 게스트의 VMID로 캐시하지 못하게 한다.

HLVX.WU가 일반 HLV.WU와 다른 점은?

HLVX는 게스트의 명령어 인출을 흉내 내려고 만들었다. 실행만 허용된 페이지에서도 명령어를 읽을 수 있지만, 예외는 일반 로드 예외로 보고된다.

더 읽을거리