명령어 하나가 데이터패스를 지나가는 길 - 레지스터 31개와 XZR, 그리고 MUX 네 개

English (N/A)

CPU 구조도에서 명령 하나가 지나가는 길을 뼈대만 그렸다. 이 글은 그 뼈대를 교과서(Patterson & Hennessy, ARM 판의 LEGv8) 수준으로 정확하게 다시 그리고, 실제 명령 네 개를 하나씩 흘려보낸다.

보고 싶은 건 세 가지다. 레지스터가 왜 31개에 XZR 하나인지, 명령의 비트가 어떻게 배선으로 직접 이어지는지, 그리고 MUX 네 개가 명령마다 무엇을 고르는지 다. 결론부터 말하면 배선은 전부 고정이고, 명령마다 달라지는 건 MUX 의 선택과 쓰기 신호뿐이다.

레지스터 32칸, 진짜는 31개

레지스터 번호 (5 bit)이름00000X000001X100010X200011X300100X400101X500110X600111X701000X801001X901010X1001011X1101100X1201101X1301110X1401111X1510000X1610001X1710010X1810011X1910100X2010101X2110110X2210111X2311000X2411001X2511010X2611011X2711100X2811101X2911110X3011111XZRX0–X7: 인자 · 반환값X8: 간접 반환 주소X9–X15: 임시 (호출자 보관)X16–X17: 링커·호출 중간값X18: 플랫폼 예약X19–X27: 보존 (피호출자 보관)X28: SP 스택 포인터X29: FP 프레임 포인터X30: LR 복귀 주소31번 XZR: 읽으면 항상 0, 써도 버려진다. 실제 저장 공간이 없다.
레지스터 번호는 5비트라 32칸을 가리킬 수 있다. 그중 31칸(X0–X30)이 실제 64비트 저장소이고, 마지막 31번은 XZR 이라는 "항상 0" 인 가짜 레지스터다. 색은 소프트웨어 관행상의 용도이고 하드웨어는 전부 똑같이 취급한다.

명령 안에서 레지스터를 가리키는 자리는 5비트다. 5비트면 32칸을 셀 수 있다. 그런데 실제 저장소는 X0 부터 X30 까지 31개고, 마지막 31번은 XZR(zero register) 이라는 "읽으면 항상 0, 써도 버려지는" 가짜 레지스터다.

색은 소프트웨어 관행이다. 인자는 X0–X7 로 넘기고, X30 에는 함수 복귀 주소가 들어가고, X28 은 스택 포인터다. 하드웨어는 이 구분을 전혀 모른다. 31개 전부 똑같은 64비트 저장소다. 다른 건 31번 하나뿐이다.

XZR 은 회로가 아니라 규칙이다

레지스터 파일 안쪽 — 읽기 포트 하나와 쓰기 포트X0 64 bitX1 64 bitX2 64 bit⋮X30 64 bit0 (고정 배선)레지스터 31개 + XZR32:1MUX읽을 번호 (5 bit)읽기 데이터 (64 bit)번호가 11111 이면 0 이 나온다쓰기디코더쓸 번호 (5 bit)+ RegWrite11111 이면 어느 줄도 안 켠다XZR 은 회로가 아니라 규칙이다읽기: MUX 의 31번 입력을 0 에 묶어둔다쓰기: 디코더가 31번 줄을 만들지 않는다저장 공간 0 비트로 레지스터 하나를 얻는다
레지스터 파일의 읽기 포트는 32:1 선택기다. 번호 5비트가 선택 신호로 들어가고, 31번 입력만 저장소가 아니라 0 에 배선되어 있다. 쓰기 쪽은 번호를 풀어서 해당 레지스터의 쓰기 신호를 켜는 디코더인데 31번 줄이 아예 없다. 그래서 XZR 은 공짜다. 실제 레지스터 파일은 읽기 포트 2개, 쓰기 포트 1개가 이 구조로 들어 있다.

레지스터 파일의 읽기 포트는 32:1 선택기다. 번호 5비트가 선택 신호로 들어가서 32개 입력 중 하나를 출력으로 내보낸다. 31번 입력에는 레지스터 대신 0 이 배선되어 있다. 그게 전부다.

쓰기 쪽은 번호를 풀어서 해당 레지스터의 쓰기 신호를 켜는 디코더다. 이 디코더에 31번 출력이 없다. 31번으로 쓰라고 하면 아무 줄도 안 켜진다.

저장 공간 0비트로 레지스터 하나를 얻은 셈인데, 이게 의외로 많은 명령을 없애준다.

  • 복사 MOV X1, X2 는 ORR X1, XZR, X2 다. 0 과 OR 하면 그대로니까.
  • 비교 CMP X1, X2 는 SUBS XZR, X1, X2 다. 빼고 결과는 버리고 플래그만 남긴다.
  • 0 으로 초기화는 ADD X1, XZR, XZR 이다.
  • 부호 반전 NEG X1, X2 는 SUB X1, XZR, X2 다.

전용 명령을 만들면 디코더와 제어 회로가 늘어난다. XZR 하나로 그 명령들이 전부 기존 명령의 특수한 경우가 된다.

명령의 비트는 어디로 가나

R 형식31:21opcode20:16Rm15:10shamt9:5Rn4:0RdADD X1, X2, X3D 형식31:21opcode20:12주소 (9 bit)11:10op29:5Rn4:0RtLDUR X1, [X2, #8]CB 형식31:24opcode23:5분기 거리 (19 bit)4:0RtCBZ X1, #4같은 자리의 비트가 항상 같은 곳으로 간다[9:5]→ 읽기 레지스터 1 (Rn)[20:16]→ 읽기 레지스터 2 후보 (Rm)[4:0]→ 읽기 레지스터 2 후보 (Rt) 이면서 쓰기 레지스터 (Rd/Rt)[31:21]→ 제어 유닛[20:12] 또는 [23:5]→ 부호 확장 (opcode 를 보고 고른다)그래서 배선은 고정이고, 명령마다 다른 건 MUX 의 선택뿐이다.
32비트 명령을 자르는 법. 형식이 달라도 Rn 은 항상 [9:5], 쓰기 대상은 항상 [4:0] 이라서 레지스터 파일까지의 배선은 고정할 수 있다. 하나 애매한 게 [20:16] 과 [4:0] 중 어느 쪽을 두 번째로 읽을지인데, 그걸 MUX 하나(Reg2Loc)가 고른다.

32비트 명령을 자르는 법이다. 형식이 셋인데, 자르는 자리가 미묘하게 맞춰져 있다. Rn 은 항상 [9:5], 쓰기 대상은 항상 [4:0] 이다. 그래서 그 두 자리는 레지스터 파일의 읽기 1 포트와 쓰기 번호 포트에 그냥 직결할 수 있다. 제어 유닛이 개입할 필요가 없다.

하나만 애매하다. 두 번째로 읽을 레지스터가 R 형식에서는 [20:16] 의 Rm 이고, 저장(STUR)과 조건 분기(CBZ)에서는 [4:0] 의 Rt 다. 그래서 그 둘 중 하나를 고르는 MUX 가 필요하고, 이름이 Reg2Loc 이다.

즉시값도 자리가 다르다. D 형식은 [20:12] 의 9비트, CB 형식은 [23:5] 의 19비트다. 부호 확장기가 opcode 를 보고 어느 쪽을 쓸지 정한다.

데이터패스 전체

단일 사이클 데이터패스 전체 — MUX 네 개와 제어 신호PC명령메모리+4명령[9:5] Rn[20:16][4:0]M01Reg2Loc읽기 2[4:0] 쓰기 번호[20:12]/[23:5][31:21]레지스터 파일X0–X30, XZR읽기 1읽기 2쓰기 번호쓰기 데이터데이터 1데이터 2RegWrite부호 확장M01ALUSrcALUALUZero데이터메모리주소쓰기 데이터읽기MemReadMemWriteM01MemtoReg<<2+M01PCSrc제어 유닛Branch
교과서의 단일 사이클 데이터패스. 왼쪽에서 오른쪽으로 PC, 명령 메모리, 레지스터 파일, ALU, 데이터 메모리가 놓이고, 갈림길마다 MUX(보라색 사다리꼴)가 있다. Reg2Loc, ALUSrc, MemtoReg, PCSrc 네 개다. 위쪽은 다음 PC 를 정하는 길이고, 제어 유닛은 opcode 만 보고 모든 MUX 와 쓰기 신호를 정한다.

이제 전체를 놓고 보자. 왼쪽에서 오른쪽으로 PC, 명령 메모리, 레지스터 파일, ALU, 데이터 메모리가 놓여 있고, 위쪽에 다음 PC 를 정하는 길이 따로 있다.

갈림길마다 MUX 가 있다. 네 개다.

  • Reg2Loc: 두 번째 읽기 번호를 [20:16] 에서 가져올지 [4:0] 에서 가져올지.
  • ALUSrc: ALU 의 두 번째 입력을 레지스터에서 읽은 값으로 할지, 부호 확장한 즉시값으로 할지.
  • MemtoReg: 레지스터에 되돌려 쓸 값을 ALU 결과로 할지, 메모리에서 읽은 값으로 할지.
  • PCSrc: 다음 PC 를 PC+4 로 할지, 분기 목표 주소로 할지.

그리고 쓰기 신호가 셋 있다. RegWrite, MemRead, MemWrite. ALU 에는 무슨 연산을 할지 알려주는 신호가 간다. 제어 유닛은 opcode [31:21] 만 보고 이 전부를 정한다.

ALU 글에서 본 것처럼 ALU 안에도 MUX 가 있다. 네 회로가 동시에 계산한 결과 중 하나를 고르는 것이었다. 데이터패스의 MUX 는 그 바깥 버전이다. 모든 길이 항상 살아 있고 값이 흐르고 있으며, MUX 는 그중 어느 것을 다음 단계로 넘길지만 고른다. 안 고른 길에도 값은 흐른다. 그냥 무시될 뿐이다.

이제 명령 네 개를 흘려보낸다. 주황이 이번 명령에서 실제로 의미 있는 길이고, 회색은 값이 흐르긴 하지만 아무도 안 보는 길이다.

ADD X1, X2, X3

ADD X1, X2, X3 (X2 = 7, X3 = 5)PC명령메모리+4명령[9:5] Rn[20:16][4:0]M01Reg2Loc=0읽기 2[4:0] 쓰기 번호[20:12]/[23:5][31:21]레지스터 파일X0–X30, XZR읽기 1읽기 2쓰기 번호쓰기 데이터데이터 1데이터 2RegWrite=1부호 확장M01ALUSrc=0ALUALU=addZero데이터메모리주소쓰기 데이터읽기MemRead=0MemWrite=0M01MemtoReg=0<<2+M01PCSrc=0제어 유닛Branch=00x40X2X3X1751212 → X1다음 PC = 0x44
R 형식. Reg2Loc=0 이라 [20:16] 의 X3 가 두 번째 읽기 번호가 되고, ALUSrc=0 이라 읽기 데이터 2 가 그대로 ALU 로 간다. MemtoReg=0 이라 ALU 결과가 레지스터 파일로 돌아가 X1 에 쓰인다. 데이터 메모리는 주소를 받긴 하지만 MemRead 도 MemWrite 도 0 이라 아무 일도 안 한다.

가장 기본적인 R 형식이다.

  1. PC 가 0x40 을 들고 있고, 명령 메모리가 그 주소의 32비트를 내놓는다. 동시에 +4 가 0x44 를 계산해둔다.
  2. [9:5] 가 X2 로 읽기 1 포트에, [20:16] 이 X3 로 Reg2Loc MUX 의 0번 입력에, [4:0] 이 X1 으로 쓰기 번호 포트에 간다. 제어 유닛이 opcode 를 보고 Reg2Loc=0 을 내니 X3 가 읽기 2 포트로 들어간다.
  3. 레지스터 파일이 7 과 5 를 내놓는다. ALUSrc=0 이라 5 가 그대로 ALU 로 간다.
  4. ALU 가 12 를 낸다. 이 12 는 데이터 메모리의 주소 포트에도 들어가지만 MemRead 도 MemWrite 도 0 이라 아무 일도 없다.
  5. MemtoReg=0 이라 12 가 되돌아가고, RegWrite=1 이라 X1 에 쓰인다. PCSrc=0 이라 PC 는 0x44 가 된다.

CMP X1, X2 — 결과를 XZR 에 버린다

CMP X1, X2 = SUBS XZR, X1, X2 (X1 = 9, X2 = 9)PC명령메모리+4명령[9:5] Rn[20:16][4:0]M01Reg2Loc=0읽기 2[4:0] 쓰기 번호[20:12]/[23:5][31:21]레지스터 파일X0–X30, XZR읽기 1읽기 2쓰기 번호쓰기 데이터데이터 1데이터 2RegWrite=1부호 확장M01ALUSrc=0ALUALU=subZero데이터메모리주소쓰기 데이터읽기MemRead=0MemWrite=0M01MemtoReg=0<<2+M01PCSrc=0제어 유닛Branch=0X1X2XZR(31)990Z=10 → XZR: 버려짐다음 PC = PC+4
비교 명령은 별도 회로가 없다. 뺄셈을 하고 쓰기 번호를 31번(XZR)으로 주는 것이다. ADD 와 완전히 같은 길을 타지만, 쓰기 디코더가 31번 줄을 만들지 않으므로 결과 0 은 아무 데도 저장되지 않는다. 남는 건 ALU 가 낸 플래그(Z=1)뿐이고 뒤따르는 조건 분기가 그걸 본다.

앞에서 CMP 는 SUBS XZR, X1, X2 라고 했다. 실제로 흘려보면 ADD 와 완전히 같은 길 을 탄다. 다른 건 세 가지뿐이다.

  • ALU 에 가는 신호가 add 가 아니라 sub 다.
  • 쓰기 번호가 11111, 즉 31번이다.
  • 결과 0 이 되돌아오지만, 쓰기 디코더에 31번 줄이 없어서 어디에도 저장되지 않는다.

남는 건 ALU 가 낸 플래그다. 9 − 9 = 0 이니 Z=1 이다. 이 플래그를 다음 명령인 조건 분기(B.EQ 같은)가 본다. 비교 전용 회로는 어디에도 없다. 뺄셈기와 "31번은 버린다"는 규칙이 비교를 만든다.

LDUR X1, [X2, #8] — MUX 두 개가 반대로 넘어간다

LDUR X1, [X2, #8] (X2 = 0x1000, 메모리[0x1008] = 42)PC명령메모리+4명령[9:5] Rn[20:16][4:0]M01Reg2Loc=x읽기 2[4:0] 쓰기 번호[20:12]/[23:5][31:21]레지스터 파일X0–X30, XZR읽기 1읽기 2쓰기 번호쓰기 데이터데이터 1데이터 2RegWrite=1부호 확장M01ALUSrc=1ALUALU=addZero데이터메모리주소쓰기 데이터읽기MemRead=1MemWrite=0M01MemtoReg=1<<2+M01PCSrc=0제어 유닛Branch=0X2X180x10000x10084242 → X1다음 PC = PC+4
D 형식. ALUSrc=1 이라 두 번째 ALU 입력이 레지스터가 아니라 부호 확장된 즉시값 8 이다. ALU 는 주소 계산(0x1000 + 8)에 쓰이고, MemRead=1 로 그 주소를 읽는다. MemtoReg=1 이라 이번에는 ALU 결과가 아니라 메모리에서 읽은 42 가 X1 로 간다. 두 번째 읽기 포트는 쓰지 않으므로 Reg2Loc 은 아무 값이어도 된다.

메모리에서 읽는 D 형식이다. 앞의 두 명령과 다른 게 MUX 에서 바로 보인다.

ALUSrc=1. ALU 의 두 번째 입력이 레지스터가 아니라 [20:12] 를 부호 확장한 8 이다. ALU 는 산술 연산이 아니라 주소 계산에 쓰인다. 0x1000 + 8 = 0x1008.

MemRead=1. 그 주소로 데이터 메모리를 읽는다. 42 가 나온다.

MemtoReg=1. 되돌아가는 값이 ALU 결과가 아니라 메모리에서 읽은 42 다. RegWrite=1 로 X1 에 쓰인다.

두 번째 읽기 포트는 이 명령에서 안 쓴다. 그래서 Reg2Loc 은 0 이든 1 이든 상관없다. 교과서에서 x 라고 표시하는 "don't care" 가 이것이다. 값이 흐르긴 하지만 ALUSrc 가 다른 쪽을 골랐으니 아무도 안 본다.

CBZ X1, #4 — 위쪽 길이 살아난다

CBZ X1, #4 (X1 = 0, PC = 0x40)PC명령메모리+4명령[9:5] Rn[20:16][4:0]M01Reg2Loc=1읽기 2[4:0] 쓰기 번호[20:12]/[23:5][31:21]레지스터 파일X0–X30, XZR읽기 1읽기 2쓰기 번호쓰기 데이터데이터 1데이터 2RegWrite=0부호 확장M01ALUSrc=0ALUALU=pass BZero데이터메모리주소쓰기 데이터읽기MemRead=0MemWrite=0M01MemtoReg=x<<2+M01PCSrc=1제어 유닛Branch=10x40X1400Z=1다음 PC = 0x40 + 4×4 = 0x50
CB 형식. 검사할 레지스터가 [4:0] 자리에 있으므로 Reg2Loc=1 로 그 필드를 두 번째 읽기 번호로 쓴다. ALU 는 입력 B 를 그대로 통과시켜 Zero 플래그만 만든다. 위쪽에서는 19비트 분기 거리를 부호 확장하고 4배(<<2)해서 PC 에 더한 값이 준비되어 있고, Branch AND Zero 가 1 이면 PCSrc 가 그쪽을 고른다. RegWrite=0 이라 레지스터에는 아무것도 안 쓴다.

조건 분기다. "X1 이 0 이면 명령 4개 앞으로 건너뛰어라."

Reg2Loc=1. 검사할 레지스터가 [4:0] 자리에 있으므로 그 필드를 두 번째 읽기 번호로 쓴다. 이 명령에서는 [20:16] 이 레지스터 번호가 아니라 분기 거리의 일부라서, Reg2Loc 을 0 으로 두면 엉뚱한 레지스터를 읽는다. 이 MUX 가 왜 필요한지가 여기서 드러난다.

ALU 는 "B 통과". 읽은 값 0 을 그대로 내보내고 Zero 플래그만 만든다. Z=1.

위쪽 길. [23:5] 의 19비트를 부호 확장하고 <<2 로 4배한 뒤(명령이 4바이트 단위니까) PC 에 더한다. 0x40 + 16 = 0x50. 이 계산은 분기 여부와 상관없이 항상 돌고 있다.

PCSrc = Branch AND Zero = 1. 그래서 PC 에 0x44 가 아니라 0x50 이 들어간다. RegWrite=0 이라 레지스터에는 아무것도 안 쓴다.

제어 신호 한눈에

명령Reg2LocALUSrcALUMemReadMemWriteMemtoRegRegWriteBranch
ADD (R 형식)00add00010
SUBS XZR (CMP)00sub0001*0
LDURx1add10110
STUR11add01x00
CBZ10B 통과00x01

* RegWrite 는 1 이지만 쓰기 번호가 31 이라 실제로는 아무 데도 안 쓰인다.

표를 세로로 읽으면 각 MUX 가 하는 일이 보인다. Reg2Loc 은 "두 번째 레지스터가 [4:0] 에 있는 명령"(STUR, CBZ)에서만 1 이다. ALUSrc 는 "즉시값을 쓰는 명령"(LDUR, STUR)에서만 1 이다. MemtoReg 는 "메모리에서 읽어오는 명령"(LDUR)에서만 1 이다. 명령 형식과 MUX 선택이 거의 일대일이다.

정리

명령어 하나가 처리되는 과정을 배선 관점으로 다시 쓰면 이렇다.

명령의 각 비트 자리는 고정된 곳으로 직결 되어 있다. [9:5] 는 읽기 1, [4:0] 은 쓰기 번호, [31:21] 은 제어 유닛. 명령을 해석한다는 건 opcode 11비트를 보고 MUX 네 개와 쓰기 신호 세 개를 정하는 것이고, 나머지는 배선이 알아서 한다.

레지스터 31개는 전부 같고, 31번 자리는 0 에 배선된 빈칸 이다. 그 빈칸 덕에 복사, 비교, 초기화, 부호 반전이 전부 기존 명령의 특수한 경우가 된다.

모든 길에는 항상 값이 흐른다. ADD 를 할 때도 데이터 메모리는 주소를 받고, 분기 목표 주소는 매 사이클 계산된다. MUX 는 계산을 시키는 게 아니라 이미 나온 값 중 어느 것을 쓸지 고른다. ALU 안에서 봤던 것과 같은 원리다.

이어서

이 데이터패스가 코어 안 어디에 놓이는지, 실제 코어는 왜 이걸 15~20단계로 쪼개고 순서를 바꾸는지는 CPU 는 어떻게 생겼나에 있다. 가운데의 ALU 가 안에서 어떻게 생겼는지는 ALU 는 어떻게 생겼나에서, 그 아래 게이트와 트랜지스터는 트랜지스터로 논리 게이트 만들기와 트랜지스터는 어떻게 생겼나에서 다뤘다.

참고