안녕하세요,
오늘은 16비트의 CPU와 메모리를 간단하게 자바로 구현해보겠습니다.
구현에 앞서 CPU의 구조와 작동원리를 알아야겠죠.
CPU의 기능
CPU는 명령어 인출과 해독을 모든 명령어에 대해 공통적으로 수행합니다. 그리고, 데이터 인출, 데이터 처리, 데이터 저장을 명령어에 따라 필요한 경우 수행합니다. 명령어 사이클의 각 개념은 아래와 같습니다.
- 명령어 인출 : 기억장치로부터 저장된 명령어를 읽어오는 과정
- 명령어 해독 : 수행해야할 동작을 결정하기 위해 명령어를 해독하는 과정
- 데이터 인출 : 데이터가 필요한 경우, 기억/입출력 장치로부터 데이터를 read
- 데이터 처리 : 데이터에 대한 산술,논리적 연산 수행
- 데이터 저장 : 처리 과정을 거쳐 얻어진 결과를 저장하는 과정
위 과정을 cpu는 clock마다 나누어 반복적으로 수행하는데, 각 명령어를 처리할 때 CPU의 같은 리소스를 사용하는 것은 아니므로 아래와 같이 병렬적으로 처리하여 시간, 공간적 효율을 높이는데 이를 명령어 파이프 라인이라고 합니다. 명령어 파이프 라인 구조를 어떻게 짤 것인가가 굉장히 중요하겠죠?
그리고 명령어 파이프 라인을 실행할 때 컴퓨터가 같은 리소스를 사용하게 되어서 1 clock에 1개의 명령어를 처리하지 못하는 경우가 생길 수도 있겠죠? 그런 경우를 해저드라고 합니다. 이번 구현에서는 명령어 파이프 라인과 해저드의 개념을 제외하고 CPU의 명령어 사이클을 순차적으로 처리하는 경우를 가정하였습니다.
CPU의 내부 구성 요소
CPU를 구현하기 위해서는 CPU의 내부 구성요소의 역할과 상호 간에 전달하는 메시지를 이해하고, 이를 객체 지향 언어로 적절하게 구현해야 합니다. 당연히 각 부품의 역할을 이해하는 것이 선행되어야 합니다. CPU는 대략적으로 아래와 같은 개념적인 구조를 갖추고 있습니다. 각 구성 요소를 이해해보겠습니다.
[ALU]
연산을 수행하는 장치로, 산술 연산과 논리 연산이 있습니다.
- 산술 : 사칙 연산
- 논리 : AND OR NOT XOR
[레지스터]
액세스 속도가 가장 빠른 기억 장치로 위 그림의 분홍색 부품들이 모두 레지스터에 속합니다.
- CPU 내부의 레지스터 수는 제한
- 명령어 실행에 필요한 레지스터 종류
- Program Counter(PC)
- 다음 인출에 필요한 명령어 주소를 가지고 있는 레지스터
- 인출 직후 명령어의 크기만큼 증가됨.
- 분기(Branch) 명령어가 실행되는 경우에는 목적지 주소를 갱신
- 누산기 (Accumulator)
- 데이터를 일시적으로 저장하는 레지스터로 연산 결과 저장
- 레지스터 크기는 CPU가 한번에 처리 가능
- MAR(Memory Address Register) : CPU가 읽거나 쓰려는 메모리 주소를 일시적으로 저장, 일부 데이터 저장하거나 데이터를 읽을 때 필요한 메모리 위치 주소를 저장한다.
- MBR(Memory Buffer Register) : 메모리에 읽거나 쓰려는 데이터 또는 명령을 일시 저장한다. 명령은 IR로 전송되며, 데이터는 AC 또는 I/O 레지스터로 전송된다. 즉, 이 레지스터는 메모리를 읽거나 메모리에 쓰려는 데이터 또는 명령을 임시 저장하는데 사용된다.
- IR(Instruction Register) : 메모리에서 명령을 가져오면 명령어 레지스터에 저장된다. 제어장치는 이 레지스터의 지시를 받아 컴퓨터 요소로 신호를 전송, 명령을 해석하며 실행한다.
- Program Counter(PC)
[기타]
- 제어 유닛
- 명령어를 해석하고 실행하기 위한 제어 신호를 발생시키는 하드웨어 모듈
- CPU 내부 버스
- 제어 유닛으로부터 발생되는 제어 신호 선들
- ALU와 레지스터 간에 데이터 이동을 위한 데이터 선
- 외부 시스템과 직접 연결 X, 버퍼 레지스터 or 시스템 버스 인터페이스 회로를 통해 접속
명령어 사이클
위에서 간단히 언급했던 명령어 사이클에 대해 자세히 알아보겠습니다. CPU가 1개의 명령어를 실행하는 전체 처리 과정을 의미, 프로그램 실행 시작부터 전원 종료나 회복 불가한 오류가 발생해서 중단될 때까지 반복합니다..
- 인출 : CPU가 기억장치에서 명령어를 읽어오는 단계
- 실행 : 명령어를 해독하고, 명령어를 실행하는 단계
[인출(fetch cycle)]
인출 사이클이 작동하는 원리는 아래와 같습니다. 클럭 주기에 따라서 메모리로부터 명령어를 아래와 같이 가져오는 인출 명령을 수행합니다. t는 CPU 클록 주기를 의미합니다.
- t0
- Memory Address Register ← PC
- PC에 존재하는 주소 값을 MAR에 전송합니다.
- t1
- Memory Buffer Register ← Memory Address Register 주소가 가리키는 Memory 데이터
- PC ← PC + 명령어의 크기 단위
- t2
- IR ← Memory Buffer Register
- MBR에 일시 저장한 명령어를 실행하기 위해 IR로 전송
[수행(execution cycle)]
수행은 인출과 다르게 IR에 저장된 명령어를 해독하여 수행하기 때문에 저장된 명령어가 무엇이냐에 따라 수행하는 행동이 결정됩니다. 명령어가 무엇인지, 그리고 명령어를 어떻게 해독하는지는 아래에서 좀 더 자세히 살펴보겠습니다. 우선 여기에서는 메모리에서 특정 위치에서 데이터를 가져와 Accumulator라는 레지스터에 전달하는 LOAD라는 명령어의 상세 과정을 살펴보겠습니다.
- t0 : Memory Address Register ← IR
- t1 : Memory Buffer Register ← Memory(Memory Address Register)
- t2 : Accumulator(여기에서는 destination Register) ← Memory Buffer Register
- Memory Address Register를 여기서에서는 Base Register와 Offset Register로 나누어서 배열에 접근함.
수행에서 산술, 논리 연산이 일어날 때, Basic 컴퓨터는 CPU에서 계산이 이루어지는 원리는 누산기 레지스터에 저장 되어있는 값과 메모리 버퍼 레지스터의 값을 로드하여 ALU에서 연산하여 ACC에 덮어 씌워집니다. 예를 들어보겠습니다.
- t0 : Memory Address Register ← IR
- t1 : Memory Buffer Register ← M(Memory Address Register)
- t2 : AC ← AC Operand MBR
Instruction Code
이제, Instruction이 무엇인지 알아보겠습니다. Instruction은 CPU를 제어하기 위한 코드로, 메모리의 코드 영역에서부터 CPU가 word 단위(레지스터의 크기와 동일)로 읽어와 CPU는 이를 차례차례 수행합니다. 메모리에 저장되는 정보는 컴파일러에 의해 바이너리의 형태인 어셈블리어로 저장되어 있습니다. 바이너리 코드를 읽어들여 CPU는 위의 decode 과정을 거쳐 차례차례 연산을 수행하게 되는 것이죠. Instruction은 바이너리 코드와 어셈블리어 사이의 약속이라고 보아도 될거같은데요, 어떻게 decode할지에 대한 명확히 정해진 규칙은 CPU마다 다릅니다. 보통 아래와 같이 Instruciton의 종류를 분류합니다.
1. Memory Reference Instruction
연산 방법을 지정하는 Operation Code(Opcode)와 피연산자가 존재하는 주소 정보를 나타내는 address part로 나누어진 Instruction 유형입니다.
- 연산 방법을 선택하는 코드와 피연산자의 주소 부분으로 나뉜다.
- Address part의 들어오는 값은 addressing mode에 따라 나뉜다.
- Address part에 들어있는 비트를 어떻게 해석할지에 대한 모드, 16비트는 2가지만 가진다.
- 0 Mod : direct addressing : operand = M[address]
- 1 Mod : indirect addressing : operand = M[M[addresss]]
- Address part에 들어있는 비트를 어떻게 해석할지에 대한 모드, 16비트는 2가지만 가진다.
2. register-reference instruction
- operand를 필요로 하지 않는 instruction
- 하위 비트로는 연산의 종류를 결정한다.
3. input-output instruction
- operand를 필요로 하지 않는 instruction
- 하위 비트로는 연산의 종류를 결정한다.
- interrupt on, input char to AC 등
16 Bit 컴퓨터를 만들 때 일단은 임의로 CPU가 동작하기 위해서 필수적인 Instruction Set만 만들어서 구현해보도록 합니다. 필수적인 Instruction은 load, store, 논리/산술 operation들이 있습니다.
프로그램 구현하기
구현에 있어서 핵심적인 객체를 먼저 정의합니다. CPU를 구현하는 것이니 CPU 객체가 있어야하고, 내부에서 데이터를 서로 전달해줄 레지스터 클래스와 데이터를 저장할 메모리 클래스를 정의했습니다. 프로그램 카운터는 레지스터의 일종이므로 레지스터를 상속받아 increment 메서드를 추가하여 정의했습니다.
[CPU]
싱글 프로세스에서 싱글 스레드인 경우를 가정했을 때, 파이프라인의 구성은 단순해집니다. 순차적으로 fetch와 execute를 반복하도록 동작하게 되며, 내부에 참조변수들로 레지스터의 배열, 프로그램 카운터, 기타 레지스터와 메모리를 가집니다.
여기에서는 예시로 fetch 메서드를 구현해보았고, 아래 execute는 abstract로 남겨두었는데요, 여러분이 한 번 instruction에 따라 분기하여 명령을 실행하도록 구현해보시기 바랍니다.
public class CpuSimulator {
public Register[] registers;
private ProgramCounter programCounter;
public Register bufferRegister = new Register();
public Register commandRegister = new Register();
public Register addressRegister = new Register();
private Alu alu;
public Memory memory;
public CpuSimulator(Memory memory) {
registers = new Register[7];
for (int index = 0; index < 7; index++) {
registers[index] = new Register();
}
programCounter = new ProgramCounter();
alu = new Alu();
this.memory = memory;
}
public void reset() {
for (int index = 0; index < 7; index++) {
registers[index].set((short)0);
}
bufferRegister.set((short)0);
commandRegister.set((short)0);
programCounter.set((short)0);
addressRegister.set((short)0);
}
public void runPipeLine() {
while (true) {
fetch();
if (!execute()) {
break;
}
}
}
private void fetch() {
addressRegister.value = programCounter.get();
bufferRegister.value = memory.get(addressRegister.get());
programCounter.increment();
commandRegister.value = bufferRegister.get();
}
abstract boolean execute();
}
[레지스터]
데이터의 기본 단위로서, 레지스터의 용량은 CPU의 word 단위와 동일하다고 했었죠. CPU는 16비트 컴퓨터이니 short(16비트)로 정의해줍니다. 그리고 Instruction을 처리해야하므로 인덱스를 입력받았을 때, 바이너리로 인덱스의 구간의 값을 리턴하는 메서드도 만들어주었습니다.
public class Register {
public short value = 0; //최대 16 Bit size(4Byte)
public short get(int startInclusive, int endInclusive) {
String subBin = View.toBinary(value, startInclusive, endInclusive);
short sum = 0;
for (int i = 0; i < subBin.length(); i++) {
sum = (short)(2 * sum + (subBin.charAt(i) - '0'));
}
return sum;
}
public short get() {
return value;
}
public void set(short value) {
this.value = value;
}
}
[메모리]
메모리의 특징은 RAM의 경우 항상 O(1)로 데이터에 접근할 수 있다는 특징이 있죠. 메모리 상에서 연속적으로 존재하는 배열도 마찬가지로 항상 O(1)의 시간복잡도로 데이터를 가져올 수 있습니다. 메모리와 실제로 유사한 자료구조로 구현하기 위해서 short 타입의 배열로 선언했습니다. 그러면 실제로 1024Byte의 메모리와 동일하게 작동할 것이라고 기대할 수 있습니다.
public class Memory {
/**
* Memory의 용량이 1024Byte라고 가정 (500 words)
* 메모리 word 단위는 2Byte(16bit)으로, short 사용해서 나타냄
* data index는 memory의 Instruction Line Number를 나타냄
*/
private short[] data = new short[512]; //1kb 메모리
public short get(short address) {
return data[address];
}
public int size() {
return data.length;
}
public void set(short address, short value) {
data[address] = value;
}
public short bin2Short(String binary) {
binary = binary.replaceAll(" ", "");
short sum = 0;
for (int i = 1; i < binary.length(); i++) {
sum = (short)(2 * sum + (binary.charAt(i) - '0'));
}
if (binary.charAt(0) == '1') {
sum = (short)(sum * (-1));
}
return sum;
}
}
이상으로 CPU와 메모리를 아주 간단하게 구현해보았습니다. 물론 실제 작동, 그리고 Instruction은 훨씬 복잡하고, 이 모델은 굉장히 간략화된 모델입니다. 그렇지만 구현을 통해서 CPU와 메모리가 어떻게 상호작용하고 Instruction을 처리하는지 알 수 있습니다.
'Computer Science > 기타' 카테고리의 다른 글
오토마타 이론과 Parser의 원리 - HTML Parser 만들기 (Java) (0) | 2023.02.01 |
---|---|
함수형 프로그래밍 입문 with 람다 대수, 함수형 인터페이스 (Java) (1) | 2023.02.01 |
객체지향 설계 연습(SOLID, GRASP) - 좌표평면 도형 그리기 (Java) (2) | 2023.01.28 |
프로세스의 메모리 영역 (Heap, Stack) 구현하기 (Java) (0) | 2023.01.15 |
덧셈과 가산기(Adder), 컴퓨터의 사칙 연산 원리 (Java) (0) | 2023.01.03 |