컴퓨터는 CPU, RAM, ROM, HDD 의 네 가지가 메인 구조이다. ( GPU 등은 부수적 요소 )
더 들어가면 CPU는 데이터를 처리하고 RAM, ROM, HDD는 데이터를 담아놓는 소자이다.
HDD ( OS ), ROM은 컴퓨터 부팅을 위한 데이터가 저장되어 있는 소자고
( HDD 는 OS를 담아놓으면서 CPU에서 처리한 데이터를 RAM을 통해 저장한다. RAM은 휘발성 소자이기 때문 )
CPU ( CPU에도 여러가지 소자가 있으며 데이터를 산술 계산하고 결과값을 내는 것은 ALU ) 는 RAM에 담아놓은 데이터를 넘겨받아 처리한 후 결과값을 다시 RAM에 담아놓을 수 있다.
이번에는 컴퓨터의 ALU가 데이터를 처리하기 위해 스택 ( RAM의 영역 중 한 부분 ) 에서 레지스터 ( CPU에 내장되어 있는 임시저장소 ) 와 어떻게 상호작용하는지 알아볼 예정이다.
CPU에는 사양에 따라 여러 종류가 있으며, 이번에는 레지스터가 16비트라고 가정하고 포스팅을 진행해보겠다.
** 참고 : 16 bit -- 2 byte
위의 사진은 CPU의 일부분을 발췌해와 RAM과의 상호작용을 도식화한 것이다. fetch ( 명령어 호출 ) 을 통해 버스를 거쳐 BIU에 도달하고 다시 명령어 레지스터 ( IR : Instruction Register ) 에 도달하여 대기하다 제어 유닛을 통해 명령어가 디코딩되어 ALU에서 결과값을 도출한다. 후에 ALU의 값을 레지스터에 저장해놓은 후 다시 RAM에 적재할 수 있다.
우리가 8개의 레지스터를 가지는 CPU를 설계한다고 가정할 때 위와 같이 구성할 수 있다.
r0 ~ r3는 연산할 값들을 담아놓거나 혹은 ALU에서 연산한 결과값을 담아놓을 용도로 마련해놓은 것이고
r4는 들어온 명령어를 담아놓는 레지스터 ( ex -- ADD r3, r1, r2 : r1과 r2를 연산한 값을 r3에 담아라 )
r5는 스택포인터로 다음 명령어 처리에 필요한 가용 스택 주소를 가르킨다. 스택을 반환하고 나면 fp의 주소로 돌아간다.
r6은 link register는 fp ( frame pointer ) 레지스터라고도 할 수 있으며 함수가 호출된 후 종료되면 스택에 담긴 데이터를 반환하게 되는데 데이터를 반환하고 돌아갈 스택의 위치를 기억하고 있는 레지스터다.
r7은 program counter로 다음 명령어가 실행될 주소를 담아놓는 레지스터다.
자, 그럼 sp, fp 레지스터 ( IR -- r5, r6 ) 를 한 번 살펴보자.
stack pointer & frame pointer
frame pointer는 스택 반환 후 돌아갈 주소를 기억하고 있는 레지스터이다.
한 가지 문제점이 있다. fp register는 sp 레지스터가 반복적으로 호출되어 주소값이 변경될 때마다 리턴해야하는 주소를 덮어쓰게 된다.
때문에 이를 해결하는 방법으로 스택에 fp 레지스터의 저장하면 된다.
main 함수가 호출되어 a, b가 선언되면 스택 ( RAM ) 에 데이터가 담기게 되고 fct 1 함수가 호출되기 직전에 fp 레지스터는 그 위에 리턴 후 돌아가야할 메모리 주소, 0번지를 얹는다.
fct1이 호출되어 c, d가 선언되고 마찬가지로 fct2가 호출되기 직전에 돌아가야할 주소, 8번지를 스택에 얹는다.
위를 반복
PUSH & POP
PUSH 0x02 --- 현재 sp 값을 참조해서 데이터 0x02를 해당 위치에 저장
PUSH r1 --- 현재 sp 값을 참조해서 레지스터 r1 값을 해당 위치에 저장
실질적으로 하는 일은 sp 레지스터에 저장된 값을 감소시키는 것이 전부다. POP은 피연산자가 불필요하고, 단순히 "POP" 이라는 형태로 명령어를 사용할 수 있다.
스택에 저장되는 모든 데이터가 4바이트라고 가정하면 다음과 같이 표현이 가능하다.
ADD sp, sp, -4 --- 현재의 sp 값에 -4를 더하여 sp에 저장
SUB sp, sp, 4 --- 현재의 sp 값에 4를 빼서 sp에 저장
instruction register
레지스터들은 16비트의 값을 가진다고 했고 이 말은 즉슨, 16개의 데이터 블록을 가진다고 할 수 있다. 각 블록들은 2가지의 데이터를 담을 수 있다. ( 0과 1 )
위의 사진은 '레지스터 r1의 값과 7을 더해 r2에 저장해라' 명령어를 담은 r4 레지스터의 간단한 모습이다.
볼 수 있듯이 연산자를 활용한 비트 구성은 예약, 연산자, 저장소, 피연산자1, 피연산자 2의 5가지 영역으로 구분시킬 수 있다. 여기서 예약 부분은 사용자가 사용할 기능 구분을 위해 남겨놓은 비트 구간이다.
( 후에 서술할 LOAD & STORE의 다이렉트, indirect 모드를 구분하기 위한 영역이다 )
그렇다면 저장소는 왜 레지스터 고정이냐? 물론 RAM에 직접적으로 접근할 수 있지만 명령어 구조, 그리고 하드웨어 구성이 복잡해지는 관계로 이런 제약사항이 필요하다.
그런데 문제점은 피연산자1과 2에 오는 코드가 숫자인지 레지스터인지 식별하기가 어렵다는 것이다. 예를 들어 피연산자에 0001이 들어오면 숫자 1인지 레지스터 r1인지 식별하기 어렵다.
( 레지스터 값이 들어오면 레지스터 안에 저장된 데이터를 참조하겠다는 의미로 해석 )
때문에 우리는 4개의 비트 중 첫번째 비트 ( 블록 ) 에 1이 오면 레지스터, 0이 오면 숫자로 식별시킬 수 있을 것이다.
그렇게 되면 우리가 실질적으로 표현할 수 있는 숫자는 3비트의 값, 피연산자 하나당 8개, 즉 0~7까지 밖에 되지 않는다.
숫자들간의 연산으로 우리가 한 번에 계산할 수 있는 값은 덧셈으로는 14, 곱셈으로는 49가 최대다.
때문에 우리는 STORE & LOAD 명령어의 개념을 도입해야 할 필요가 있다.
아래는 어셈블리어로 작성된 코드를 실행하는 시퀀스를 그려논 이미지다.
출처: https://popcorntree.tistory.com/55?category=832214 [어떤 프로그래머]
LOAD & STORE
위에서 우리는 연산을 진행할 때 명령어 구조의 복잡성 때문에 저장소는 레지스터 고정, 피연산자는 숫자와 레지스터로 제한해놓았다. 표현 범위가 좁아졌고 LOAD & STORE를 이용하면 해결할 수 있다.
메모리 주소에 값을 할당하고 그 주소를 LOAD로 레지스터에 적재 한 후 연산을 진행하고 그 값을 다시 STORE로 메모리에 적재해놓으면 더 넓은 범위의 값을 표현 가능하다.
기존 사칙연산자에서는 메모리에 직접 접근하는게 불가했지만 LOAD & STORE를 사용하면 가능하다.
아래는 C 언어와 어셈블리어가 병행되는 시퀀스이다.
( C ) int로 변수를 선언하면 sp 레지스터가 가르키는 가용한 스택의 주소에 값이 저장된다.
( asem ) LOAD와 ADD 그리고 STORE로 나머지 프로세스를 진행한다.
아래는 LOAD 명령 구조의 예시이다.
4가지 영역으로 나뉘어져 있으며 예약, 명령어, 저장소는 마찬가지로 레지스터 고정, source는 메모리의 주소 정보이다.
의미 : LOAD r3, 0x07 (0x07에 존재하는 데이터를 레지스터 r3에 저장하라)
컨트롤 유닛은 명령어가 LOAD임을 보고, 뒤에 오는 피연산자는 두 개 임을 알고 그에 맞게 정보를 해석하는 과정을 거친다.
이번에는 STORE를 보자
source와 destination이 뒤바뀌었다. source의 레지스터를 destination의 메모리 지번에 저장하라는 의미다.
의미 : STORE r2, 0x08 (레지스터 r2에 존재하는 데이터를 메인메모리 0x08번지에 저장하라
아래 사진의 연산 과정을 살펴보자
LOAD r1, 0x10 // 0x10번지에 저장된 데이터를 r1로 이동
LOAD r2, 0x20 // 0x20번지에 저장된 데이터를 r2로 이동
ADD r3, r1, r2 // r1, r2에 저장된 값을 더해서 r3에 결과 저장
STORE r3, 0x30 // r3에 저장된 값을 0x30번지에 저장
LOAD & STORE의 한계와 Direct & Indirect 모드
LOAD & STORE도 역시 표현의 한계에 대한 문제점이 있었다.
우리가 실질적으로 표현할 수 있는 메모리 주소는 8비트, 즉 10진수로 255 ( 2^{8}-1)까지만 가능하다.
즉, 0x0000 ~ 0x00ff 까지만 표현 가능하기에 0x0100번지에 저장된 데이터를 참조하려면 문제가 생긴다.
LOAD r1, 0x0100 은 메모리 지번 256의 주소에 담긴 데이터를 r1 레지스터에 담으라는 명령어인데 위에서 언급한 표현의 한계 때문에 이렇게 메모리 지번 256에 단번에 접근할 순 없다.
그렇다면 메모리 주소 0x0100에 접근하기 위해선 어떻게 해야할까?
MUL r0, 4, 4 // r0에 4 4 값 저장
MUL r2, 4, 4 // r2에 4 4 값 저장
MUL r3, r0, r2 // r3에 16 * 16 값 저장
STORE r3, 0x20 // 곱셈 연산을 통해 만든 0x0100( 256 )을 255의 범위 내에 있는 0x20에 저장
LOAD r1, [0x20] // 0x20에 저장된 주소번지를 참조하여 값을 가져와 r1에 저장
즉, 곱셈 연산을 통해 원하는 번지수의 값을 만들고 그것을 접근 가능한 스택에 넣은 후 indirect mode 로 접근할 수 있다.
direct와 indirect 모드를 구분하기 위해 우리는 예약 공간을 활용하여 00이면 dir, 01이면 indir 등 사용자 편의에 따라 정의하여 구분시킬 수 있다.
링크텍스트 --- 유용한 참고 주소
링크텍스트 --- 참고 주소2