Chapter 06

RV64I, RV32E, 그리고 RV128

5장의 RV32I는 레지스터가 32비트였다. 서버와 스마트폰은 4 GiB보다 큰 메모리를 다뤄야 하므로 레지스터와 주소를 64비트로 넓힌 RV64I가 필요하다. 반대로 아주 작은 마이크로컨트롤러는 레지스터 32개조차 부담스러워 레지스터를 16개로 줄인 RV32E를 쓴다. 이 장에서는 베이스 ISA가 넓어지고 좁아질 때 무엇이 바뀌고 무엇이 그대로 남는지, 그리고 한때 규격에 있었던 RV128I가 지금 어떤 상태인지를 살펴본다.

XLEN=64: 무엇이 넓어지나

규격은 정수 레지스터의 비트 폭을 XLEN(integer register width)이라 부른다. RV64I는 RV32I 위에 지어진 베이스 ISA로, 규격의 RV64I 장은 “RV32I와 다른 점만” 적는다. 핵심은 한 줄이다. 정수 레지스터 x1–x31과 pc, 그리고 사용자 주소 공간이 64비트로 넓어진다. 명령어 길이는 여전히 32비트이고, 형식(R·I·S·B·U·J)과 opcode 배치도 RV32I 그대로다. 그래서 add는 RV64에서 그냥 64비트 덧셈이 된다.

XLEN = 32 64 63 32 31 0 a0 ← 0x80000000 (부호 있는 32비트로 −2³¹) 1000 0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111 a1 ← 5 0000 0000 0000 0000 0000 0000 0000 0101 0000 0000 0000 0000 0000 0000 0000 0000 a0 = 0xFFFFFFFF_80000000 a1 = 0x00000000_00000005 bit 31(부호 비트)을 위쪽 32비트에 복사한다 → 64비트로 읽어도 같은 −2³¹, 같은 5 명령어는 그대로 32비트 · 레지스터·pc·주소만 64비트로
그림 6-1. 32비트 레지스터가 64비트로 넓어지는 모습. RV64에서 32비트 값을 레지스터에 담을 때는 bit 31(금색 칸)을 위쪽 32비트 전체에 복사한다. 이것이 이 장 전체를 꿰뚫는 부호 확장 규칙이다. 음수(−2³¹)는 위쪽이 모두 1, 양수(5)는 모두 0이 된다.

RV32I와 RV64I의 차이를 표로 모으면 생각보다 짧다.

항목RV32IRV64I
정수 레지스터·pc 폭32비트64비트
사용자 주소 공간2³² 바이트2⁶⁴ 바이트 (실제로 쓸 수 있는 범위는 실행 환경이 정한다)
명령어 길이와 형식32비트, R·I·S·B·U·J같음
즉시값 시프트의 shamt5비트 (0–31)6비트 (0–63)
sll·srl·sra가 보는 시프트 양rs2[4:0]rs2[5:0]
lui·auipc 결과32비트 그대로32비트 결과를 64비트로 부호 확장
적재·저장lb lh lw lbu lhu sb sh sw+ ld, lwu, sd (lw는 부호 확장)
32비트 연산 전용 명령필요 없음 (모든 연산이 32비트)*W 명령 9개
상수 하나를 만드는 li최대 2개 명령최대 8개 명령 (이 장의 방식 기준)
RV32I는 RV64I의 부분집합이 아니다

규격 서론은 “왜 RV32I를 RV64I의 엄격한 부분집합으로 만들지 않았나”라는 질문에 직접 답한다. 베이스 ISA를 따로 두면 각자 자기 폭에 맞게 최적화할 수 있다는 것이다. 예를 들어 add는 RV32I에서 32비트 덧셈, RV64I에서 64비트 덧셈이다. 같은 기계어가 두 베이스에서 다른 결과를 낼 수 있으므로, RV64 하트가 RV32 프로그램을 돌리려면 “지금은 32비트 모드”라고 알려 주는 장치가 필요하다(마지막 절).

RV64I가 더한 12개 명령

RV64I가 새로 더한 명령은 열두 개뿐이다. 64비트를 메모리와 주고받는 ld·sd, 32비트를 0 확장으로 읽는 lwu, 그리고 32비트 값을 위한 *W 명령 9개다. *W 명령은 두 개의 새 major opcode, OP-IMM-32(0011011)와 OP-32(0111011)를 쓴다. 이름 그대로 OP-IMM과 OP의 “32비트 판”이다.

inst[6:5] ↓ · inst[4:2] → 000001 010011 100101 110111 (>32b) 00 01 10 11 LOAD LOAD-FP custom-0 MISC-MEM OP-IMM AUIPC OP-IMM-32 reserved STORE STORE-FP custom-1 AMO OP LUI OP-32 reserved MADD MSUB NMSUB NMADD OP-FP OP-V custom-2 reserved BRANCH JALR reserved JAL SYSTEM OP-VE custom-3 reserved RV64I 전용 opcode: *W 명령 9개 RV64I가 funct3·shamt를 더 쓰는 opcode
그림 6-2. 기본 opcode 지도(inst[1:0]=11)에서 RV64I가 쓰는 칸. 금색 두 칸 OP-IMM-32와 OP-32는 RV64I가 *W 명령을 위해 쓰는 opcode다. 파란 칸은 RV32I와 같은 opcode이지만 RV64I가 새 funct3(ld·lwu·sd)나 6비트 shamt를 더한 곳이다. 칸 이름은 v20260120 규격의 표를 따랐다.
명령opcodefunct3funct7뜻
ldLOAD 0000011011—64비트 적재
lwuLOAD 0000011110—32비트 적재, 0 확장 (lw는 부호 확장)
sdSTORE 0100011011—64비트 저장
addiwOP-IMM-32 0011011000—32비트 덧셈 후 부호 확장. addiw rd, rs, 0 = sext.w
slliwOP-IMM-32001000000032비트 왼쪽 시프트 (shamt 5비트)
srliwOP-IMM-32101000000032비트 논리 오른쪽 시프트
sraiwOP-IMM-32101010000032비트 산술 오른쪽 시프트
addwOP-32 0111011000000000032비트 덧셈
subwOP-32000010000032비트 뺄셈. subw rd, x0, rs = negw
sllwOP-32001000000032비트 왼쪽 시프트, 양은 rs2[4:0]
srlwOP-32101000000032비트 논리 오른쪽 시프트
srawOP-32101010000032비트 산술 오른쪽 시프트

눈여겨볼 점은 *W가 붙은 명령이 덧셈·뺄셈·시프트뿐이라는 것이다. andw, orw, xorw, sltw 같은 명령은 없다. 비교 명령과 논리 연산은 64비트 판을 그대로 써도 32비트 결과가 맞게 나오기 때문이다. 그 이유가 다음 절의 주제다.

32비트 값은 부호 확장해서 보관한다

C의 int나 unsigned int는 RV64에서도 32비트다. 이런 값을 64비트 레지스터에 어떻게 담을지 정해 두지 않으면, 연산할 때마다 위쪽 32비트를 정리하는 명령이 붙어야 한다. RV64I는 컴파일러와 호출 규약이 지키는 하나의 약속, 즉 부호 확장 불변식(sign-extension invariant)으로 이 문제를 푼다.

규격 원문에서
“The compiler and calling convention maintain an invariant that all 32-bit values are held in a sign-extended format in 64-bit registers. Even 32-bit unsigned integers extend bit 31 into bits 63 through 32.”

Volume I, RV64I · Integer Computational Instructions — 부호 없는 32비트 값까지 bit 31을 위로 복사한다는 점이 핵심이다.

부호 없는 값까지 부호 확장한다는 말이 처음엔 이상하게 들린다. 0xFFFFFFFF(부호 없는 4294967295)가 레지스터에서는 0xFFFFFFFF_FFFFFFFF로 보관된다는 뜻이다. 그런데 이렇게 하면 놀라운 일이 생긴다. 64비트 sltu·bltu로 비교해도 32비트 부호 없는 비교와 결과가 같다. 아래 그림이 그 이유다.

32비트 값 (부호 없는 해석, 0 → 2³²−1) A: 0 … 0x7FFFFFFF B: 0x80000000 … 0xFFFFFFFF 위 32비트 = 0 위 32비트 = 1…1 A < B 이면 A′ < B′ — 순서가 그대로다 64비트 레지스터 (부호 없는 해석, 0 → 2⁶⁴−1) A′ B′ 32비트 값은 이 사이에 절대 오지 않는다 0x00000000_00000000 0xFFFFFFFF_FFFFFFFF A′: 0 … 0x00000000_7FFFFFFF B′: 0xFFFFFFFF_80000000 … 끝
그림 6-3. 부호 없는 32비트 값을 부호 확장해 64비트에 담아도 크기 순서가 보존된다. 아래쪽 절반(A)은 64비트 공간의 맨 앞으로, 위쪽 절반(B)은 맨 끝으로 가고 둘 다 안에서는 순서가 그대로다. 그래서 64비트 sltu·bltu·bgeu가 32비트 부호 없는 비교 결과를 그대로 낸다. 부호 있는 비교(slt·blt)는 부호 확장의 정의상 당연히 맞는다.

규격은 이 불변식에서 따라 나오는 결과를 이렇게 정리한다.

*W 명령의 정의는 단순하다. 입력의 위쪽 32비트는 무시하고, 아래 32비트로 계산한 뒤, 결과 32비트를 64비트로 부호 확장해 쓴다. 그러니 결과는 언제나 정규형이다. 아래 시뮬레이터로 64비트 판과 W 판의 차이를 비트 단위로 비교해 보자.

SIMULATOR

add vs addw: 부호 확장 탐색기

add

63위쪽 32비트32
31아래쪽 32비트0

addw

63bit 31의 복사본32
3132비트 결과0
연산
예제
두 결과가 같은가—
입력이 정규형(부호 확장형)인가—
32비트 부호 있는 오버플로—
예제 0x7fffffff + 1에서 add는 0x00000000_80000000(양수 2³¹)을, addw는 0xFFFFFFFF_80000000(−2³¹)을 낸다. 64비트 결과는 더 이상 “부호 확장된 32비트 값”이 아니다(빨간 테두리). 규격은 RV64I에서 add와 addw의 결과를 비교하면 32비트 부호 있는 덧셈의 오버플로를 검사할 수 있다고 말한다. 또 위쪽 쓰레기 무시를 눌러 W 명령이 입력의 위쪽 32비트를 정말로 보지 않는지 확인하자.
sext.w와 zext.w

addiw rd, rs, 0은 아래 32비트를 부호 확장하는 sext.w 의사 명령이다. 반대로 0 확장(zext.w)은 베이스 ISA만으로는 slli rd, rs, 32 + srli rd, rd, 32 두 명령이 필요하다. Zba 확장의 add.uw가 있으면 한 명령(zext.w)으로 줄어든다(14장). 배열 인덱스로 쓰는 unsigned int를 주소에 더할 때 자주 나온다.

시프트: shamt가 6비트로

64비트 값을 0–63칸 시프트하려면 시프트 양(shamt)이 6비트 필요하다. RV32I의 slli는 I형의 즉시값 12비트 중 아래 5비트를 shamt로, 위 7비트를 funct7처럼 쓴다. RV64I는 여기서 funct7의 맨 아래 비트(inst[25])를 shamt[5]로 빌려 온다. 그래서 RV64의 즉시값 시프트는 6비트 funct6 + 6비트 shamt 꼴이 된다. 산술 시프트를 고르는 bit 30은 그대로다.

inst[25] RV32I slli funct70000000 shamt[4:0]5비트 rs1 funct3001 rd OP-IMM0010011 bit 25는 funct7의 일부라 0이어야 한다. shamt는 0–31. RV64I slli funct6000000 shamt[5:0]6비트 rs1 funct3001 rd OP-IMM0010011 bit 25가 shamt[5]가 된다. shamt는 0–63. srai는 funct6 = 010000. RV64I slliw funct70000000 shamt[4:0]5비트 rs1 funct3001 rd OP-IMM-320011011 32비트 연산이라 shamt는 5비트. imm[5] ≠ 0 인 인코딩은 reserved.
그림 6-4. 즉시값 시프트의 세 가지 배치. 금색 세로 띠가 inst[25]다. RV32I와 slliw에서는 funct7의 일부(0)이지만, RV64I의 slli·srli·srai에서는 shamt의 6번째 비트가 된다. 같은 기계어 0x02051513(bit 25=1)은 RV64I에서 slli a0, a0, 32이지만 RV32I에서는 정의된 명령이 아니다.

레지스터로 시프트 양을 주는 sll·srl·sra도 같은 원리다. RV64I에서는 rs2의 아래 6비트를 보고, 32비트 판인 sllw·srlw·sraw는 아래 5비트(rs2[4:0])만 본다.

illegal에서 reserved로

예전 규격은 slliw·srliw·sraiw에서 imm[5] ≠ 0이면 잘못된 명령 예외를 일으키라고 했다. 지금 규격은 이 인코딩을 reserved로 바꿨고, 이는 하위 호환되는 변경이라고 밝힌다. 예외를 “반드시” 일으킬 필요가 없어진 대신, 나중에 표준 확장이 그 자리를 쓸 수 있게 된 것이다. 같은 이유로 RV32E/RV64E 장도 “x16–x31을 쓰는 인코딩은 reserved”라고 적는다.

lui·auipc와 64비트 상수 만들기

lui와 auipc는 RV64I에서도 같은 opcode를 쓴다. 다른 점은 결과의 폭이다. lui는 20비트 즉시값을 위쪽에 놓고 아래 12비트를 0으로 채운 32비트 결과를 64비트로 부호 확장한다. auipc도 같은 32비트 오프셋을 부호 확장한 뒤 pc에 더한다.

bit 63 32 31 0 lui a0, 0x7FFFF bit 31 = 0 0000 0000 7FFF F000 lui a0, 0x80000 bit 31 = 1 FFFF FFFF 8000 0000 lui+ld, auipc+jalr 같은 짝으로 닿을 수 있는 오프셋 [ −2³¹ − 2¹¹ , 2³¹ − 2¹¹ − 1 ] ≈ ±2 GiB
그림 6-5. RV64의 lui. 만들어진 32비트 값의 bit 31이 1이면 위쪽 32비트가 모두 1로 채워진다(금색). 그래서 lui 하나로는 0x00000000_80000000 같은 “bit 31만 켜진 양수”를 만들 수 없다. 아래 상자는 규격이 밝힌, 상위 즉시값과 12비트 오프셋을 짝지어 닿을 수 있는 범위다.

li는 최대 몇 개의 명령이 될까

RV32에서 아무 32비트 상수나 lui + addi 두 명령이면 만들 수 있었다(4장). 64비트 상수는 그렇게 되지 않는다. 즉시값은 한 번에 최대 20비트(lui)와 12비트(addi)뿐이고, 레지스터 위쪽에 값을 직접 넣는 명령이 없기 때문이다. 컴파일러와 어셈블러는 다음과 같이 재귀적으로 상수를 만든다(LLVM의 RISCVMatInt를 단순화한 방식).

  1. 값이 부호 있는 32비트 범위에 들면 lui + addiw(또는 작은 값이면 addi 하나)로 끝낸다. addiw를 쓰는 까닭은 0x7FFFF800–0x7FFFFFFF처럼 lui가 0x80000으로 반올림되어 음수가 된 경우에도 32비트 덧셈이 넘쳐 다시 올바른 양수로 돌아오기 때문이다.
  2. 그렇지 않으면 아래 12비트를 부호 있는 값 lo로 떼어 낸다. 남은 값(value − lo)은 아래 12비트가 0이므로 뒤쪽 0의 개수만큼 오른쪽으로 민다.
  3. 민 값을 같은 방법으로 만든 뒤(재귀), slli로 되돌리고 addi lo를 더한다.

한 번 재귀할 때마다 많아야 두 명령(slli, addi)이 붙고, 최악의 경우 8개 명령이 된다. 이 책의 구현을 무작위 64비트 값 20만 개로 시험해 보면 모두 8개 이하였다.

SIMULATOR

64비트 상수 빌더

    실행
    예제
    명령 수—
    현재 a0—
    목표와 일치—
    0x80000000을 넣어 보자. 32비트 RV32라면 lui 한 개면 되지만, RV64에서는 lui 0x80000이 음수(0xFFFFFFFF_80000000)가 되므로 addi 1 + slli 31로 만든다. 0x7ffff800에서는 addiw가 32비트에서 넘쳐 양수로 돌아오는 모습을 보자. 금색으로 깜빡이는 칸이 그 명령이 바꾼 비트다.
    실제 컴파일러는 더 영리하다

    이 장의 방식은 이해하기 쉬운 기본형이다. LLVM과 GCC는 여기에 “1을 넣고 왼쪽으로 민 뒤 srli로 앞쪽 0을 만드는” 요령이나, Zba·Zbb·Zbs 확장 명령을 섞어 더 짧은 열을 찾는다. 예를 들어 0x7FFFFFFF_FFFFFFFF는 addi −1 + srli 1 두 명령이면 된다(이 시뮬레이터는 3개). 또 8개 명령짜리 상수는 아예 메모리 상수 풀에서 ld로 읽어 오는 편이 나을 때도 있다.

    RV32E와 RV64E: 레지스터를 16개로

    반대 방향의 변형도 있다. RV32E(E = embedded)와 RV64E는 각각 RV32I·RV64I에서 정수 레지스터를 x0–x15의 16개로 줄인 것 말고는 바뀐 것이 없다. 명령어 인코딩도 같고, rd·rs1·rs2 필드도 여전히 5비트다. 다만 x16–x31을 가리키는 인코딩은 모두 reserved가 된다. v20260120 규격에서 RV32E와 RV64E는 버전 2.0으로 비준된 베이스 ISA다.

    RV32I · 레지스터 32개 RV32E · 레지스터 16개 x0 zero x1 ra x2 sp x3 gp x4 tpx5 t0x6 t1x7 t2 x8 s0x9 s1x10 a0x11 a1 x12 a2x13 a3x14 a4x15 a5 x16 a6x17 a7x18 s2x19 s3 x20 s4x21 s5x22 s6x23 s7 x24 s8x25 s9x26 s10x27 s11 x28 t3x29 t4x30 t5x31 t6 x0 zero x1 ra x2 sp x3 gp x4 tpx5 t0x6 t1x7 t2 x8 s0x9 s1x10 a0x11 a1 x12 a2x13 a3x14 a4x15 a5 x16–x31 없음 이 레지스터를 가리키는 인코딩은 reserved RV32I 코어 나머지 코어 (메모리 제외) x16–x31 ≈ 1/4 RV32E 코어 나머지 코어 (메모리 제외) 약 25% 절약 막대 비율은 규격 비고의 “약 4분의 1”을 그대로 옮긴 개념도다.
    그림 6-6. RV32I와 RV32E의 레지스터 파일. RV32E에서는 아래 16개(빨강)가 사라진다. 규격 비고에 따르면 작은 RV32I 코어에서 위쪽 16개 레지스터가 메모리를 뺀 코어 면적의 약 4분의 1을 차지하므로, 이를 없애면 코어 면적이 약 25% 줄고 전력도 그만큼 준다.
    규격 원문에서
    “We have found that in the small RV32I core implementations, the upper 16 registers consume around one quarter of the total area of the core excluding memories, thus their removal saves around 25% core area with a corresponding core power reduction.”

    Volume I, RV32E and RV64E · Programmers’ Model — RV64E는 큰 SoC 안의 마이크로컨트롤러나, 문맥 상태를 줄이고 싶은 다중 스레드 64비트 프로세서를 위해서도 관심을 받는다고 덧붙인다.

    ILP32E 호출 규약

    레지스터가 절반이니 함수 호출 약속도 달라진다. psABI의 ILP32E 규약은 일반 정수 호출 규약과 같되 다음이 다르다(7장).

    항목ILP32 (RV32I)ILP32E (RV32E)
    인자 레지스터a0–a7 (8개)a0–a5 (6개)
    피호출자 저장 레지스터s0–s11 (12개)s0–s1 (2개)
    임시 레지스터t0–t6 (7개)t0–t2 (3개)
    스택 포인터 정렬16바이트4바이트(32비트 경계)면 충분
    D 확장과 함께가능 (ILP32D)쓸 수 없다
    시스템 호출 번호 레지스터 a7이 없다

    리눅스나 이 책의 에뮬레이터는 a7(x17)에 시스템 호출 번호를 넣는다. RV32E에는 x17이 없으므로 그런 약속은 그대로 쓸 수 없고, RV32E용 실행 환경은 x0–x15 안의 다른 레지스터를 쓰는 별도 약속을 정해야 한다. 또 psABI v1.0 문서는 ILP32E가 GCC의 현재 구현을 기록한 것이며 바뀔 수 있다고 적어 두었다.

    EXPLORER

    RV32E 호환성 검사기

    어셈블리 (riscv.js로 어셈블한 뒤 레지스터 필드를 검사한다)

    정수 레지스터 파일

    코어 면적 (메모리 제외, 규격 비고의 어림값)

    나머지 코어 ≈ 75%x16–x31 ≈ 25%

    호출 규약

    베이스 ISA
    예제
    검사
    명령 수—
    x16–x31 사용—
    판정—
    “x16 이상 섞임” 예제에서 빨갛게 표시된 명령을 RV32E용으로 고쳐 보자. a6은 x16, s2는 x18, t3은 x28이다. 기계어의 rd·rs1·rs2 필드가 16 이상이면 그 인코딩은 RV32E에서 reserved다. 위쪽 단추로 RV32I로 바꾸면 같은 코드가 모두 통과한다.

    RV128I는 어디로 갔나

    RISC-V는 처음부터 주소 공간이 64비트로도 모자랄 날을 대비했다. 2014년의 사용자 ISA 2.0 문서에 128비트 주소 공간 변형 RV128이 처음 서술되었고, 2017년 2.2판과 2019년 비준판(20191213)에도 RV128I 1.7 (Draft) 장이 있었다. 레지스터를 128비트로 넓히고, *W처럼 64비트 값을 다루는 *D 명령을 두는 식의 스케치였다. 하지만 이 장은 끝내 동결·비준되지 않았다.

    2014사용자 ISA 2.0RV128 첫 서술 2017 · v2.2RV128I 1.7동결 안 됨 2019 · 20191213베이스 비준판RV128I 1.7 Draft 20240411비준 안 된 내용 정리RV128I 스케치 삭제 v20260120베이스 ISA 4개RV32I·RV64I·RV32E·RV64E 지금 규격에서 RV128I는 서론의 “언젠가 필요할 수 있다”는 한 문장으로만 남아 있다
    그림 6-7. RV128I의 행적. 문서 버전 20240411의 머리말은 바뀐 점으로 “비준되지 않은 내용의 삭제, RV128I 베이스 ISA 스케치 포함”을 든다. 그래서 v20260120 비특권 규격의 목차에는 RV32I(1장)·RV32E/RV64E(2장)·RV64I(3장)만 베이스 ISA로 남아 있다.

    그렇다고 128비트의 문이 닫힌 것은 아니다. v20260120의 서론은 지금도 “RISC-V에는 현재 네 개의 베이스 ISA가 있다”고 말하면서, 다음과 같이 여지를 남긴다.

    규격 원문에서
    “A larger flat 128-bit address space might eventually be required and could be accommodated with a new RV128I base ISA within the existing RISC-V ISA framework.”

    Volume I, Introduction · RISC-V ISA Overview

    흔적은 다른 곳에도 있다. 특권 규격의 misa.MXL 필드는 1이면 XLEN=32, 2이면 64를 뜻하고, 예전에 128을 뜻하던 3은 지금 Reserved로 비어 있다. 또 예전 판 opcode 지도에서 custom-2·custom-3 칸에는 “rv128”이라는 꼬리표가 붙어 있었지만(RV128이 쓸 수도 있다는 뜻), 지금 지도(그림 6-2)에는 없다.

    RV64 하트에서 RV32 코드 돌리기

    앞에서 본 대로 RV32I는 RV64I의 부분집합이 아니다. add 하나도 두 베이스에서 결과의 폭이 다르다. 그래서 RV64 하트가 RV32 프로그램을 돌리려면 하드웨어가 “이 모드에서는 XLEN이 32다”라고 알아야 한다. 특권 규격은 이것을 mstatus CSR의 두 필드로 제공한다(17장).

    mstatus (RV64) 위쪽 일부 6335343332310 SD 62:36 (MPV·GVA·MBE·SBE 등) SXL[1:0] UXL[1:0] 31:0 (MIE·SIE·MPP·…) M 모드MXLEN = 64 S 모드 (커널)SXL=2 → 64 U 모드 (옛 앱)UXL=1 → 32 값 → XLEN 1 → 32 2 → 64 3 → 예약 UXL·SXL은 misa.MXL과 같은 인코딩을 쓰는 WARL 필드다. 구현은 이 필드를 읽기 전용으로 만들어 “항상 64”로 고정해도 된다 — 즉 RV32 사용자 프로그램 지원은 선택 사항이다. MXLEN=32인 하트에는 이 필드가 없다(SXLEN = UXLEN = 32).
    그림 6-8. RV64 하트의 모드별 XLEN. mstatus의 SXL(bit 35:34)과 UXL(bit 33:32)이 S 모드와 U 모드의 XLEN을 정한다. 예를 들어 64비트 커널(SXL=2) 위에서 옛 32비트 앱을 돌리려면 UXL=1로 둔다. 다만 규격은 UXLEN이 SXLEN보다 커지는 값은 허용하지 않는다.
    상황규격의 규칙 (특권 규격 · Base ISA Control in mstatus)
    MXLEN = 32SXL·UXL 필드가 없다. SXLEN = UXLEN = 32.
    MXLEN = 64, S 모드 없음SXL은 읽기 전용 0.
    MXLEN = 64, S 모드 있음SXL은 SXLEN을 담는 WARL 필드. SXLEN = MXLEN으로 고정해도 된다.
    MXLEN = 64, U 모드 있음UXL은 UXLEN을 담는 WARL 필드. UXLEN = MXLEN 또는 SXLEN으로 고정해도 되고, UXLEN > SXLEN 이 되는 값은 쓸 수 없다.
    왜 ADD가 아니라 ADDW인가

    규격 서론에는 “RV32I의 ADD와 RV64I의 ADDW를 같은 인코딩으로 했으면 어땠을까”라는 질문도 나온다. 그렇게 하면 32비트 덧셈이 두 베이스에서 같은 기계어가 되어 더 일관되었을 것이다. 초기 RISC-V에는 실제로 그런 설계가 있었지만 2011년 1월에 지금 방식으로 바뀌었다. 규격은 돌이켜 보면 근거가 충분하지 않았을 수 있다고 인정하면서도, 위에서 본 이유(어차피 모드 전환이 필요하고, 호출 규약과 시스템 호출이 달라 32비트 라이브러리를 64비트 코드에 섞어 쓰기는 비현실적)로 실질적인 손해는 적다고 정리한다.

    핵심 정리

    1. RV64I는 x1–x31, pc, 사용자 주소 공간을 64비트로 넓힌다. 명령어 길이와 형식은 그대로이며, add 같은 기존 명령은 64비트 연산이 된다.
    2. 새 명령은 12개다: ld·sd·lwu와 *W 명령 9개(addiw slliw srliw sraiw addw subw sllw srlw sraw). *W는 입력의 위쪽 32비트를 무시하고 32비트 결과를 부호 확장한다.
    3. 32비트 값은 부호 없는 값까지 부호 확장된 꼴로 보관한다. 그래서 부호 변환이 공짜이고, 64비트 비교·분기·논리 연산이 32비트 값에도 그대로 맞는다.
    4. 즉시값 시프트의 shamt는 6비트가 되어 inst[25]가 shamt[5]로 쓰인다. slliw·srliw·sraiw에서 imm[5] ≠ 0은 reserved다.
    5. lui·auipc는 32비트 결과를 부호 확장한다. 64비트 상수 li는 재귀적 전개로 최대 8개 명령이 된다.
    6. RV32E/RV64E는 레지스터를 16개로 줄인 비준 베이스 ISA(2.0)다. 작은 코어에서 면적을 약 25% 줄이며, x16–x31 인코딩은 reserved다.
    7. RV128I 스케치는 20240411판에서 삭제되었고, v20260120에는 서론의 한 문장과 misa.MXL의 예약값 3만 남아 있다. RV64 하트는 mstatus.UXL/SXL로 낮은 모드의 XLEN을 32로 바꿀 수 있다(선택 사항).

    확인 퀴즈

    RV64I에서 a1 = 0x00000000_7FFFFFFF, a2 = 1일 때 addw a0, a1, a2의 결과는?

    32비트 결과 0x80000000의 bit 31이 1이므로 위쪽 32비트가 모두 1로 부호 확장된다. RISC-V는 오버플로로 예외를 내지 않는다. 같은 입력의 add는 0x00000000_80000000이므로 두 결과를 비교하면 32비트 오버플로를 알 수 있다.

    RV64에서 32비트 부호 없는 값도 부호 확장해 보관하는 가장 큰 이점은?

    부호 확장은 부호 없는 32비트 값의 크기 순서를 64비트에서도 보존한다(그림 6-3). 그래서 *W 판이 필요한 것은 덧셈·뺄셈·시프트뿐이다.

    RV64I의 slli에서 inst[25]의 역할은?

    RV64I는 shamt가 6비트라 funct7의 맨 아래 비트를 빌려 쓴다. 산술 시프트 선택은 bit 30이다.

    RV64I에서 lui a0, 0x80000을 실행한 뒤 a0의 값은?

    lui는 32비트 결과 0x80000000을 64비트로 부호 확장한다. 그래서 양수 0x80000000을 만들려면 다른 방법(예: addi 1 + slli 31)이 필요하다.

    RV32E 하트에서 add x16, x1, x2의 인코딩은 규격상 어떤 상태인가?

    RV32E/RV64E는 x16–x31을 가리키는 모든 인코딩을 reserved로 둔다. 예전 초안은 이를 custom으로 열어 두었지만, 지금 판은 나중에 custom이나 새 표준에 배정할 수 있도록 더 보수적으로 reserved로 정했다.

    v20260120 규격에서 RV128I의 상태로 옳은 것은?

    20240411판에서 비준되지 않은 RV128I 스케치가 삭제되었다. 지금은 “새 RV128I 베이스 ISA로 수용할 수 있을 것”이라는 서론의 문장만 남아 있고, misa.MXL의 값 3도 Reserved다.

    64비트 리눅스 커널(S 모드) 위에서 32비트 RISC-V 사용자 프로그램을 돌리려면 무엇을 바꿔야 하나?

    UXL은 WARL 필드라서 구현이 64로 고정해 둘 수도 있다. 그 경우 RV32 사용자 프로그램은 돌릴 수 없다. misa.MXL은 읽기 전용이다.

    이 장의 재귀적 li 전개에서 임의의 64비트 상수에 필요한 명령 수의 최댓값은?

    lui+addiw 두 개에서 시작해 재귀할 때마다 slli+addi가 붙는다. 0x123456789abcdef0 같은 값은 8개 명령이 된다.

    더 읽을거리