Chapter 14

B 확장과 스칼라 암호

기본 정수 ISA만으로도 모든 계산을 할 수 있다. 하지만 “비트를 한 칸 돌리기”, “1인 비트 세기”, “바이트 순서 뒤집기”처럼 흔한 일에 명령이 네다섯 개씩 들고, 앞쪽 0의 개수 세기는 루프까지 돌아야 한다. B 확장은 이런 자잘한 비트 작업을 명령 하나로 줄인다. 그 위에 스칼라 암호 확장(Zk*)은 AES·SHA-2·SM4·SM3의 핵심 단계를 하드웨어로 옮기고, 실행 시간이 비밀 데이터에 따라 달라지지 않는다는 약속까지 규격에 담았다. 이 장에서는 비트를 직접 눌러 보며 이 명령들을 하나씩 익힌다.

왜 비트 조작 명령이 따로 필요한가

RV32I 에는 AND·OR·XOR 과 세 가지 시프트가 있다. 이것만 있으면 원리상 어떤 비트 연산이든 만들 수 있다. 문제는 비용이다. 32비트 값을 오른쪽으로 회전하려면 “반대 방향 시프트 양 구하기 → 왼쪽 시프트 → 오른쪽 시프트 → OR” 네 명령이 필요하고, 맨 위에서부터 0이 몇 개인지 세려면 루프를 돌거나 수십 개의 명령을 써야 한다. 컴파일러, 표준 라이브러리, 압축·암호·해시 코드, 운영체제의 비트맵 할당기에서 이런 패턴은 끝없이 반복된다.

하고 싶은 일기본 정수 ISA로는비트 조작 확장으로는
a AND (NOT b)not + and (2개)andn (1개, Zbb)
32비트 오른쪽 회전neg + sll + srl + or (4개)ror (1개, Zbb)
16비트 값 0 확장slli + srli (2개)zext.h (1개, Zbb)
앞쪽 0 개수 세기루프 또는 이분 탐색 (10개 이상)clz (1개, Zbb)
1인 비트 개수비트 트릭 10여 개 + 상수 적재cpop (1개, Zbb)
배열 주소 base + i×4slli + add (2개)sh2add (1개, Zba)
k번째 비트 세우기li + sll + or (3개)bset (1개, Zbs)
GF(2) 다항식 곱셈비트마다 시프트·XOR 루프 (100개 안팎)clmul + clmulh (Zbc)

그래서 RISC-V 는 비트 조작 명령을 용도별로 작은 확장 여러 개로 나눴다. 하나의 거대한 “B”를 만들려던 초안은 여러 해 논의 끝에 2021년, 꼭 필요한 것만 추린 네 확장(Zba·Zbb·Zbc·Zbs)으로 비준되었다. 같은 시기에 암호용 확장(Zbkb·Zbkc·Zbkx·Zk*)도 비준되었는데, 일부 명령은 두 쪽에 동시에 들어 있다. 명령의 인코딩은 어느 확장에서 오든 하나뿐이어서, 두 확장을 함께 구현해도 하드웨어가 겹치지 않는다.

B 확장 v1.0.0 = Zba + Zbb + Zbs Zba 주소 계산 sh1add · sh2add sh3add · add.uw slli.uw (RV64) Zbb 기본 비트 조작 clz · ctz · cpop rol · ror · rev8 orc.b · min · max Zbs 단일 비트 bset · bclr binv · bext + 즉시값 형식 Zbc 캐리 없는 곱셈 clmul · clmulh clmulr B에 포함되지 않음 이름 규칙 Zb* · 비트 조작 Zk* · 암호 Zbk* · 암호용 비트 점선: 같은 명령을 공유한다 스칼라 암호 Zk* v1.0.1 Zbkb pack · zip brev8 · ror Zbkc clmul clmulh Zbkx xperm4 xperm8 Zkne · Zknd AES 암호화 AES 복호화 Zknh SHA-256 SHA-512 Zksed · Zksh SM4 블록 암호 SM3 해시 Zkr · Zkt seed CSR 상수 시간
그림 14-1. 비트 조작과 스칼라 암호 확장의 지도. B 는 Zba·Zbb·Zbs 세 확장의 묶음 이름이고, Zbc 는 따로 골라 넣는다. 암호용 Zbkb 는 Zbb 의 일부(회전, andn/orn/xnor, rev8)에 pack·brev8·zip 등을 더한 것이고, Zbkc 는 Zbc 에서 clmulr을 뺀 것이다.
규격 원문에서
“The B standard extension comprises instructions provided by the Zba, Zbb, and Zbs extensions.” … “The instructions have mnemonics and encodings that are independent of the extensions in which they appear. Thus, when implementing extensions with overlapping instructions, there is no redundancy in logic or encoding.”

Volume I, Bit Manipulation Extensions — 확장 이름은 “묶음표”일 뿐이고, 명령 하나하나의 인코딩은 유일하다.

확장버전내용명령 수 (RV32 / RV64)
Zba1.0.0시프트-덧셈 주소 계산, .uw 형3 / 8
Zbb1.0.0부정 논리, 비트 세기, 최소·최대, 부호/0 확장, 회전, orc.b, rev818 / 24
Zbs1.0.0단일 비트 세우기·지우기·뒤집기·꺼내기8 / 8
Zbc1.0.0캐리 없는 곱셈 (아래·위·뒤집힌 절반)3 / 3
B1.0.0Zba + Zbb + Zbs 의 별칭—
Zbkb · Zbkc · Zbkx1.0.0암호용 비트 조작·캐리 없는 곱셈·크로스바 치환—
접미사 읽는 법: w, .uw, .b/.h

RV64 에서 점 없는 w 접미사(clzw, rolw …)는 기본 ISA 의 addw 와 같은 규칙을 따른다. 입력의 아래 32비트만 보고, 32비트 결과를 64비트로 부호 확장한다. .uw(unsigned word)는 한 피연산자의 아래 32비트를 0 확장해서 쓰고 나머지는 XLEN 전체로 계산한다. .b·.h·.w 는 아래 8·16·32비트만 보고 XLEN 폭 결과를 만든다는 뜻이다. 이 장의 시뮬레이터는 모두 RV32 기준으로 계산한다.

프로파일에서의 위치

RVA23U64 프로파일은 B 와 Zkt 를 필수로 요구한다(23장). 반면 스칼라 Zbc 는 “확장 옵션”으로만 남았고, 스칼라 암호(Zkn 등)는 RVA23U64 의 옵션 목록에서 빠졌다. 응용 프로세서에서는 더 빠른 벡터 암호(Zvk*, 15장)를 표준으로 삼겠다는 뜻이다. 스칼라 암호는 벡터 유닛이 없는 마이크로컨트롤러·보안 칩에서 주로 쓰인다.

Zba: 배열 주소를 한 번에

C 의 a[i]는 기계어로 “시작 주소 + i × 원소 크기”다. 원소 크기는 대개 2·4·8바이트이니 곱셈 대신 왼쪽 시프트를 쓴다. 기본 ISA 로는 slli로 i 를 밀고 add로 시작 주소에 더하는 두 명령이 든다. Zba 의 sh1add·sh2add·sh3add는 이 둘을 하나로 합친다.

sh2add rd, rs1, rs2 # X(rd) = X(rs2) + (X(rs1) << 2) # 쓰는 법: rs1 = 인덱스, rs2 = 배열 시작 주소 sh2add t0, a1, a0 # t0 = &a[i] (a0 = a, a1 = i, int 배열) lw t1, 0(t0) # t1 = a[i]
a1 · 인덱스 i 0x00000003 a0 · 배열 시작 주소 0x00001000 ≪ 2 (×4) 배선만 옮김 + t0 · 원소 주소 0x0000100c 메모리 (int a[8]) 0x1000a[0] 0x1004a[1] 0x1008a[2] 0x100ca[3] 0x1010a[4] 0x1014a[5] 0x1018a[6] 0x101ca[7] 기본 ISA: slli t0, a1, 2 → add t0, a0, t0 → lw (명령 3개) Zba: sh2add t0, a1, a0 → lw (명령 2개)
그림 14-2. sh2add의 데이터 흐름. 2비트 왼쪽 시프트는 덧셈기 입력의 배선을 두 칸 옮기는 것일 뿐이라 논리가 거의 늘지 않는다. 규격이 시프트 양을 1·2·3으로만 한정한 까닭도 “덧셈기의 임계 경로를 늘리지 않기 위해서”다.

세 명령은 인코딩도 가지런하다. 모두 R 형식, opcode 는 OP(0110011), funct7 은 0010000이고, funct3 만 다르다. funct3 의 위 두 비트를 읽으면 그대로 시프트 양이 된다.

3125 2420 1915 1412 117 60 funct7 rs2 (시작 주소) rs1 (인덱스) funct3 rd opcode sh1add 0010000 rs2 rs1 010 rd 0110011 sh2add 0010000 rs2 rs1 100 rd 0110011 sh3add 0010000 rs2 rs1 110 rd 0110011 sh1add.uw 0010000 rs2 rs1 010 rd 0111011 funct3 의 위 두 비트(01 · 10 · 11) = 시프트 양 1 · 2 · 3. .uw 형은 같은 funct 값에 opcode 만 OP-32(0111011)로 바뀐다.
그림 14-3. Zba 시프트-덧셈 명령의 인코딩(규격 인코딩 그림에서 옮김). sh2add.uw·sh3add.uw 도 같은 방식으로 OP-32 opcode 를 쓴다. OP-32 는 RV64 에만 있는 opcode 이므로 .uw 형은 RV64 전용이다.

.uw 형: RV64 에서 unsigned int 인덱스

RV64 에는 한 가지 함정이 더 있다. psABI 는 32비트 정수를 64비트 레지스터에 담을 때 부호 없는 타입이라도 부호 확장해 둔다고 정한다. 그래서 unsigned int i = 0x80000000 은 레지스터에 0xffffffff80000000으로 들어 있다. 이 값으로 주소를 계산하려면 먼저 위 32비트를 0으로 지워야 하는데, 기본 ISA 로는 slli 32 + srli 두 명령이 더 든다. add.uw·sh1add.uw~sh3add.uw·slli.uw는 “rs1 의 아래 32비트를 0 확장한 뒤” 계산하므로 이 정리 작업까지 한 명령에 흡수한다. zext.w rd, rs는 add.uw rd, rs, zero의 의사 명령이다.

SIMULATOR

Zba 배열 인덱싱: 명령 수 비교

기본 ISA

Zba 사용

배열 a 의 원소 주소 (a0 = 0x10000)
기본 ISA
Zba
XLEN
원소 크기
인덱스 타입
주소 계산 (기본)2
주소 계산 (Zba)1
아낀 명령 수—
&a[i]—
RV64 에서 unsigned int 인덱스를 고르면 기본 ISA 쪽은 0 확장 때문에 명령이 하나 더 늘지만 Zba 쪽은 그대로 한 개다. 원소 크기를 16 B로 바꾸면 시프트 양 4는 sh*add 범위 밖이라 이득이 사라진다(단, unsigned 인덱스라면 slli.uw가 여전히 한 개를 아낀다).

Zbb: 기본 비트 조작

Zbb 는 B 확장의 중심이다. 일곱 무리로 나뉘는데, 대부분이 “기본 ISA 로 2~4개, 많게는 수십 개 걸리던 관용구”를 한 명령으로 줄인 것이다.

명령뜻 (RV32 기준)대표 쓰임
andn · orn · xnorrs1 & ~rs2 · rs1 | ~rs2 · ~(rs1 ^ rs2)마스크로 비트 지우기, 해시·암호의 논리 단계
clz · ctz위·아래쪽부터 연속된 0의 개수 (입력이 0이면 XLEN)log₂, 정규화, 비트맵에서 첫 빈칸 찾기
cpop1인 비트의 개수 (population count)집합 크기, 해밍 거리, 체스 비트보드
min · minu · max · maxu부호 있는/없는 비교로 작은·큰 값분기 없는 범위 제한(clamp)
sext.b · sext.h · zext.h아래 8·16비트 부호 확장, 16비트 0 확장char·short 변환
rol · ror · rori왼쪽·오른쪽 회전 (rol 의 즉시값형은 없다)해시, 암호, 난수 생성기
orc.b바이트마다 하나라도 1이면 0xff, 아니면 0x00문자열의 NUL 바이트 찾기
rev8레지스터 전체의 바이트 순서 뒤집기엔디언 변환, 네트워크 바이트 순서
왜 rol 의 즉시값 형식은 없을까

왼쪽으로 k 칸 회전하는 것은 오른쪽으로 XLEN−k 칸 회전하는 것과 같다. 시프트 양이 상수라면 어셈블러가 rori rd, rs, XLEN−k로 바꿔 쓰면 되므로 인코딩 공간을 아끼려고 roli를 두지 않았다.

부정 논리: 반전기를 다시 쓴다

andn·orn·xnor는 하드웨어 입장에서 거의 공짜다. 뺄셈 sub는 이미 rs2 를 반전해 더하는 회로를 갖고 있으므로, 그 반전기를 AND·OR·XOR 입력 앞에도 놓으면 된다. 인코딩도 sub와 같은 funct7(0100000)을 쓰고, funct3 은 각각 and(111)·or(110)·xor(100)와 같다.

rs1 rs2 ~rs2 sub 와 공유 MUX반전? AND OR XOR 선택 rd and · or · xor andn · orn · xnor
그림 14-4. 부정 논리 명령의 구현 힌트(규격의 Implementation Hint). rs2 경로에 이미 있는 반전기를 AND·OR·XOR 앞에서도 고르게 하면 여섯 명령이 같은 회로를 나눠 쓴다. 파란 점선이 반전되었을 수도 있는 두 번째 피연산자다.

회전: 밀려난 비트가 돌아온다

시프트는 밀려난 비트를 버리고 빈자리를 0(또는 부호 비트)으로 채운다. 회전은 밀려난 비트를 반대쪽 끝으로 다시 넣는다. 정보가 하나도 사라지지 않으므로 SHA-256, ChaCha20, SM3 같은 “더하기·회전·XOR(ARX)” 계열 알고리즘의 핵심 연산이다. 기본 ISA 로는 neg + sll + srl + or 네 명령이 들던 것을 규격은 바로 이 이유로 넣었다고 적고 있다.

76543210 76543210 srl x, 1 시프트 0 1 0 1 1 0 0 1 1 0xB3→0x59 밀려난 비트 0 은 버려지고, 빈자리(비트 7)에는 0 이 들어온다 ror x, 1 회전 1 0 1 1 0 0 1 1 0xB3→0xD9 밀려난 비트 0 이 아래로 돌아 비트 7 자리로 들어간다. 사라지는 정보가 없다.
그림 14-5. 8비트 값 0xB3을 한 칸 오른쪽으로 시프트할 때(위)와 회전할 때(아래). 금색 상자가 비트 0 이다. 실제 ror은 XLEN 폭 전체에서 같은 일을 하고, 회전 양은 rs2 의 아래 log₂(XLEN) 비트만 쓴다.

rev8 과 orc.b: 바이트 단위로 보기

rev8은 레지스터의 바이트 순서를 통째로 뒤집는다. 리틀 엔디언 RISC-V 에서 빅 엔디언 데이터(네트워크 패킷 헤더, 암호 표준의 입력 블록)를 다룰 때 바로 쓰인다. 반쪽(16비트)이나 워드(RV64 의 32비트)만 뒤집고 싶으면 rev8 뒤에 srai(또는 srli)로 내려 주면 된다고 규격의 Software Hint 가 알려 준다. 인코딩은 RV32 와 RV64 에서 서로 다르다(즉시값 필드 0x698과 0x6b8).

rs = 0x12345678 바이트 3 (31:24)바이트 2 (23:16)바이트 1 (15:8)바이트 0 (7:0) 0x12 0x34 0x56 0x78 0x12 0x34 0x56 0x78 바이트 3바이트 2바이트 1바이트 0 rd = rev8(rs) = 0x78563412
그림 14-6. rev8(RV32)의 동작. 바이트 k 가 바이트 3−k 자리로 옮겨 간다. 비트가 아니라 바이트 단위라서 각 바이트 안의 비트 순서는 그대로다. 바이트 안의 비트 순서까지 뒤집는 brev8은 Zbkb 에 있다.

orc.b(OR-Combine, byte granule)는 각 바이트 안의 8비트를 OR 해서, 그 결과를 바이트 전체에 퍼뜨린다. 0인 바이트는 0x00, 0이 아닌 바이트는 0xff 가 된다. 별것 아닌 것 같지만 C 문자열의 끝(NUL)을 찾는 데 꼭 맞는 연산이다. 뒤의 strlen 절에서 자세히 본다.

clz · ctz · cpop 과 min · max

세는 명령은 입력이 0일 때의 결과가 분명히 정해져 있다. clz(0)과 ctz(0)은 XLEN 이고, 최상위 비트가 1이면 clz는 0이다. 그래서 31 − clz(x)는 0이 아닌 x 의 ⌊log₂ x⌋ 이고, ctz(x)는 비트맵에서 가장 낮은 번호의 1(예: 비어 있는 페이지 번호)을 바로 알려 준다. min·max는 분기 예측이 어려운 데이터 의존 분기를 없앤다. 예를 들어 x를 [lo, hi] 범위로 자르는 일은 max t0, a0, a1 + min a0, t0, a2 두 명령으로 끝난다.

Zbs: 비트 하나만 콕 집어

장치 레지스터의 플래그, 운영체제의 비트맵, 집합을 비트로 표현한 자료구조는 “i번째 비트 하나”를 다루는 일이 잦다. 기본 ISA 로는 li t0, 1 → sll t0, t0, a1로 마스크를 만든 뒤 OR·AND·XOR 를 해야 한다. Zbs 는 마스크 만들기와 연산을 한 명령에 묶는다.

76543210 rs1= 0xB6 마스크= 1 ≪ (rs2 & 31) 10110110 00100000 bset bclr binv bext 10110110 10010110 10010110 00000001 rs2 = 5 (또는 즉시값 5) rs1 | 마스크 = 0xB6 (이미 1) rs1 & ~마스크 = 0x96 rs1 ^ 마스크 = 0x96 (rs1 ≫ 5) & 1 = 1
그림 14-7. 같은 값 0xB6의 비트 5 에 Zbs 네 명령을 적용한 결과(아래 8비트만 그림). 비트 위치는 rs2 의 아래 log₂(XLEN) 비트만 쓰므로 RV32 에서 rs2 = 37 도 비트 5 를 뜻한다. bext만 결과를 비트 0 으로 내려 0 또는 1 을 만든다.

네 명령 모두 즉시값 형식(bseti·bclri·binvi·bexti)이 있다. 시프트 즉시값 명령처럼 shamt 필드에 비트 번호를 담으며, RV32 에서 shamt[5] = 1 인 인코딩은 예약되어 있다. 특히 bseti/bclri는 andi/ori의 12비트 즉시값이 닿지 않는 비트 11 이상을 한 명령으로 다룰 수 있다는 점이 쓸모 있다.

li t0, 1 # 기본 ISA: 3개 (마스크를 만들 즉시값이 12비트 범위를 벗어난다) slli t0, t0, 20 or a0, a0, t0 bseti a0, a0, 20 # Zbs: 1개

비트 놀이터: 직접 눌러 보기

지금까지 본 명령을 직접 계산해 보자. 아래 rs1(A)·rs2(B)의 비트를 눌러 바꾸거나 16진수를 입력하고, 명령을 고르면 rd 가 계산된다. 결과 비트 중 A 와 달라진 비트는 금색 테두리로 표시된다. 함께 나오는 기계어는 op a0, a1, a2(단항 명령은 op a0, a1)를 RV32 로 인코딩한 값이다.

SIMULATOR

비트 놀이터 (RV32)

rs1 = A
rs2 = B
—
rd
값 바꾸기
rd (16진수)—
rd (부호 있음)—
확장—
기계어—
A = 0x12345678, B = 8 에서 ror과 rol을 비교해 보자. rd → A를 눌러 rev8을 두 번 적용하면 원래 값으로 돌아온다. B 를 37 로 두고 bset을 하면 비트 5 가 켜진다(아래 5비트만 쓰기 때문). clmul에서 A = B = 0xffffffff이면 왜 0x55555555가 나올지 생각해 보자.

orc.b 로 strlen 빠르게

C 문자열의 길이를 구하는 가장 단순한 방법은 바이트를 하나씩 읽어 0인지 보는 것이다. 문자 하나마다 lbu · addi · bnez 세 명령이 든다. 레지스터에 4바이트(RV64 는 8바이트)를 한꺼번에 읽어 “이 안에 0인 바이트가 있나?”를 한 번에 물을 수 있다면 훨씬 빠를 것이다. orc.b가 바로 그 질문에 답한다. 0이 없는 덩어리는 orc.b 결과가 모두 1(= −1)이므로 beq 한 번으로 판정이 끝난다.

메모리 0x1000 → 0x1003 'a' 61 'b' 62 'c' 63 NUL 00 리틀 엔디언: 첫 바이트가 오른쪽 끝 lw a2, 0(a1) a2 = 0x00636261 00 63 62 61 orc.b a2, a2 a2 = 0x00ffffff ≠ −1 → 루프 탈출 00 ff ff ff not a2, a2 a2 = 0xff000000 ff 00 00 00 ctz a2, a2 → 24 (오른쪽부터 0이 24개), srli a0, a2, 3 → 3 = strlen("abc")
그림 14-8. orc.b로 NUL 바이트를 찾는 원리(RV32, 리틀 엔디언). orc.b는 NUL 이 아닌 바이트를 0xff 로, NUL 바이트를 0x00 으로 만든다. not으로 뒤집으면 NUL 자리만 1이 되고, ctz가 오른쪽(낮은 주소)부터 0을 세어 첫 NUL 의 비트 위치를 준다. 8로 나누면(srli 3) 바이트 위치다.

규격 부록의 strlen 예제는 이 원리 위에 시작 주소가 정렬되지 않은 경우의 처리(첫 덩어리를 시프트해 앞부분을 버림)를 더한 것이다. 핵심 루프는 단 네 명령이다.

.Lloop: REG_L a2, SZREG(a1) # 다음 덩어리(4 또는 8바이트) 읽기 addi a1, a1, SZREG orc.b a2, a2 # NUL 아닌 바이트 → 0xff, NUL → 0x00 beq a2, a4, .Lloop # a4 = -1: 모두 0xff 면 NUL 없음 → 계속 .Lepilogue: not a2, a2 # NUL 자리만 0xff ctz a2, a2 # 리틀 엔디언: 낮은 주소 = 낮은 비트 ... srli a2, a2, 3 # 비트 위치 → 바이트 위치
ANIMATION

orc.b 로 문자열 끝 찾기

한 단계 또는 자동 재생을 눌러 시작하자.
XLEN
실행
읽은 덩어리0
바이트 루프 (3 × 바이트)—
orc.b 루프 (4 × 덩어리)—
strlen 결과?
문자열 시작이 정렬되어 있다고 가정하고 루프 본문 명령만 센다. 문자열 길이를 4의 배수(예: 8자)로 맞추면 NUL 이 새 덩어리의 맨 앞에 오는 경우를 볼 수 있다. NUL 뒤의 바이트(회색 x)는 무엇이든 결과에 영향을 주지 않는다는 점도 확인하자.
strcmp 에서는 rev8 이 짝꿍

규격 부록의 strcmp 예제는 두 문자열을 워드 단위로 비교하다가 다른 워드를 만나면 rev8로 두 워드를 빅 엔디언 순서로 뒤집은 뒤 sltu 한 번으로 크기를 비교한다. 바이트 순서를 뒤집으면 “앞 글자가 더 중요한” 사전식 비교가 정수 비교와 같아지기 때문이다.

Zbc: 캐리 없는 곱셈

보통 곱셈은 부분곱을 더한다. 같은 자리에서 1 + 1 이 나오면 윗자리로 올림(carry)이 생긴다. 캐리 없는 곱셈(carry-less multiplication)은 부분곱을 XOR 한다. 1 ⊕ 1 = 0 이고 올림은 없다. 이것은 계수가 0과 1뿐인 다항식, 곧 GF(2) 위의 다항식 곱셈과 정확히 같다. 비트 k 를 xᵏ 의 계수로 읽으면 된다.

보통 곱셈 11 × 6 캐리 없는 곱셈 clmul(11, 6) A 1011 × B 0110 B₀=0 0000 B₁=1: A≪1 10110 B₂=1: A≪2 101100 B₃=0 0000 합 (+) 1000010 A 1011 × B 0110 B₀=0 0000 B₁=1: A≪1 10110 B₂=1: A≪2 101100 B₃=0 0000 XOR (⊕) 111010 = 1000010₂ = 66 (비트 2 의 1+1 이 올림을 만든다) = 111010₂ = 58 (비트 2 의 1⊕1 = 0, 올림 없음) 다항식으로 읽으면 (x³ + x + 1)(x² + x) = x⁵ + x⁴ + x³ + x — 두 x² 항이 서로 지워진다
그림 14-9. 같은 부분곱을 더하면 보통 곱셈, XOR 하면 캐리 없는 곱셈이 된다. 금색 열(비트 2)에서 두 1이 만난다. 보통 곱셈에서는 올림이 위로 번져 결과 전체가 달라지지만, 캐리 없는 곱셈에서는 그 자리만 0이 되고 끝난다.

XLEN 비트 두 수의 캐리 없는 곱은 최대 2·XLEN − 1 비트다. Zbc 는 이것을 세 가지로 잘라 준다.

명령결과 (2·XLEN 비트 곱 P 에서)쓰임
clmulP[XLEN−1 : 0] — 아래 절반GCM 의 GHASH, CRC
clmulhP[2·XLEN−1 : XLEN] — 위 절반clmul 과 짝을 이뤄 전체 곱
clmulrP[2·XLEN−2 : XLEN−1] — “뒤집힌” 절반비트 순서가 반대인 CRC (Zbkc 에는 없음)

clmulr의 r 은 reversed 다. 규격 비고에 따르면 이 명령은 “입력 둘을 비트 반전한 뒤 clmul을 하고 결과를 다시 비트 반전한 것”과 같다. 이더넷 CRC-32 처럼 비트를 LSB 부터 보내는 프로토콜의 CRC 를 계산할 때 비트 반전 없이 바로 쓸 수 있다.

SIMULATOR

보통 곱셈 vs 캐리 없는 곱셈 (8비트)

A ()
B ()

보통 곱셈: 부분곱을 더한다 (mul)

캐리 없는 곱셈: 부분곱을 XOR 한다 (clmul)

비트를 눌러 A·B 를 정하고 부분곱 하나 더를 눌러 보자.
진행
예제
A × B—
clmul(A, B)—
결과가 다른 비트 수—
0xff × 0xff 예제를 보자. 보통 곱셈은 0xfe01 이지만 캐리 없는 곱은 0x5555 다. 자기 자신과의 캐리 없는 곱(제곱)은 언제나 비트 사이에 0을 끼워 넣은 꼴이 된다. 교차항이 두 번씩 나와 XOR 로 지워지기 때문이다.
CRC 와 AES-GCM 에서 왜 필요한가

CRC 는 메시지를 GF(2) 다항식으로 보고 생성 다항식으로 나눈 나머지다. 바렛 축약(Barrett reduction)을 쓰면 이 나눗셈을 캐리 없는 곱셈 몇 번으로 바꿀 수 있어서, 비트마다 시프트·XOR 하던 루프나 큰 테이블 조회가 필요 없어진다. AES-GCM 의 인증 태그를 만드는 GHASH 는 GF(2¹²⁸) 의 곱셈이며, 128비트 곱셈을 64비트(또는 32비트) clmul/clmulh 조각으로 나눠 계산한다. 테이블 조회를 쓰지 않으니 캐시 타이밍으로 키가 새는 위험도 사라진다. 그래서 암호용 Zbkc 는 GHASH 에 필요한 clmul·clmulh만 담았다.

스칼라 암호 Zk*: AES · SHA-2 · SM4 · SM3

스칼라 암호 확장(v1.0.1)은 정수 레지스터만으로 표준 암호 알고리즘을 빠르고 안전하게 돌리기 위한 명령 모음이다. 벡터 유닛이 없는 작은 코어도 쓸 수 있도록 32비트·64비트 정수 레지스터 위에서 동작한다. 규격은 쓰임새에 따라 확장을 잘게 나눴다. TLS 1.3 의 GCM 모드처럼 복호화가 필요 없는 경우가 있어 AES 암호화(Zkne)와 복호화(Zknd)를 나눴고, 나라마다 요구하는 알고리즘이 달라 NIST 계열과 중국 ShangMi(SM) 계열도 나눴다.

Zk Zks ZknNIST 묶음 Zkr엔트로피 소스 Zkt상수 시간 ZkneAES 암호화 ZkndAES 복호화 ZknhSHA-2 Zbkb비트 조작 Zbkcclmul Zbkxxperm ZksedSM4 ZkshSM3 Zk = Zkn + Zkr + Zkt. Zks 는 Zk 에 들어 있지 않은 별도 묶음이며, Zbk* 세 확장은 Zkn 과 Zks 가 함께 쓴다.
그림 14-10. 스칼라 암호 확장의 묶음 구조. 묶음 이름(Zkn·Zks·Zk)을 지원한다고 밝히는 코어는 아래에 달린 확장을 모두 구현해야 한다.

AES: RV32 와 RV64 의 서로 다른 전략

AES 한 라운드는 SubBytes(S-box 치환) → ShiftRows(행 회전) → MixColumns(열 섞기) → AddRoundKey(키 XOR)로 이뤄진다. 128비트 상태를 레지스터에 어떻게 담느냐에 따라 두 가지 설계가 나왔다.

rs2 (입력 상태의 한 열) b3 b2 b1 b0 ← bs = 2 가 고른 바이트 S-boxSubBytes 0x000000‖S32비트로 넓힘 rol (bs × 8)= 16비트 회전 ⊕ rs1누적값·라운드 키 rd aes32esi: S-box 결과를 그대로 회전해 XOR (마지막 라운드) aes32esmi: S-box 뒤에 MixColumns 의 그 바이트 몫을 더 적용 (중간 라운드) 서로 다른 입력 열에서 바이트를 골라 bs = 0·1·2·3 으로 네 번 누적하면 출력 상태의 한 열(32비트)이 완성된다.
그림 14-11. aes32esi rd, rs1, rs2, bs의 데이터 흐름(규격 Sail 코드를 그림으로 옮김). S-box 는 하드웨어 표 조회이지만, 규격은 이 명령의 실행 시간이 데이터에 따라 달라지면 안 된다고 못 박는다. 소프트웨어 표 조회가 캐시 타이밍으로 키를 흘리던 문제를 없애는 것이 목적이다.
aes64esm t2, t0, t1 # t0·t1 = 현재 상태, t2·t3 = 다음 상태 aes64esm t3, t1, t0 # 두 번째는 레지스터 순서를 뒤집는다 # 이후 라운드 키를 xor 로 더한다

SHA-2: 시그마 함수를 한 명령으로

SHA-256 의 메시지 확장과 압축 단계는 회전 두세 번과 XOR 로 이뤄진 네 함수를 반복해서 부른다. Zbb 의 ror만 있어도 함수 하나에 회전 3 + XOR 2 = 5개쯤 드는 일을, Zknh 는 한 명령으로 줄인다.

명령계산 (32비트)SHA-256 표기
sha256sig0ror(x,7) ⊕ ror(x,18) ⊕ (x ≫ 3)σ₀ (메시지 확장)
sha256sig1ror(x,17) ⊕ ror(x,19) ⊕ (x ≫ 10)σ₁ (메시지 확장)
sha256sum0ror(x,2) ⊕ ror(x,13) ⊕ ror(x,22)Σ₀ (압축)
sha256sum1ror(x,6) ⊕ ror(x,11) ⊕ ror(x,25)Σ₁ (압축)

SHA-512 용으로는 RV64 에 같은 꼴의 64비트 명령 네 개(sha512sig0 등)가, RV32 에는 64비트 값을 두 레지스터로 나눠 계산하는 여섯 개(sha512sig0h/l, sha512sig1h/l, sha512sum0r, sha512sum1r)가 있다.

SM4 · SM3 과 Zbk* 보조 명령

중국 표준 블록 암호 SM4 는 암호화와 복호화가 같은 라운드 함수를 쓰므로 확장도 하나(Zksed: sm4ed, sm4ks)다. 구조는 RV32 AES 명령과 같다. bs로 바이트 하나를 골라 S-box 와 선형 변환을 거친 뒤 회전해 rs1 에 XOR 한다. 해시 SM3 용 Zksh 는 치환 함수 P0(x ⊕ rol(x,9) ⊕ rol(x,17))과 P1(x ⊕ rol(x,15) ⊕ rol(x,23))을 각각 한 명령으로 만든다.

암호용 비트 조작 확장은 이렇게 쓰인다. Zbkb 의 pack·packh는 바이트 문자열을 워드로 모으고, brev8은 GHASH 의 비트 순서를 맞추며, RV32 전용 zip·unzip은 SHA-3 의 64비트 회전을 32비트 레지스터 둘로 흉내 낼 때 쓴다. Zbkx 의 xperm4·xperm8은 레지스터 안에 담은 작은 표에서 4비트·8비트 원소를 골라 오는 “레지스터 속 표 조회”다. 메모리 표 조회 없이 S-box 를 구현할 수 있어 실행 시간이 비밀 값에 따라 달라지지 않는다. 규격은 이 방식으로 구현할 수 있는 8비트 S-box 의 예로 AES, SM4, 일본의 Camellia 와 함께 한국의 ARIA를 든다.

Zkr 엔트로피 소스와 Zkt 상수 시간

암호는 좋은 알고리즘만으로 완성되지 않는다. 예측할 수 없는 난수가 필요하고, 계산하는 동안 시간으로 비밀이 새지 않아야 한다. 스칼라 암호 확장의 마지막 두 조각이 이 둘을 맡는다.

seed CSR: 하드웨어 엔트로피 16비트씩

Zkr 은 비특권 CSR seed를 주소 0x015에 정의한다. 물리적 잡음원에서 모은 엔트로피를 한 번에 16비트씩 내준다. 이 값은 그대로 난수로 쓰기보다, SHA-2 등으로 정리(conditioning)한 뒤 AES 기반 CTR_DRBG 같은 결정적 난수 생성기의 씨앗(seed)으로 쓰라고 규격은 권한다.

3130 2924 2316 150 OPST상태 reserved표준용 예약 custom구현 자유 (실험용) entropyOPST = ES16 일 때만 유효한 16비트 csrrw a0, seed, x0 → a0 = 0x8000ABCD : OPST = 10 (ES16), entropy = 0xABCD 읽기 전용 형식(csrrs/csrrc 의 rs1 = x0 등)으로 접근하면 불법 명령 예외. 쓰는 값은 무시된다.
그림 14-12. seed CSR(0x015)의 비트 배치. 읽으면 엔트로피가 지워지는(wipe-on-read) CSR 이라서 “읽기와 쓰기를 함께 하는” csrrw rd, seed, x0로 읽는 것이 정석이다. OPST 가 ES16 이 아니면 entropy 필드는 0이어야 한다.
reset BISTOPST = 00 WAITOPST = 01 ES16OPST = 10 DEADOPST = 11 치명적이지 않은 경보 (기록만 하고 계속) 치명적 오류 한번 DEAD 면 계속 DEAD 16비트 엔트로피 유효! 엔트로피가 아직 모이지 않음
그림 14-13. seed의 OPST 상태 전이(규격 그림 “Entropy Source state transition diagram”을 옮김). 평소에는 WAIT(아직 없음)와 ES16(16비트 유효)을 오간다(금색 점). BIST 는 리셋 직후의 자체 시험이며, 동작 중에 BIST 로 돌아가면 치명적이지 않은 경보라는 뜻이다. DEAD 는 복구할 수 없는 오류다.
poll: csrrw t0, seed, x0 # 읽으면서 쓴다 → 읽은 엔트로피는 지워진다 srli t1, t0, 30 # t1 = OPST li t2, 2 # ES16 = 0b10 beq t1, t2, got li t2, 3 # DEAD = 0b11 beq t1, t2, fatal # 복구 불가: 보고하고 멈춘다 j poll # BIST·WAIT: 잠시 후 다시 got: zext.h t0, t0 # 아래 16비트가 엔트로피 (zext.h 는 Zbb)

seed는 기본적으로 M 모드에서만 접근할 수 있다. S·U 모드에서 쓰게 하려면 M 모드가 mseccfg CSR 의 SSEED·USEED 필드를 켜 줘야 하고, 그렇지 않으면 불법 명령 예외가 난다. 가상 머신 안의 운영체제에는 하이퍼바이저가 이 접근을 가로채 소프트웨어 난수 생성기(규격의 “가상 엔트로피 소스”, 최소 256비트 보안 수준)로 흉내 낸 값을 줄 수 있다.

Zkt: 실행 시간이 데이터에 따라 달라지지 않는다는 약속

타이밍 공격은 “계산이 얼마나 걸렸는지”만으로 비밀 키를 알아낸다. 곱셈기가 0을 만나면 일찍 끝난다거나, 비밀 값에 따라 분기 방향이나 메모리 주소가 바뀌면 시간 차이가 생긴다. 암호 라이브러리 개발자들은 그동안 “이 명령은 상수 시간일 것”이라고 가정하며 코드를 써 왔다. Zkt 는 그 가정을 규격의 약속으로 바꾼다.

Zkt 목록에 있는 것 (구현했다면 데이터 독립 시간)목록에 없는 것
RVI 의 산술·논리·시프트·slt*, lui·auipc·addi 등load·store, 조건 분기
M 의 곱셈 (mul·mulh·mulhsu·mulhu·mulw)나눗셈·나머지 (div·rem …)
C 의 해당 압축 명령, Zcb 의 c.mul·c.not·c.zext.b부동소수점 F·D·Q
Zbkb·Zbkc·Zbkx 전체, 모든 Zk* 암호 명령Zicsr·Zifencei, HINT 인코딩
규격 원문에서
“The Zkt extension attests that the machine has data-independent execution time for a safe subset of instructions. … Zkt does not define a set of instructions available in the core; it just restricts the behaviour of certain instructions if those are implemented.”

Volume I, Scalar Cryptography — Zkt — Zkt 는 새 명령이 아니라 기존 명령의 “시간적 성질”에 대한 약속이다. 그래서 곱셈기가 없는 단순한 RV32I 코어도 Zkt 를 만족할 수 있다.

Zkt 가 막지 못하는 것

Zkt 는 명령 하나의 실행 시간만 약속한다. 비밀 값으로 분기하거나 비밀 값을 주소로 메모리를 읽는 코드는 여전히 위험하다(그래서 load·store·분기가 목록에서 빠져 있다). 스펙터(Spectre)류의 추측 실행 공격도 Zkt 의 범위 밖이라고 규격이 분명히 적고 있다. 비순차 코어가 명령을 합치거나 쪼개는 최적화는 허용되지만, 그 결정이 피연산자 값에 따라 달라져서는 안 된다.

핵심 정리

  1. B = Zba + Zbb + Zbs(v1.0.0)다. 캐리 없는 곱셈 Zbc 는 B 에 들어가지 않는다. 명령의 인코딩은 어느 확장에서 오든 유일하다.
  2. Zba의 sh1add/sh2add/sh3add는 rd = rs2 + (rs1 ≪ n)로 배열 주소 계산을 한 명령에 끝낸다. RV64 의 .uw 형은 32비트 인덱스의 0 확장까지 흡수한다.
  3. Zbb는 부정 논리, clz·ctz·cpop, min·max, 부호/0 확장, 회전, orc.b, rev8을 담는다. clz·ctz 는 입력이 0이면 XLEN 을 돌려준다.
  4. Zbs의 bset·bclr·binv·bext 는 rs2(또는 즉시값)의 아래 log₂(XLEN) 비트로 비트 하나를 고른다.
  5. orc.b + not + ctz로 여러 바이트 중 첫 NUL 의 위치를 한 번에 찾는다. strlen 의 핵심 루프는 네 명령이다.
  6. clmul/clmulh/clmulr는 부분곱을 XOR 하는 GF(2) 다항식 곱셈이다. CRC 와 AES-GCM 의 GHASH 를 표 없이 빠르게 계산한다.
  7. 스칼라 암호(v1.0.1)는 Zk = Zkn + Zkr + Zkt, Zkn = Zbkb+Zbkc+Zbkx+Zkne+Zknd+Zknh, Zks = Zbkb+Zbkc+Zbkx+Zksed+Zksh 로 묶인다.
  8. seed CSR(0x015)은 OPST(BIST·WAIT·ES16·DEAD)와 16비트 엔트로피를 준다. Zkt 는 목록의 명령이 데이터와 무관한 시간에 실행된다는 약속이다.

확인 퀴즈

“B” 확장을 지원한다고 할 때 반드시 들어 있는 확장의 조합은?

B 는 Zba·Zbb·Zbs 의 묶음 이름이다. Zbc(캐리 없는 곱셈)는 별도로 골라 넣는 확장이다.

sh2add a0, a1, a2를 실행한 뒤 a0 의 값은?

규격의 정의는 X(rd) = X(rs2) + (X(rs1) ≪ 2)다. rs1 에 인덱스, rs2 에 배열 시작 주소를 둔다.

RV32 에서 orc.b를 0x00410042에 적용한 결과는?

바이트별로 0x00 → 0x00, 0x41 → 0xff, 0x00 → 0x00, 0x42 → 0xff 가 된다.

RV64 에서 unsigned int 인덱스로 배열에 접근할 때 sh2add.uw가 기본 ISA 보다 특히 유리한 이유는?

psABI 는 32비트 정수를 부호 없는 타입이라도 부호 확장해 둔다. 기본 ISA 는 slli 32 + srli 로 0 확장을 해야 하지만 .uw 형은 rs1 의 아래 32비트를 0 확장해 쓴다.

clmul로 3(0b11)과 3(0b11)을 곱한 결과는?

부분곱 0b11 과 0b110 을 XOR 하면 0b101 = 5 다. 다항식으로는 (x+1)² = x² + 1 이다.

csrrw a0, seed, x0의 결과가 0x8000ABCD였다. 올바른 해석은?

비트 31:30 = 10 은 ES16 이고, 비트 15:0 이 엔트로피다. 규격이 직접 드는 예다.

Zkt 확장에 대한 설명으로 옳은 것은?

Zkt 는 명령을 추가하지 않고 기존 명령의 시간적 성질만 제한한다. load·store·분기·나눗셈은 목록에서 빠져 있다.

RV64 의 aes64esm t2, t0, t1에 대한 설명으로 옳은 것은?

RV64 AES 명령은 두 레지스터에 상태 전체를 담는다. 나머지 절반은 레지스터 순서를 바꾼 aes64esm t3, t1, t0로 만든다. MixColumns 가 없는 마지막 라운드용은 aes64es다.

더 읽을거리