CPU 작동 원리
2022년 1월 25일프로그래밍을 하다가 문득 이런 생각이 들었습니다. ‘CPU가 내가 쓴 코드를 처리 할 텐데 어떤 방식으로 처리하는 거지?’ 서비스나 프로젝트를 생각 할 때 소프트웨어적으로만 생각했지 하드웨어적으로 생각해본 적이 없었던 것 같습니다. 전에 설명한 인터럽트도 일종의 소프트웨어라서 하드웨어 쪽을 크게 신경 쓸 필요 없이 코드를 작성하면 됐었습니다. 필자는 학부 생활하면서 컴퓨터 구조를 수강했음에도 불구하고 하드웨어에 대한 그림이 잘 그려지지 않았습니다 (수업을 똑바로 안 듣..). 이참에 하드웨어와 친해지기 위해 익숙하지만 잘 모르는 CPU 친구를 알아보도록 합시다.
CPU란 무엇일까요? Central Processing Unit의 약자로 흔히 컴퓨터의 ‘뇌’로 비유합니다. 이름처럼 중앙에서(Central) 모든 연산 처리(Processing)를 담당합니다. 예를 들어 CPU는 SSD에 있는 사진 파일을 클릭해서 데이터를 메모리로 가져오고 이를 모니터에 보여 줄 때까지의 모든 연산 과정을 처리합니다. CPU 구조를 이해하기 위해 computer abstraction layers를 하나 씩 살펴보면 좋습니다. Computer abstraction layers가 생소할 수 있는데 네트워크 OSI 7계층을 떠올리면 감이 오실 것 같습니다. OSI 7계층의 하위 계층에서 상위 계층으로 올라가면서 네트워크 통신 과정을 단계별로 파악할 수 있듯이, 컴퓨터 구조를 여러 층으로 나누고 하위 계층에서 상위 계층으로 올라가면서 복잡한 컴퓨터 구조를 이해하기 수월할 것입니다. Intel이 제시한 computer abstraction layers에 따라 컴퓨터 구조를 10계층으로 나누고 1계층에서 7계층(Physics ~ Instruction set architecture)까지의 내용을 알아보도록 하겠습니다.
- Physics
- Transistors
- Logic gates
- Functional units
- Execution units
- Instruction set architecture
- Microarchitecture
- Operating system
- Programming languages
- Application
Physics와 Transistors를 시작으로 computer abstraction layers를 차근차근 하나씩 알아보겠습니다. Physics는 atom(원자) 단계입니다. CPU, 즉 반도체는 주로 실리콘(규소, Si)으로 만들어집니다. 이는 실리콘 원자의 특성 때문인데, 최외각 전자가 8개일 때 화학적으로 안정한 결합을 합니다. 실리콘의 최외각 전자가 4개이기 때문에 다른 실리콘 원자들과 단단하게 결합할 수 있습니다. 결합 중간에 불순물을 넣어봅시다. 최외각 전자가 3개인 붕소(B)와 결합하면 1개의 양공이 생깁니다. 최외각 전자 수가 5개인 인(P)이라면 1개의 자유전자가 생깁니다. 붕소를 넣어 전자 1개를 부족한 상태는 양(positive)의 성질을 띄어 P형 반도체, 인을 넣어 전자 1개가 많은 상태는 음(negative)의 성질을 띄어 N형 반도체라 합니다.

이제 이 둘을 붙여서 살펴보면 N형의 전자가 P형의 양공으로 이동하여 중간에 전자가 이동하지 않는 균형이 잡힌 상태인 공핍 영역이 생깁니다. 여기에 인위적으로 전지를 연결하고 전압을 걸어주는 방향에 따라 전류가 흐르게 하거나 흐르지 않게 할 수 있습니다. 전류를 흐르게 하는 방향을 순방향이라 하고 흐르지 않게 하는 방향을 역방향으로 연결했다고 말합니다. 순방향으로 연결하면 공핍 영역을 넘을 만큼의 힘이 가해져 전류가 흐르도록 합니다. 역방향이라면 애초에 공핍영역으로 전류가 흐르지 않는데 방향도 달라 전류가 흐르는 것이 더욱 어렵게 됩니다. 이렇게 PN 반도체의 성질과 전압 방향을 조합하여 만든것이 transistor(이하 트랜지스터)입니다. 트랜지스터의 가장 큰 특징은 앞서 말한 순방향, 역방향에 따라 전류를 제어하는 스위치 작용입니다. 트랜지스터의 기호는 다음과 같은데 가운데에 전압을 걸어주면 전류가 흐르는 걸 추상화한 기호입니다.

이러한 트랜지스터를 조합하여 logic gate(논리 회로)를 만들 수 있습니다. 논리 회로는 프로그래밍 언어의 논리 연산을 떠올리면 생각하면 편합니다. 입력 값 둘 중의 하나만 참이라면 참을 반환하는 OR, 모두 참이어야 참을 반환하는 AND 등 NOT, XOR, NOR, XNOR 같은 논리 회로들은 트랜지스터들을 조합하여 만들 수 있습니다. OR는 2개의 트랜지스터를 위아래로 연결하면 만들 수 있고, AND는 2개의 트랜지스터를 옆으로 연결하면 만들 수 있습니다. 여기서 컴퓨터가 이진법을 쓰는 이유를 엿볼 수 있습니다. 이진법은 0과 1로 수를 표현할 수 있는데 트랜지스터에 전류가 흐르는지(1) 흐르지 않는지(0)에 대한 상태를 표현하기에 적합한 수 체계라 할 수 있습니다. 논리 회로를 조합하여 덧셈을 할 수 있는 adder부터 decoder, multiplexer, counter, flip-flops 등과 같은 functional unit을 만들 수 있습니다.

Funtional Units들이 모여 더욱 복잡한 연산을 하는 execution unit이 됩니다. 가장 대표적인 execution unit은 CPU의 ALU(Arithmetic Logic Unit, 산술논리장치)입니다. ALU는 이름 뜻 그대로 더하기, 빼기 같은 연산(산술)과 OR, AND와 같은 연산(논리)을 하는 장치, 쉽게 말하면 계산기입니다. 계산기가 있지만 계산할 입력 값들이 필요합니다. 사용자가 수를 입력해야 하는데 어떻게 CPU까지 숫자를 전달해야 할까요? Microarchitecture와 instruction set architecture(이하 ISA)을 살펴보면서 하드웨어와 소프트웨어 간에 소통하는 방법에 대해 알아봅시다.
Intel에서 말한 computer abstraction layers의 microarchitecture와 ISA는 computer architecture 개념으로 묶을 수 있습니다. Microarchitecture는 CPU architecture라고도 하며, CPU의 설계 방식을 다룹니다. 구체적으로 CPU가 어떻게 동작할지, 어떻게 효율적으로 설계할지, ISA를 물리적으로 구현하는 방법에 관해 기술한 것입니다. ISA는 하드웨어가 이해하고 실행할 수 있는 기계어 명령어 집합을 말합니다. 한국인이 한국어를 이해하듯이 컴퓨터는 ISA를 통해 들어오는 명령어를 이해합니다. 또한 한국어, 영어, 스페인어와 같이 다양한 언어가 있듯이 ISA도 x86, ARM, MIPS와 같은 다양한 ISA 종류가 있습니다. 한국어를 영어로 번역하면 이해할 수 있듯이 Apple의 Rosetta2와 같은 번역기로 x64를 ARM으로 번역하면 ARM ISA가 이를 실행할 수 있습니다. 비슷하게 영한사전을 통해 영어의 문법, 단어, 의미를 한글로 풀어준다면 ISA는 0과 1로 이루어진 데이터를 사전에 정의된 AND, OR, NOT, LOAD, MOVE, STORE, BRANCH와 같은 명령어로 해석해 줍니다.
- The CPU architecture defines the basic instruction set, and the exception and memory models that are relied on by the operating system and hypervisor.
- The CPU microarchitecture determines how an implementation meets the architectural contract. The microarchitecture defines the design of the processor, covering such things as: power, performance, area, pipeline length, and levels of cache.
(Arm, https://developer.arm.com/architectures/cpu-architecture)
Instruction 구조도 한 번 살펴보겠습니다. Instrcution은 opcode와 operand로 이루어져 있습니다. Opcode는 앞서 말한 ADD, LOAD, STORE와 같은 명령어가 담겨져 있고 operand는 명령어를 실행 할 주소 값이 담겨져 있습니다. 예를 들어, 이진법으로 LOAD 명령어를 1000, ADD 명령어는 1001이라 가정하고 다음의 instruction을 해석해봅시다.

위 예시는 ‘0001 주소에 있는 데이터를 메모리로 가져오고(LOAD) 0010 주소에 있는 데이터와 덧셈하라(ADD)’라는 의미로 해석할 수 있습니다. CPU를 설계하는 방식에 따라 opcode, operand의 길이와 opcode의 명령어 종류가 다양하게 구현될 수 있습니다.
현대의 컴퓨터 구조는 기본적으로 폰 노이만 구조(Von Neumann Architecture)를 따릅니다. 폰 노이만 구조의 가장 큰 특징은 연산에 필요한 명령어와 데이터가 메모리라는 곳에 저장되어 있다는 점입니다. 그 이전에는 연산할 때마다 필요한 명령어와 데이터를 일일이 전선을 바꿔 끼워가면서 진행했습니다. 폰 노이만 구조에서는 명령 주기(instruction cycle)에 따라 CPU가 메모리에 저장된 명령어와 데이터를 가져와 연산을 수행합니다. 명령 주기는 크게 4단계로 나뉘어 집니다.
- Fetch: 명령어(or 데이터) 가져오기
- Decode: 명령어 해석하기
- Execute: 명령어(or 데이터) 실행(or 계산)하기
- Store: 데이터 값 저장하기
용어만 봐서는 선뜻 감이 오시지 않을 겁니다. 위의 과정을 간략하게 표현한 아래의 예제와 함께 살펴봅시다. 앞 4비트는 opcode, 뒤 4비트는 operand로 총 8비트 instruction 구조를 가지고 opcode 1000은 LOAD, 1001은 ADD, 1002 STORE 명령어라 정의해봅시다.

Program counter는 명령을 실행할 메모리 주소 값을 나타내며 명령이 수행되고 나면 +1을 하여 다음 메모리 주소의 명령을 가리키도록 합니다. 편의상 1000 0001을 LOAD [0001], 1001 0002를 ADD [0002]와 같이 바꿔서 봅시다. Step 2, 4, 6에서 메모리에 있는 instruction을 가지고 오고 Step 3, 5에서 메모리에 있는 데이터를 가지고 옵니다. 이 과정을 fetch라고 합니다. Step 3, 5, 7에서 메모리부터 가지고 온 instruction의 opcode를 해석하는 것을 decode(ex. 1000 → LOAD), opcode 명령을 실행하는 것을 execute(ex. LOAD [0001] → 0001번지의 데이터를 가지고 오라)라 합니다. Step 7에서 연산한 값을 메모리에 저장하는 것을 store이라 합니다.
Logic gates 부분에서 언급했듯이 컴퓨터는 0과 1로 통신합니다. 이처럼 0과 1로 이루어진 언어를 기계어라하고 사람이 더 쉽게 이해할 수 있도록 만든 언어가 어셈블리어입니다. 위의 예시로 보면 1000 0001 부분이 기계어이고 LOAD [0001] 부분이 어셈블리어입니다. 어셈블리어의 특징 중 하나는 1000은 LOAD, 1001은 ADD, 1002는 STORE와 같이 기계어와 1:1 대응합니다. 따라서 어셈블리어는 각기 다른 정의를 가지고 있는 다른 ISA와 호환이 되지 않습니다. 즉, 어셈블리어는 이식성이 낮습니다. 기계어와 어셈블리어를 칭할 때 low-level langauge(저급 언어)라 합니다.
Low-level이라는 것이 사람 친화적이지 않다고 해서 붙여졌다고 하는데 저는 이식성이 낮아(low) 기계어와 어셈블리어가 low-level language라 불리는 게 아닌지 생각해 봅니다. 이 같은 문제를 해결하기 위해 자연스럽게 high-level langauge(고급 언어)가 나타납니다. 사람이 이해하기 더욱 쉽고 다른 하드웨어와 호환이 가능하도록 만든 것이 C, Java, Go와 같은 언어입니다.
고급 언어들은 컴파일러의 도움으로 하드웨어에 맞는 어셈블리어로 변환하고 ISA가 다시 기계어로 변환하여 최종적으로 하드웨어가 이해할 수 있는 형태로 만듭니다. 각 아키텍처마다의 컴파일러로 동일한 코드로 동일한 결과물을 얻을 수 있습니다. 이를 조금만 응용하면 크로스 컴파일을 환경을 구성하여 성능이 다소 낮은 ARM 아키텍처 임베디드를 대신해 x64 환경에서 컴파일을 수행하고 결과물을 임베디드로 옮겨서 실행할 수 있습니다. 결론적으로 고급 언어를 사용하면 컴파일러가 어셈블리어로 변환하고 ISA가 기계어로 변환하여 CPU가 instruction cycle을 통해 연산을 수행합니다. ISA가 어셈블리어를 기계어로 변환해 주는 길목에 있어서 소프트웨어와 하드웨어의 다리라고도 불립니다. ISA가 CPU에 있어서 얼마나 중요한 존재인지 아시겠나요?
여기까지 읽으신 분들이 있을진 모르겠지만 저의 모자란 글 읽느라 수고 많으셨습니다. 위의 내용들을 잘 안다고 해서 좋은 서비스를 만든다는 보장은... 없겠지만 무척 흥미롭지 않나요? 마음 같아선 Microarchitecture와 ISA쪽을 더 파헤쳐 Branch Prediction, RISC와 CISC, Superscalar 같은 개념도 버무리고 싶지만 필자의 글 능력도 그렇고 필자의 머리도 그래서 여기까지 하도록 하겠습니다. 이번 주제의 글을 쓰면서 몇 번이나 참고했던 훌륭한 영상을 공유하면서 마치도록 하겠습니다 :)
참고 자료