안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

컴퓨터 구조 (Architecture)/컴퓨터의 개념 및 실습

[Chapter 5] The LC-3 - ISA를 이해하는 첫 걸음

피그브라더 2020. 1. 26. 15:29

지금까지 떠들었던 ISA(Instruction Set Architecture)의 진짜 정체는 무엇일까? ISA도 결국 특정 규칙과 체계를 가지고 있는 하나의 언어이다. 다만 한국말이나 영어와 같은 자연어가 아니고, C 언어나 자바와 같은 고급 언어도 아니며, CPU가 이해할 수 있는 0과 1로 이뤄진 기계어일 뿐이다. 이번 포스팅에서는 ISA의 개념에 대해 명확히 이해해 보고, 우리가 공부해볼 LC-3라는 교육용 ISA의 명령어 체계에 대해 구체적으로 한 번 알아볼 것이다.

 

1. ISA (Instruction Set Architecture)

ISA는 일종의 CPU 설계도와 같다. 해당 CPU가 어떤 데이터들을 대상으로 어떤 연산들을 수행할 수 있는지, 어떤 종류의 레지스터들을 몇 개 사용하는지, 어떤 구조의 메모리와 호환이 가능한지 등을 명시한 규칙과 체계이다. 이러한 맥락에서 ISA를 소프트웨어(프로그램)와 하드웨어(CPU) 사이의 인터페이스라고도 부른다. C 언어 등으로 작성된 프로그램이 ISA의 규칙에 맞게 적절히 기계어로 번역이 되면 그것을 CPU가 읽고 해석해서 지시된 동작을 수행할 수 있기 때문이다.

 

하나의 CPU는 반드시 하나의 ISA를 사용하므로, 어떤 CPU를 설계하기 위해서는 그 CPU가 사용할 ISA에 대한 이해가 선행돼야 한다. 예를 들어, 내가 인텔의 x86이라는 ISA를 사용하는 CPU를 만들어야 한다고 해보자. 그러면 나는 x86이 정의하고 있는 사항들을 충분히 숙지한 뒤에 CPU를 구성하는 논리 회로들을 설계해야 한다. 가령 x86이 명령어들을 64비트로 표현하고 그것의 상위 16비트를 opcode로 사용한다면, 64비트의 입력 중 상위 16비트만 Decoder에 입력시켜서 어떤 동작을 수행할지 판단하게 하는 논리 회로를 설계해야 한다.

 

또한 우리가 작성한 프로그램이 정상적으로 동작하려면, 프로그램의 코드를 번역하는 컴파일러나 어셈블러도 해당 CPU가 사용하는 ISA에 맞춰 설계되어야 한다. 예를 들어, 명령어를 16비트로 표현하며 그것의 상위 4비트를 opcode로 사용하는 ISA가 있고, 프로그램의 코드를 그 ISA의 규칙에 맞춰서 기계어로 번역하는 컴파일러나 어셈블러를 사용한다고 해보자. 그러면 프로그래밍이 정상적으로 된 프로그램이라 할지라도, CPU가 그 ISA를 사용하는 CPU가 아니라면 번역된 기계어를 제대로 이해해서 동작할 수 없게 된다. 

 

그렇다면 ISA가 구체적으로 정의하고 있는 내용으로는 구체적으로 무엇이 있을까? 크게 세 가지를 정의하고 있다고 말할 수 있는데, 첫째는 명령어 집합(Instruction Set)이며, 둘째는 레지스터 집합(Register Set), 그리고 마지막 세 번째는 메모리 구조(Memory Organization)이다.


1-1. 명령어 집합 (Instruction Set)

ISA가 첫 번째로 정의하는 것은 바로 해당 CPU가 사용할 수 있는 모든 명령어들의 집합이다. 즉 간단하게 말하면, 해당 CPU가 어떤 종류의 명령어들을 실행할 수 있는가를 명시한 것이다. 구체적으로는 다음과 같은 내용들이 정의된다.

 

1-1-1. 연산 종류 (opcode 종류)

opcode는 해당 명령어가 어떤 종류의 연산을 수행하는지를 나타내는 부분이다. 명령어가 몇 개의 비트로 이뤄지는지와 그중 어떤 비트들이 opcode를 나타내는지를 포함하여, 그러한 opcode의 종류로는 무엇이 있는지 정의된다. 

 

1-1-2. 자료형 (Data Type)

어떤 유형의 데이터들에 대한 연산을 지원하는지 정의한다. 어떤 자료형으로 표현되는 데이터를 대상으로 하는 명령어가 정의되어 있다면, 해당 ISA가 그 자료형을 지원(Support)한다고 표현한다. 가령 정수 데이터를 대상으로 한 ADD 명령어가 존재한다면 정수를 지원하는 것이고, 실수 데이터를 대상으로 한 ADD 명령어도 존재한다면 실수까지 지원한다고 볼 수 있는 것이다.

 

1-1-3. 주소지정방식 (Addressing Mode)

명령어가 연산을 위해 필요로 하는 데이터가 어디에 위치해 있는지를 명령어 비트 위에 표현하는 방식을 나타낸다. 데이터가 위치한 레지스터의 번호를 비트에 적을 수도 있고, 데이터가 위치한 메모리의 주소를 계산해내기 위한 정보들을 비트 위에 표현할 수도 있다. 


1-2. 레지스터 집합 (Register Set)

해당 CPU가 어떤 종류의 레지스터들을 몇 개 사용하는지 정의한다. 또한 각 레지스터의 길이(비트 수)도 정의하는데, 이것은 곧 해당 CPU의 Word Length를 정의한다는 말과 같다. 즉 한 번에 처리하는 데이터의 기본 단위를 정의한다는 것과 같은 맥락이다.


1-3. 메모리 구조 (Memory Organization)

해당 CPU와 호환 가능한 메모리의 AddressabilityAddress Space를 정의한다. Addressability는 메모리 내 한 location의 비트 수(레지스터의 길이와 같을 필요 없음)이며, Address Space는 메모리 location의 총개수를 의미한다고 하였다. 만약 각 레지스터의 길이, 즉 Word Length가 k비트로 정의된다면 메모리 주소도 k비트로 표현하게 되므로 Address Space는 2^k보다 작거나 같아야 할 것이다. 참고로 x86의 경우에는 메모리의 Addressability를 8비트로, 레지스터의 길이는 32비트로 정의하고 있다. 그래서 메모리에 저장된 특정 주소를 읽어오기 위해선 한 번에 4개의 location을 읽어와야 한다(즉 한 번에 반드시 하나의 location만 읽어오는 건 아님).

 

2. LC-3 (Little Computer 3)

2-1. 정의 사항

지금까지 ISA에는 구체적으로 무슨 내용들이 정의되어 있는지 알아보았다. 그렇다면 우리가 공부해볼 LC-3는 그것들을 어떻게 정의하고 있을까? 위에서 했던 것처럼 세 가지로 나누어서 알아보도록 하자.

 

정의 사항 내용
명령어 집합
(Instruction Set)
- 명령어의 길이 : 16비트 (opcode : 상위 4비트)

- opcode 개수 : 15개 ("1101"은 사용 X)
① 계산 명령어 : ADD, AND, NOT
② 데이터 이동 명령어 : LD, LDI, LDR, LEA, ST, STR, STI
③ 흐름 제어 명령어 : BR, JSR/JSRR, JMP/RET, RTI, TRAP

- 지원하는 자료형 : 2의 보수로 표현되는 16비트 정수

- 주소지정방식
① 메모리에 접근하지 않는 경우 : immediate, register
② 메모리에 접근하는 경우 : PC-relative, indirect, base+offset
레지스터 집합
(Register Set)
- 각 레지스터의 길이 (Word Length) : 16비트

- 범용 레지스터 (General Purpose Register) : 8개

- 특별 레지스터 : 직접 접근은 불가, 특정 명령어들에 의해 사용된다.
① PC (Program Counter) : 다음에 실행할 명령어의 주소 저장
② Condition Code : 조건문 분기 처리를 위한 정보를 저장 (특정 명령어에 의해 값이 설정됨)
메모리 구조
(Memory Organization)
- Address Space : 216개의 location

- Addressability : 16비트

2-2. 계산 명령어 (Operation Instruction)

데이터를 대상으로 실제로 어떤 연산을 수행하는 명령어를 말한다. LC-3에는 ADD, AND, NOT 세 가지가 존재한다. ADD와 AND는 두 피연산자를 더하는 연산이며, NOT은 하나의 피연산자 비트를 전부 반전시키는 연산이다. ADD와 AND는 또다시 주소지정방식에 의해 두 가지로 나뉜다. 하나는 두 피연산자를 모두 레지스터에서 가져오는 register 모드, 다른 하나는 한 피연산자를 레지스터에서 가져오되 나머지 한 피연산자는 명령어 비트 자체에서 가져오는 immediate 모드이다. register 모드와 immediate 모드는 레지스터의 주소지정방식에 해당한다.

 

참고로 Sign Extension이란 데이터의 비트 길이를 늘려주는 작업으로, 양수라면 0을, 음수라면 1을 왼쪽에 필요한 개수만큼 붙여주면 된다. 예를 들어, 16비트짜리 데이터와 5비트짜리 데이터를 더하기 위해선 5비트짜리 데이터를 16비트로 Sign Extension 해줘야 한다.

 

아래는 LC-3에 존재하는 계산 명령어들이다. 빨간색으로 표시된 비트는 명령어의 opcode, 파란색으로 표시된 비트는 동일한 opcode 내에서 주소지정방식을 구분해주는 비트에 해당한다. 또한 DR(Destination Register)은 결괏값이 저장될 레지스터, SR(Source Register)은 값을 가져올 레지스터이며, imm5는 해당 자리에 적혀 있는 5비트 그 자체를 의미한다.

 

 

※ 참고

- 뺄셈 : 2의 보수를 취하여 부호를 바꾼 뒤 ADD 명령어를 실행하면 된다.

- OR : AND와 NOT 연산을 활용하여 드모르간 법칙을 응용하면 된다.

- 레지스터 간 데이터 이동 : ADD(immediate 모드) 명령어에서 imm5를 0으로 설정하면 된다.

- 레지스터 값 0으로 초기화 : AND(immediate 모드) 명령어에서 imm5를 0으로 설정하면 된다.


2-3. 데이터 이동 명령어 (Data Movement Instruction)

레지스터 혹은 메모리에 존재하는 데이터를 이동시키기 위한 명령어를 말한다. 이를 위해서는 데이터의 위치를 비트에 표현하는 방법이 필요하다. 그것을 주소지정방식이라 부른다고 앞서 말한 바 있다. 레지스터의 주소지정방식으로는 위에서 말했듯이 register 모드와 immediate 모드가 있다. 한편 메모리의 주소지정방식으로는 PC-relative 모드, Indirect 모드, Base + Offset 모드가 있는데, 각각에 대해 한 번 알아보도록 하자.

 

PC-relative 모드는 PC 레지스터 값에 특정 값을 더해서 접근하고자 하는 메모리 주소를 표현하는 방식이다. 명령어 16비트 중에 opcode를 위한 4비트와 DR/SR을 위한 3비트를 제외했을 때 남는 9비트에 해당하는 정수(2의 보수)를 현재 PC 값에 더해서 메모리 주소를 표현한다. 그러므로 표현할 수 있는 메모리 주소는 최소 (현재 PC의 값 - 256), 최대 (현재 PC의 값 + 255)가 된다. 단 여기서 주의할 점은, 표현된 주소를 계산하는 시점(EVALUATE ADDRESS)은 FETCH 단계보다 나중이기 때문에 PC의 값이 이미 1만큼 증가한 상태라는 것이다. 따라서 현재 실행 중인 명령어의 주소가 A라면, A-255와 A+256 사이의 메모리 주소를 표현할 수 있게 된다.

 

Indirect 모드는 PC-relative 모드 방식대로 계산된 메모리 주소에서 읽어온 값을 접근하고자 하는 메모리 주소로 사용한다. 현재 PC의 값에 따라 표현할 수 있는 메모리 주소의 범위가 한정적이었던 PC-relative 모드 방식과 달리, 제한 없이 메모리 주소를 표현할 수 있다.

 

Base + Offset 모드는 Base 레지스터 값에 특정 값을 더해서 접근하고자 하는 메모리 주소를 표현하는 방식이다. 명령어 16비트 중에 opcode를 위한 4비트, DR/SR을 위한 3비트, Base 레지스터를 위한 3비트를 제외했을 때 남는 6비트에 해당하는 정수(2의 보수)를 Base 레지스터 값에 더해서 메모리 주소를 표현한다. 이 또한 PC-relative 모드 방식과 달리 제한 없이 메모리 주소를 표현할 수 있다.

 

한편 Load Effective Address라고 불리는 방식도 있는데, 이는 PC-relative 모드 방식대로 표현되는 주소를 특정 레지스터에 저장하는 방식을 말한다. 표현되는 메모리 주소를 레지스터에 저장만 할 뿐, 그 메모리 주소에 직접 접근하지는 않는다는 것에 주의하자. LC-3에서는 LEA라는 명령어가 이 방식을 구현하고 있다.

 

아래는 LC-3에 존재하는 데이터 이동 명령어들이다.

 


2-4. 흐름 제어 명령어 (Control Instruction)

PC 값을 바꿈으로써 명령어 실행의 흐름을 바꾸는 명령어를 말한다. LC-3의 흐름 제어 명령어는 크게 세 종류로 나눌 수 있다. 하나는 조건 분기(Conditional Branch), 다른 하나는 무조건 분기(Unconditional Branch, or Jump), 그리고 나머지 하나는 TRAP이다. 각각에 대해 한 번 알아보도록 하자.

 

1) 조건 분기 (Conditional Branch)

특정 조건이 만족될 때만 PC 값을 바꿔준다. 특정 조건이 만족된다는 게 무슨 의미일까? 앞서 LC-3의 CPU에는 Condition Code라고 불리는 특별한 레지스터가 존재한다고 말한 바 있다. 이는 N, Z, P 세 종류의 레지스터로 구성되어 있는데, 각각 Negative, Zero, Positive를 의미한다. 명령어 중에 레지스터에 값을 저장하는 명령어(ADD, AND, NOT, LD, LDI, LDR, LEA)의 경우에는 그 실행의 결과로 N, Z, P 레지스터의 값을 세팅한다. 레지스터에 저장되는 값이 양수이면 P 레지스터만, 0이면 Z 레지스터만, 음수이면 N 레지스터만 1로 세팅한다. 조건 분기 명령어는 이러한 N, Z, P 레지스터의 값을 살펴본 뒤 조건을 만족하는 경우에만 분기를 한다. LC-3에서는 BR 명령어가 이에 해당한다.

 

2) 무조건 분기 (Unconditional Branch, or Jump)

특정 조건을 살피지 않고 무조건 PC 값을 바꾼다. 즉 Condition Code 레지스터(N, Z, P)의 값과 무관하게 무조건 분기를 한다는 의미이다. LC-3에서는 JMP 명령어가 이에 해당한다.

 

3) TRAP

 운영체제의 특정 서비스 루틴 주소로 PC 값을 바꾼다. 앞서 운영체제(Operating System, OS)도 메모리에 올라가 있는 하나의 프로그램이라고 설명한 바 있다. 그 프로그램이 가지고 있는 각각의 루틴들을 서비스 루틴(Service Routine)이라고 부르며, 서비스 루틴을 호출하는 것을 시스템 콜(System Call)이라고 부른다. 쉽게 말해서 시스템 콜은 OS의 함수를 호출하는 것이다. 반면에 사용자가 직접 정의한 함수들은 서브 루틴(Sub Routine)이라고 부르며, 서브 루틴을 호출하는 것은 곧 사용자 정의를 호출하는 것이다.

 

LC-3의 경우 OS 서비스 루틴들의 시작 주소를 메모리[x00] ~ 메모리[xFF]에 저장해둔다. 따라서 LC-3의 TRAP 명령어는 호출하고자 하는 서비스 루틴의 시작 주소가 저장된 메모리 공간의 주소를 명령어 비트에 표현한다. 한편, 서비스 루틴의 실행이 종료되면 다시 원래의 실행 흐름으로 돌아갈 필요가 있는데, 이를 위해서는 PC 값을 바꾸기 전에 현재의 PC 값(이미 1이 증가한 상태)을 어딘가에 저장해둬야 한다. LC-3는 현재 PC의 값을 R7(Rn은 CPU에 존재하는 8개의 범용 레지스터 중 n번째 레지스터를 의미)에 저장해 두고, 서비스 루틴의 마지막 부분에서는 R7을 대상으로 한 JMP 명령어(이를 두고 RET 명령어라고 부르기도 함)를 실행함으로써 R7에 저장되어 있는 PC 값을 복구하여 원래의 실행 흐름으로 돌아오게 된다. TRAP과 관련한 내용은 이후 포스팅에서 훨씬 더 자세히 설명할 것이므로 여기서는 이 정도로만 이해하고 넘어가도록 하자.

 

참고로 LC-3의 TRAP 명령어에서 서비스 루틴의 시작 주소를 저장하고 있는 메모리 주소를 표현하는 8비트를 Trap Vector라고 부른다. 대표적으로 x23은 키보드로부터 문자 하나를 입력받아서 R0에 저장하는 서비스 루틴, x21은 R0에 저장된 문자 하나를 모니터로 출력하는 서비스 루틴, x25는 프로그램을 종료(halt)시키는 서비스 루틴에 해당한다.

 

아래는 LC-3에 존재하는 흐름 제어 명령어들이다.

 

* RTI 설명에서 "R6 ← Saved.USP 레지스터 값 (IF PSR[15] = 1)" 전에 "Saved.SSP ← R6 레지스터 값 (IF PSR[15] = 1)"이 누락되어 있음.

 

3. LC-3 CPU의 구조 (LC-3 Data Path) - Revisited

이전 포스팅에서 LC-3 CPU의 구조를 이미 보여준 바 있다. 이제는 LC-3의 각 명령어에 대해 알게 되었으니 LC-3 CPU의 구조와 동작 방식을 조금 더 자세히 뜯어보자. 다음 그림에서 보이는 LC-3 CPU의 구조와 동작 방식을 크게 세 가지로 나눠 설명할 것이다. 하나는 글로벌 버스(데이터 교환을 위한 도선), 다른 하나는 구성 요소(CPU를 이루는 각종 장치), 그리고 나머지 하나는 컨트롤 신호(CPU 전반을 통제하는 각종 컨트롤 신호)이다. 이전에는 이해가 안 되던 부분들이 조금씩 이해가 될 것이다.

 


3-1. 글로벌 버스 (Global Bus) - 데이터 교환을 위한 공통 도선

모든 장치들이 데이터를 교환하기 위해 사용하는 공통 도선으로, 간단하게 버스라고 부르기도 한다. LC-3의 Word Length는 16비트이므로, 버스도 16비트를 전달할 수 있게끔 16개의 도선으로 이루어졌다고 생각하면 된다. 버스에 값을 싣기 위해서는 그 부분에 해당하는 Gate 장치(Tri-state device)에 Enable 신호를 줘야 한다. 이 컨트롤 신호는 컨트롤 유닛의 유한 상태 기계에 의해 발생되며, 올바른 동작을 위해 반드시 한 클락 사이클에 하나의 Gate에만 Enable 신호를 주도록 되어 있다. 물론 여러 장치가 동시에 버스에서 값을 읽는 건 가능하다.


3-2. 구성 요소 (Component) - CPU를 이루는 장치

구분 구성 요소 (그림 기준) 설명
메모리
(Memory)
MEMORY Addressability : 16비트, Address Space : 216개
MAR 접근하고자 하는 메모리 공간의 주소를 저장하는 16비트 레지스터
MDR 메모리로부터 읽거나 메모리에 쓸 데이터를 저장하는 16비트 레지스터
처리 장치
(Processing Unit)
ALU 16비트 데이터를 대상으로 ADD, AND, NOT 연산을 수행하는 회로
REG FILE 16비트 범용 레지스터 8개 (R0 ~ R7)
컨트롤 유닛
(Control Unit)
PC 다음에 실행할 명령어의 메모리 주소를 저장하는 16비트 레지스터
+1
(= PC ADDER)
(FETCH 단계) PC의 값을 1만큼 증가시키는 회로
IR 현재 실행 중인 명령어를 저장하고 있는 16비트 레지스터
FINITE STATE MACHINE 명령어의 실행 단계 전반을 통제하는 유한 상태 기계 → 각 사이클(상태)마다 필요한 컨트롤 신호만 활성화
입출력 장치
(Input and Output)
INPUT 외부 입력 장치 (EX. 키보드)
OUTPUT 외부 출력 장치 (EX. 모니터)
기타 +
(= SPECIAL ADDER)
(필요한 메모리 주소를 계산하는 경우) 명령어의 offset과 PC 혹은 Base 레지스터의 값을 더해주는 회로 
N, Z, P, LOGIC
(Condition Code)
(ADD, AND, NOT, LD, LDI, LDR 명령어의 경우) LOGIC 회로는 명령어 실행 결과 값을 읽어서 N, Z, P 레지스터 하나만 1로 셋팅함 → 유한 상태 기계가 PC의 값을 바꿀지 말지 판단할 때 사용
MARMUX (메모리 접근이 필요한 경우) 메모리 주소로서 SPECIAL ADDER의 계산값을 사용할지, Trap Vector를 사용할지 판단하는 회로
PCMUX (PC의 값을 바꿔야 하는 경우) 1만큼 증가한 값으로 바꿀지, SPECIAL ADDER의 계산값으로 바꿀지, 레지스터 값으로 바꿀지(JMP 명령어) 판단하는 회로
SR2MUX (ADD, AND 명령어의 경우) 두 번째 피연산자로서 레지스터 값을 사용할지(register 모드), 명령어의 offset을 사용할지(immediate 모드) 판단하는 회로
ADDR1MUX (필요한 메모리 주소를 계산하는 경우) offset과 더할 레지스터로서 PC 레지스터를 사용할지, Base 레지스터를 사용할지 판단하는 회로
ADDR2MUX (필요한 메모리 주소를 계산하는 경우) 레지스터 값에 더할 offset으로서 몇 비트의 offset을 사용할지 판단하는 회로
ZEXT (TRAP 명령어의 경우) Trap Vector를 16비트로 Zero Extension 하는 회로
SEXT (명령어의 offset 비트가 필요한 경우) 해당 offset 비트를 16비트로 Sign Extension 하는 회로

3-3. 컨트롤 신호 (Control Signal) - 유한 상태 기계가 발생시키는 신호

구분 컨트롤 신호 (그림 기준) 설명
MUX Selctor

MARMUX (1비트) MARMUX의 입력 신호를 선택 (2개 중 1개)
PCMUX (2비트) PCMUX의 입력 신호를 선택 (3개 중 1개)
SR2MUX (1비트) SR2MUX의 입력 신호를 선택 (2개 중 1개)
ADDR1MUX (1비트) ADDR1MUX의 입력 신호를 선택 (2개 중 1개)
ADDR2MUX (2비트) ADDR2MUX의 입력 신호를 선택 (4개 중 1개)
Write Enable (WE) LD.MAR (1비트) MAR 레지스터에 값을 쓸 수 있게 함
LD.MDR (1비트) MDR 레지스터에 값을 쓸 수 있게 함
LD.REG (1비트) 범용 레지스터(R0 ~ R7)에 값을 쓸 수 있게 함
LD.PC (1비트) PC 레지스터에 값을 쓸 수 있게 함
LD.IR (1비트) IR 레지스터에 값을 쓸 수 있게 함
LD.CC (1비트) N, Z, P 레지스터에 값을 쓸 수 있게 함
레지스터 컨트롤 신호 SR1 (3비트) 첫 번째 피연산자로 사용할 레지스터의 번호 (0~7)
SR2 (3비트) 두 번째 피연산자로 사용할 레지스터의 번호 (0~7)
DR (3비트) 결과값을 저장할 레지스터의 번호 (0~7)
메모리 컨트롤 신호

MEM.EN 메모리를 활성화
R.W READ 또는 WRITE를 활성화
ALU 컨트롤 신호 ALUK ALU가 무슨 연산을 수행할지 결정 (ADD, AND, NOT)
Gate 장치 Enable 신호



GateMARMUX MARMUX의 출력값이 버스에 실릴 수 있게 함
GateMDR MDR에 저장된 값이 버스에 실릴 수 있게 함
GateALU ALU의 출력값이 버스에 실릴 수 있게 함
GatePC PC에 저장된 값이 버스에 실릴 수 있게 함