명령어 하나가 데이터패스를 지나가는 길 - 레지스터 31개와 XZR, 그리고 MUX 네 개
CPU 구조도에서 명령 하나가 지나가는 길을 뼈대만 그렸다. 이 글은 그 뼈대를 교과서(Patterson & Hennessy, ARM 판의 LEGv8) 수준으로 정확하게 다시 그리고, 실제 명령 네 개를 하나씩 흘려보낸다.
보고 싶은 건 세 가지다. 레지스터가 왜 31개에 XZR 하나인지, 명령의 비트가 어떻게 배선으로 직접 이어지는지, 그리고 MUX 네 개가 명령마다 무엇을 고르는지 다. 결론부터 말하면 배선은 전부 고정이고, 명령마다 달라지는 건 MUX 의 선택과 쓰기 신호뿐이다.
레지스터 32칸, 진짜는 31개
명령 안에서 레지스터를 가리키는 자리는 5비트다. 5비트면 32칸을 셀 수 있다. 그런데 실제 저장소는 X0 부터 X30 까지 31개고, 마지막 31번은 XZR(zero register) 이라는 "읽으면 항상 0, 써도 버려지는" 가짜 레지스터다.
색은 소프트웨어 관행이다. 인자는 X0–X7 로 넘기고, X30 에는 함수 복귀 주소가 들어가고, X28 은 스택 포인터다. 하드웨어는 이 구분을 전혀 모른다. 31개 전부 똑같은 64비트 저장소다. 다른 건 31번 하나뿐이다.
XZR 은 회로가 아니라 규칙이다
레지스터 파일의 읽기 포트는 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 하나로 그 명령들이 전부 기존 명령의 특수한 경우가 된다.
명령의 비트는 어디로 가나
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 를 보고 어느 쪽을 쓸지 정한다.
데이터패스 전체
이제 전체를 놓고 보자. 왼쪽에서 오른쪽으로 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
가장 기본적인 R 형식이다.
- PC 가 0x40 을 들고 있고, 명령 메모리가 그 주소의 32비트를 내놓는다. 동시에 +4 가 0x44 를 계산해둔다.
- [9:5] 가 X2 로 읽기 1 포트에, [20:16] 이 X3 로 Reg2Loc MUX 의 0번 입력에, [4:0] 이 X1 으로 쓰기 번호 포트에 간다. 제어 유닛이 opcode 를 보고 Reg2Loc=0 을 내니 X3 가 읽기 2 포트로 들어간다.
- 레지스터 파일이 7 과 5 를 내놓는다. ALUSrc=0 이라 5 가 그대로 ALU 로 간다.
- ALU 가 12 를 낸다. 이 12 는 데이터 메모리의 주소 포트에도 들어가지만 MemRead 도 MemWrite 도 0 이라 아무 일도 없다.
- MemtoReg=0 이라 12 가 되돌아가고, RegWrite=1 이라 X1 에 쓰인다. PCSrc=0 이라 PC 는 0x44 가 된다.
CMP X1, X2 — 결과를 XZR 에 버린다
앞에서 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 두 개가 반대로 넘어간다
메모리에서 읽는 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 — 위쪽 길이 살아난다
조건 분기다. "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 이라 레지스터에는 아무것도 안 쓴다.
제어 신호 한눈에
| 명령 | Reg2Loc | ALUSrc | ALU | MemRead | MemWrite | MemtoReg | RegWrite | Branch |
|---|---|---|---|---|---|---|---|---|
| ADD (R 형식) | 0 | 0 | add | 0 | 0 | 0 | 1 | 0 |
| SUBS XZR (CMP) | 0 | 0 | sub | 0 | 0 | 0 | 1* | 0 |
| LDUR | x | 1 | add | 1 | 0 | 1 | 1 | 0 |
| STUR | 1 | 1 | add | 0 | 1 | x | 0 | 0 |
| CBZ | 1 | 0 | B 통과 | 0 | 0 | x | 0 | 1 |
* 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 는 어떻게 생겼나에서, 그 아래 게이트와 트랜지스터는 트랜지스터로 논리 게이트 만들기와 트랜지스터는 어떻게 생겼나에서 다뤘다.
참고
- Patterson & Hennessy, Computer Organization and Design, ARM Edition — 4장 "The Processor" 의 LEGv8 단일 사이클 데이터패스와 제어 신호 표. 이 글의 그림과 신호 이름은 이 책을 따랐다.
- Zero register - Wikipedia — XZR 과 다른 ISA 의 제로 레지스터
- AArch64 - Wikipedia — 실제 ARMv8 에서 31번이 SP 와 XZR 을 겸하는 방식
- Procedure Call Standard for the Arm 64-bit Architecture (AAPCS64) — X0–X30 의 소프트웨어 용도
- Multiplexer - Wikipedia — MUX 의 동작