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

컴퓨터 구조 (Architecture)/CSAPP

[CSAPP] Y86-64 - Basics

피그브라더 2020. 3. 5. 22:57

1. Y86-64

x86-64에 대해 알아보았으니, 이제는 그러한 ISA를 탑재하는 CPU의 구현 방식을 살펴볼 때이다. 동일한 ISA를 탑재한다고 하더라도, CPU가 명령어를 해석하고 실행하는 방식의 구현은 여러 가지가 될 수 있기 때문이다. 그중에서도 우리가 알아볼 구현 방식은 바로 Sequential Implementation과 Pipelined Implementation이다.

 

하지만 앞서 배운 x86-64를 기준으로 CPU의 명령어 실행 구현 방식을 알아보는 것은 상당히 까다롭다. x86-64는 대중의 인기를 얻기 위해 셀 수 없이 많은 최적화를 시도하였고 그로 인해 내부 구현도 이해하기 어려운 수준으로 복잡해져 있기 때문이다. 그래서 우리는 x86-64를 단순화한 버전인 Y86-64라는 ISA를 다룰 것이다. 이는 x86-64의 중요한 특성들을 대부분 포함하고 있지만 x86-64보다 CPU를 구현하기에 훨씬 수월한 체계를 갖추고 있기 때문에 공부 대상으로 아주 적합하다.

 

CPU를 구현하기 위해서는 먼저 그 CPU가 탑재하는 ISA의 정의 사항을 숙지해야 한다. 따라서 이번 포스팅에서는 Y86이 프로세서의 상태와 그것들을 조작하기 위한 수단으로서의 명령어 집합을 어떻게 정의하고 있는지 살펴볼 것이다. x86-64를 이해했다면 Y86은 조금 더 수월하게 이해가 될 것이니, 새로운 ISA를 공부한다고 해서 너무 걱정하지 않아도 된다.

 

2. Y86-64 프로세서 상태

2-1. 프로그램 레지스터 (Program Register)

15개의 64비트 범용 레지스터 15개를 의미한다. 각 레지스터는 다음과 같이 0부터 E까지의 고유 번호를 부여받으며, 이 값은 명령어 비트 배열에 레지스터 번호를 인코딩할 때 사용하게 된다. 참고로 F는 아무 레지스터도 가리키지 않는 고유 번호로, 명령어 비트 배열에 인코딩 되면 "접근할 레지스터가 없음"을 의미하게 된다.

 


2-2. 컨디션 코드 (Condition Code)

산술 혹은 논리 연산을 수행하는 명령어에 의해 0 또는 1로 세팅이 되는 레지스터들을 의미한다. Y86-64에는 다음과 같이 세 종류의 컨디션 코드 레지스터가 존재한다. ZF(Zero Flag)는 연산 결괏값이 0일 때 세팅이 되고, SF(Sign Flag)는 연산 결괏값의 MSB가 1일 때 세팅이 되고, OF(Overflow Flag)는 부호 있는 값의 연산에서 오버플로우가 발생했을 때 세팅이 된다.

 


2-3. 프로그램 카운터 (Program Counter, PC)

다음에 실행할 명령어의 메모리 주소를 저장하는 64비트 레지스터로, x86-64의 %rip 레지스터와 대응된다.


2-4. 프로그램 상태 (Program Status, Stat)

명령어의 실행이 정상적인지 혹은 에러를 발생시키는지를 나타내는 64비트 레지스터로, Stat이라고도 부른다. 만약 AOK라면 문제없이 다음 명령어를 실행하면 되고, 나머지 경우라면 현재 프로그램의 실행을 중단시켜야 할 것이다.

 

Mnemonic Code Meaning
AOK 1 명령어의 실행이 정상적으로 이뤄졌음을 나타낸다.
HLT 2 Halt 명령어가 실행되는 경우를 나타낸다.
ADR 3 잘못된 메모리 주소로 접근한 경우를 나타낸다.
INS 4 잘못된 명령어를 실행하는 경우를 나타낸다.

2-5. 메모리 (Memory)

바이트 배열 형태의 저장 공간으로, 프로그램의 데이터와 코드가 이곳에 로드된다. Y86-64에서는 바이트 오더링으로 Little Endian 방식을 채택하므로, 메모리에 로드되는 데이터들은 MSB가 주소가 큰 쪽에 위치하게 된다. 이는 뒤에서 명령어 비트 배열을 살펴볼 때 기억하고 있어야 한다. 그렇지 않으면 명령어 비트 배열에 상수가 인코딩 되는 방식을 보고 고개를 갸우뚱할 수도 있기 때문이다.

 

3. Y86-64 명령어 집합 체계

이번에는 Y86-64가 정의하는 명령어 집합 체계에 대해 알아보자. 복잡한 명령어가 여러 개 존재하는 x86-64와 달리, Y86-64는 상대적으로 명령어 집합 체계가 간소화되어 있고 명령어 비트 배열을 인코딩하는 방식도 단순하다. 또한 Y86-64의 명령어들은 그 길이가 제각기 다르다. 제일 짧은 명령어는 1바이트이고, 제일 긴 명령어는 10바이트이다.

 

CPU가 현재 명령어를 실행하면서 PC의 값을 올바른 값으로 갱신하기 위해서는 현재 명령어의 길이를 알아야 한다. 그것은 명령어 비트 배열의 첫 번째 바이트를 보면 판단할 수 있다. 첫 번째 바이트는 명령어 코드(Instruction Code)함수 코드(Function Code)로 이뤄져 있는데, 명령어 코드가 그 명령어의 종류를 명시하기 때문이다. 반면에 함수 코드는 해당 명령어의 구체적인 기능을 구별한다. 예를 들어 OPq 명령어는 함수 코드에 의해 구체적으로 무슨 연산을 수행할지가 결정이 된다.

 

이제 각 명령어에 대해 알아볼 차례이다. 명령어는 다음과 같이 크게 다섯 종류로 나눠서 설명할 것이다. 설명 시에 보여주는 그림은 명령어의 비트 배열(1바이트 ~ 10바이트)과 해당 명령어가 수행하는 동작을 나타낸다.


3-1. 계산 명령어 : OPq

산술, 논리 연산을 수행하는 명령어들이다. 연산 결괏값에 따라 컨디션 코드 레지스터들의 값을 세팅한다. 참고로 x86-64와 달리, Y86-64에서는 레지스터를 대상으로만 계산 명령어가 지원된다는 것을 알 수 있다.

 


3-2. 데이터 이동 명령어 : cmovXX, irmovq, rmmovq, mrmovq

레지스터와 메모리 사이에서 데이터를 이동시키는 명령어들이다. x86-64의 movq 명령어와 상당히 유사하다. 하지만 주소지정방식(메모리 주소 표현 방식)이 훨씬 단순하다는 것과, 어디서 어디로 데이터를 이동시키는지를 기준으로 명령어를 4개로 쪼개서 갖추고 있다는 점이 다르다. cmovXX는 컨디션 코드 레지스터들의 값이 특정 조건을 만족할 때만 레지스터에서 레지스터로 데이터를 이동시킨다.

 

 

다음 예시를 통해 각 명령어가 어떻게 비트 배열로 인코딩 되는지 살펴보자. 이때, Little Endian 규칙에 맞춰서 상수를 인코딩하는 것에 주목하자. 예를 들어 상수 0xabcd는 [cd ab 00 00 00 00 00 00]으로 인코딩 된다.

 


3-3. 흐름 제어 명령어 : jXX

PC의 값을 바꿔서 프로그램의 실행 흐름을 바꾸는 명령어들이다. 점프할 주소를 PC-relative 방식으로 표현할 수 있는 x86-64와 달리, Y86-64에서는 점프할 주소를 명령어 비트 배열에 직접 쓰는 방식만 제공된다는 점을 알 수 있다.

 


3-4. 스택 관련 명령어 : call, ret, pushq, popq

Y86-64의 스택은 x86-64의 스택과 거의 동일하다. 주소가 작아지는 방향으로 데이터가 쌓이며, %rsp 레지스터의 값이 스택의 TOP을 가리킨다. 푸시 연산을 할 때는 %rsp의 값을 감소시키고, 팝 연산을 할 때는 %rsp의 값을 증가시킨다. 스택과 관련된 명령어로는 4가지가 있다. 함수 호출을 위한 call, 함수 리턴을 위한 ret, 데이터 푸시를 위한 pushq, 데이터 팝을 위한 popq가 바로 그것이다.

 


3-5. 기타 명령어 : halt, nop

그 외 명령어도 알아보도록 하자.

 

 

4. CISC vs RISC

4-1. CISC (Complex Instruction Set Computer)

ISA 체계와 CPU 내부 회로의 형태가 복잡한 컴퓨터를 의미하며, 1980년대 중반 유행했던 ISA 디자인 방식이다. CISC의 기본적인 디자인 철학은 전형적인 프로그래밍 작업들을 위한 명령어들을 전부 마련하는 것이다. 그리고 메모리 참조는 상당히 느리고 무거운 작업이기 때문에 한 번 명령어를 가져올 때 최대한 많은 일을 수행할 수 있게 한다. CISC는 다음과 같은 특징들을 지니고 있다. 

 

4-1-1. 스택-지향 (Stack-oriented)

매개 변수로 인자를 전달할 때, 혹은 복귀 주소에 해당하는 PC의 값을 백업할 때 스택을 사용한다. 또한 푸시 연산과 팝 연산을 위한 명령어를 각각 별도로(명시적으로) 갖추고 있다.

 

4-1-2. 복잡하고 다양한 명령어 체계

예를 들어, CISC의 산술 연산 명령어는 메모리에 접근이 가능하다. 이 말은 즉슨, 한 번 메모리를 읽고 그것에 특정 값을 더한 결과를 다시 메모리에 접근하여 저장하는 과정을 단 하나의 명령어로 처리가 가능하다는 것이다. 이를 위해서는 CPU 내부 회로도 복잡해진다.

 

4-1-3. 컨디션 코드

산술 혹은 논리 연산 명령어의 연산 결괏값에 의해 컨디션 코드 레지스터들의 값이 세팅된다.


4-2. RISC (Reduced Instruction Set Computer)

ISA 체계와 CPU 내부 회로의 형태가 단순한 컴퓨터를 의미한다. 하드웨어 기술이 발달하여 속도나 성능 등의 문제가 많이 해결되면서, 최대한 CPU를 작고 단순하게 만들자는 것이 기본적인 철학이다. 예를 들어 캐시 메모리 기술이 발달하면서 메모리 참조 속도의 문제가 많이 해결되었기 때문에, 굳이 명령어 하나가 지나치게 무거운 작업을 수행하도록 함으로써 CPU 내부 구현을 어렵게 만들지 않겠다는 것이다. 단순한 작업을 수행하는 명령어들만 최소한으로 남겨 놓고, 모든 명령어들의 길이를 같게 함으로써 CPU를 작게 만드는 것이 훨씬 더 이득이라는 것이다. RISC는 다음과 같은 특징들을 지니고 있다.

 

4-2-1. 레지스터-지향 (Register-oriented)
매개 변수로 인자를 전달할 때, 혹은 복귀 주소에 해당하는 PC의 값을 백업할 때 레지스터를 사용한다. 또한 CISC보다 많은 수의 레지스터를 갖추고 있다(보통 32개). 가능한 한, 스택보다는 레지스터를 활용함으로써 메모리 참조를 최소화한다.

 

4-2-2. 단순한 명령어 체계

CISC에 비해 동일한 작업을 처리하기 위해 필요로 하는 명령어의 개수가 많은 대신, 각 명령어는 단순한 작업을 수행한다. 작으면서도 빠른 하드웨어가 많이 개발되기 시작하면서 이러한 철학이 지지받기 시작하였다. 예를 들어, RISC에서는 메모리 접근을 위한 명령어로서 보통 LOAD, STORE 단 두 개만 제공하는 경우가 많다. 메모리 접근이 필요한 경우에는 모두 이 명령어들로 구현하는 것이다.

 

4-2-3. 컨디션 코드 부재

컨디션 코드 레지스터가 따로 존재하지 않고, 대신에 Test 명령어를 제공하여 이를 통해 특정 레지스터의 값을 0 또는 1로 세팅하게끔 한다.


4-3. RISC 레지스터 구성 및 명령어 예시 (MIPS)


4-4. CISC vs RISC

CISC 추종자들은 프로그램의 코드가 더 적은 바이트로 이뤄져 있음을 장점으로 내세우고, RISC 추종자들은 작고 단순한 CPU로도 프로그램을 빠르게 돌릴 수 있다는 것을 장점으로 내세운다. 그렇다면 최근 트렌드는 어떨까? 데스크탑 프로세서의 경우, ISA의 디자인 방식이 과거에 비해 크게 중요하게 고려되지 않는다. 이미 성능이 뛰어난 하드웨어가 많이 개발되었기 때문에, 빠르게 만드는 건 일도 아니라는 것이다. 그래서 오히려 그런 것보다는 호환성 등의 문제가 더 관심사가 된다. 반면 임베디드 프로세서의 경우에는 RISC가 조금 더 지배적이다. 작고, 저렴하고, 적은 전력을 사용하기 때문이다. 그래서 대부분의 스마트폰도 RISC에 해당하는 ARM 프로세서를 채택하고 있다.

'컴퓨터 구조 (Architecture) > CSAPP' 카테고리의 다른 글

[CSAPP] Sequential Implementation  (0) 2020.03.09
[CSAPP] Y86-64 - Logic Design  (0) 2020.03.07
[CSAPP] x86-64 - Miscellaneous Topics  (5) 2020.03.03
[CSAPP] x86-64 - Data  (0) 2020.03.02
[CSAPP] x86-64 - Procedures  (2) 2020.03.01