이 글은 컴퓨터 구조의 게시글이예요

해당 컴퓨터구조 시리즈는 교재 컴퓨터 구조 및 설계와 🪢 고려대학교 컴퓨터구조 - 최린 교수님 의 강의를 토대로 공부하고 작성되었습니다.

컴퓨터는 어떻게 명령어를 실행하고 데이터를 읽을까 ?의 썸네일

컴퓨터는 단순히 1과0으로 이뤄진 전기 신호를 읽어 작동한다.

Computer 라는 이름에서 알 수 있듯이 컴퓨터는 단순히 계산을 시행하는 장치이다. 다만 매우 빠르게 실행하는

컴퓨터 내부의 연산 장치인 CPU는 전기 신호의 ON/OFF 만으로 산술 연산과 논리 연산을 시행하며 시행된 결과값들을 저장한다.

자세한 CPU의 동작 원리는 해당 영상을 참고하자 정말 멋진 영상이다.

🪢 CPU는 어떻게 작동할까? - bRd 3D

이런 전기 신호는 1 (ON 혹은 True) , 0 (OFF 혹은 False) 으로 이뤄진 이진수 형태로 CPU에서 연산되며 , 저장된 값은 메모리나 보조메모리에 이진수 형태로 저장된다.

상위 수준 언어로 작성된 코드
a = 5 + 10;

이라는 코드는 CPU는 읽을 수 없다. CPU는 단순히 1과0으로 이뤄진 이진수를 읽고 수행 할 수 있기 때문이다.

따라서 코드들은 CPU 가 읽을 수 있는 이진수 형태 (기계어 , machine language) 형태로 변환 되어 CPU 에게 전달되고 CPU는 기계어를 토대로 작동한다.

기계가 읽을 수 있는 기계어로 컴파일 된 코드
001000 00000 01001 0000 0000 0000 0101
001000 00000 01010 0000 0000 0000 1010
000000 01001 01010 01000 00000 100000

컴퓨터가 정보들을 이진수로 변환하는 과정

a = 5 + 10 을 예시로 들어 생각해보자

우선 해당 코드에선 =, + 와 같은 연산자가 존재하고 , a 라는 변수와 5 , 10 이라는 피연산자가 존재한다.

하지만 위에서 말했듯 컴퓨터는 이진수만 이해하기 때문에 해당 체계들을 이진수 형태로 변환하는 과정이 필요하다.

해당 포스트에선 십진수를 이진수로 변환하는 과정이나 문자열등을 이진수로 변환하는 과정을 모두 알고 있단 가정하에 작성한다.

또한 32bit 메모리 체계를 사용하는 CPU를 가정하고 실행한다. 메모리 체계가 32bit 라면 레지스터의 용량도 32bit이다.

산술 명령어를 이진수로 변환하는 과정

위 코드에선 + , = 와 같은 산술 명령어들이 존재한다.

기계어에선 명령어들을 가리키는 이진수들이 테이블 형태로 저장되어 있다.

예를 들어 + 라는 명령어는 기계어로 000000 이고 = 라는 명령어는 기계어로 101011 이다.

변수를 이진수로 변환하는 과정

해당 과정을 이해하기 위해선 우선 레지스터에 대한 개념을 이해해야 한다.

CPU 는 단순히 기계어로 이뤄진 명령을 처리하는 장치이다. 그렇다면 연산이 이뤄지기 위한 값을 저장하거나 연산이 이뤄진 후의 값을 저장할 저장소가 필요하다.

이렇게 CPU가 연산에 필요한 데이터를 저장하는 저장소를 레지스터라고 한다.

레지스터는 CPU와 가까운 거리에 존재하여 데이터를 불러오는 시간이 매우 빠르다. (레지스터에 저장되는 데이터의 형태들도 모두 이진수 형태이다.)

레지스터의 개수들은 사용하는 하드웨어에 따라 다르겠지만 약 32개 가량이 일반적이다.

레지스터 개수가 많으면 많을 수록 좋은게 아닌가 라는 생각이 들 수 있지만, 개수가 늘어나게 된다면 그 만큼 CPU 와의 거리가 멀어지기 때문에 가까워 데이터를 불러오는 속도가 빠르다는 장점이 줄어들기 때문에 레지스터의 개수들은 제한적이다.

이렇게 레지스터의 개수들은 32개로 고정되어 있기에 각 레지스터들을 가리키는 주소는 5bit 인 00000 로 표현 가능하다.

예를 들어 첫 번째 레지스터는 00000 이고 아홉 번째 레지스터는 00101 으로 표현 가능하다.

그렇다면 위 예시에서 a 라는 변수를 아홉 번째 레지스터인 01010 으로 둘 수 있다.

기억하자, 연산에 필요한 모든 데이터들은 레지스터에 저장되며, 각 레지스터들은 레지스터를 가리키는 이진수 형태로 참조한다.

이 때 고정된 값들이 존재해야 하는 레지스터들은 고정된 값이 저장되어 있다. 예를 들어 00000 와 같은 첫 번째 레지스터에선 0 이란 값이 담겨 있으며

해당 레지스터는 0이라는 숫자를 참조 할 때 사용한다.

데이터를 이진수로 변환하는 과정

십진수인 5 , 10 은 컴퓨터가 이해 할 수 있는 이진수 형태로 변환되어야 한다.

이에 숫자 5와 10은 이진수인 0010101010 로 변환된다.

이진수로 변환된 명령을 CPU가 실행하는 과정

위 예시에서 변수와 명령어, 데이터들을 모두 이진수 형태로 변환했다.

그렇다면 CPU는 어떻게 명렁들을 시행할까 ?

그것은 바로 변수와 명령어, 데이터들을 실행하기 위한 명렁어 필드가 존재하며 해당 필드에 맞게 이진수들을 나열해주면 된다.

명령어 필드

FieldBitsDescription
opcode6Operation code
rs5Source register
rt5Target register
rd5Destination register
shamt5Shift amount
funct6Function code (for R-type only)

각 명령어 필드들은 명령어 타입 타입 별로 필드의 생김새가 다른데, 우선 산술에 사용하는 R type 의 명령어 필드를 살펴보자

우선 6비트로 이뤄진 opcode 들이 존재한다. 위에서 예시로 들었던 +,= 와 같은 산술 명령어들이 들어가는 필드이다.

rs , rt , rd 는 각 레지스터들을 가리키는 주소를 담은 비트이다. 32개의 레지스터는 5비트의 이진수로 표현 가능하기 때문에 5비트만큼의 필드가 필요하다.

shamt 는 비트를 왼쪽,오른쪽으로 몇만큼씩 이동 시킬 때 사용하는 필드이며 현재 코드에선 사용하지 않는다.

다만 어떤 경우에 필요하냐면 어떤 이진수에 2^4 만큼 곱해주고 싶다면 실제 곱셈 연산을 하는 것 보다

4비트만큼 비트를 왼쪽으로 이동시켜준다면 곱해준 것과 같은 연산 결과를 갖는다.

funct 비트는 opcode 와 같이 실행 할 연산에 대한 정보를 제공한다.

이러한 명령어 필드들은 명령어의 타입들에 따라 유동적으로 변환이 가능한데 , 이러한 변환들은 추후 어셈블리어 때 더 자세히 설명하도록 하자.

위의 기계어들을 명령어 필드에 맞춰 살펴보자

기계가 읽을 수 있는 기계어로 컴파일 된 코드
001000 00000 01001 0000 0000 0000 0101
001000 00000 01010 0000 0000 0000 1010
000000 01001 01010 01000 00000 100000

해당 코드를 명령어 필드와 함께 살펴보자,

첫 번째 기계어
001000 00000 01001 0000 0000 0000 0101

001000 은 상수 연산 후 할당을 의미하는 opcode 이다. 해당 opcode 는 레지스터에 저장 된 값들을 연산 할 때 사용하는 것이 아닌 상수 그 자체로 연산을 시행한다.

이해하기 쉽게 이야기 하자면 에를 들어 5라는 값을 01001 레지스터에 담고 싶을 때 00000 레지스터 (0 값을 담고 있는 레지스터) + 5 의 값 (funct 필드에 존재하는 상수)을 01001 레지스터에 담으라는 것을 의미한다.

만약 상수 연산을 사용하지 않는다면 00101 (5) 라는 값을 어떤 레지스터에 담아준 후 다시 레지스터간 연산을 해야하는데, 연산 시 상수 연산자를 사용하면 불필요한 할당 과정을 스킵 할 수 있다.

첫 번째 기계어 , 01001 레지스터에 00000 레지스터 + 0101 을 더한 값을 담아라
001000 (opcode for addi) 00000 (source register $zero) 01001 (target register $t1) 0000 0000 0000 0101 (immediate value 5)
두 번째 기계어 , 01010 레지스터에 00000 레지스터 + 1010 을 더한 값을 담아라
001000 00000 01010 0000 0000 0000 1010

해당 연산도 동일하다. 01010 레지스터에 00000 레지스터 + 1010 (10) 값을 담으라는 기계어이다.

마지막 기계어 , 01000 레지스터에 01001 레지스터 + 01010 레지스터의 더한 값을 담아라
000000 01001 (첫 번째 기계어로 연산된 레지스터) 01010 (두 번째 기계어로 연산된 레지스터) 01000 (연산 결과가 저장될 레지스터) 00000 100000

마지막 기계어는 00000: 더하고 할당해라 , 01001 , 01010 : 레지스터의 값들을 , 01000 : 레지스터에 를 의미한다.

결국 a = 5 + 10 이라는 상위 수준 언어는 5라는 값을 상수 연산을 통해 레지스터에 저장하고 , 10이란 값을 레지스터에 저장하고

a 변수를 가리키는 레지스터에 저장 된 레지스터들의 더한 값을 할당하는 과정을 거친다.

기계어와 1:1 매칭이 되는 어셈블리 언어

인간이 이해 할 수 있는 상위 수준의 언어가 등장하기 전 까지 프로그래머들은 위처럼 기계어로 코딩을 작성했다.

기계어는 컴퓨터가 이해하기엔 매우 적합한 언어였지만 인간이 이해하기에는 직관적이지 못한 언어이다.

이에 기계어와 1:1 매칭이 가능하면서 인간이 이해하기 쉬운 어셈블리 언어의 개념이 등장했다.

해당 시리즈에선 MIPS 어셈블리어를 이용하여 포스트를 작성하도록 할 것이다.

상위 수준을 어셈블리 언어로 변환한 코드
# MIPS Assembly Code
li $t1, 5       # Load immediate value 5 into register $t1
li $t2, 10      # Load immediate value 10 into register $t2
add $t0, $t1, $t2 # Add values in $t1 and $t2, store result in $t0

위의 어셈블리어들은 기계어들을 어셈블리어로 변환한 코드이다.

첫 ~ 두번째 어셈블리어까지는 $t0 (01001 레지스터)$t1 (01010 레지스터)0+5 , 0+10 의 값을 담는 것이고

마지막 어셈블리어는 $t2 (01000 레지스터)$t0 + $t1 의 값을 담는 것이다.

각 레지스터에는 $ 와 같은 달러 사인이 붙는데 해당 기호는 실제 레지스터를 표현하기 위한 기호이다.

어셈블리 어는 위의 명령어 필드들과 1:1 매칭 되기 위해 타입 별 명령어 필드들과 호환되는 명령어 필드를 또 갖는다.

명령어 필드들에 대해서는 추후 어셈블리어를 이용한 다양한 연산들을 이야기 할 때 상황에 맞춰 설명하도록 하겠다.

정리

컴퓨터는 이진수를 이용해 산술 연산 및 논리 연산들을 시행하며 각 이진수들은 각자 역할에 맞는 필드에 맞춰 구분되어 읽힌다.

이런 언어를 기계어라고 하며 인간이 기계어를 이해 할 수 있게 사용하는 언어가 어셈블리어라는 것이다.

연산 과정에선 값들을 저장하기 위한 레지스터들이 존재하며 연산은 레지스터에 저장된 값들을 이용해 CPU가 연산한다는 것이다.