컴퓨터가 이해하는 정보
컴퓨터는 인간과 다른 언어인 이진수 정보, 0과 1로만 이해를 한다. 영화 매트릭스과 같이 0과 1로 이루어진 환경을 기계가 컴퓨터를 활용해 구현하는 것과 같다. 그렇다면 인간의 언어와 데이터를 컴퓨터는 어떻게 이해하는 걸까? 그리고 숫자와 문자, 명령어들은 이진수로 어떻게 표현하고 처리하는 것일까
bit: 정보의 크기 단위
인간의 언어 중 자음과 모음처럼 가장 기본 단위가 있드시, CPU 또한 가장 기존적인 정보의 단위가 있는데, 이를 Bit
라고 한다. 이진수의 가장 작은 단위는 0과 1 이므로 1Bit는 [0, 1]을 의미한다. 1Bit로 표현할 수 있는 것은 (0)과 (1), 2개이다 (). Bit가 커질 수록 표현 할 수 있는 개수가 많아진다. 예를 들어 2Bit는 0과 1을 2개씩, [(0,0), (0,1), (1,0), (1,1)]으로 총 4개를 표현할 수 있다 (). 이처럼 Bit는 으로 표현 수가 커진다. 하지만 Bit는 너무나 작은 단위이기에 Byte, KB, MB등으로 구분하여 정보의 크기 단위를 확장한다.
Bit와 Byte는 프로그램의 정보의 크기 단위다. CPU는 이 정보 전체를 이해하고 처리하는 것이 아니다. CPU는 그 하드웨어 크기 만큼 작아 그렇게 큰 정보를 해결할 수 없다. 그렇기에 CPU는 프로그램 정보들을 잘게 나누어 처리한다.
이렇게 잘게 나뉘어진 정보는 Word. CPU가 한번에 처리할 수 있는 데이터의 크기를 의미한다. CPU는 각 아키텍처에 따라 프로그램의 정보를 word 단위로 나누어 처리한다. 우리는 이 내용을 자주 보았다. 특히 프로그램을 설치할 때 보는 32bit, 64bit가 바로 이 Word이다.
숫자는 어떻게 저장되고 표현되는가
정수: 대부분 이진법
CPU는 0과 1로 이루어진 이진법으로 이해한다. 그런데 표현하는 숫자의 크기가 커질 수록 이진법으로 표현해야 하는 그 길이도 길어진다. 가령, 10진수로 표현한 1백만 ()을 이진법으로 표현하면 로 표현할 수 있다. 1백만도 길게 표현되는데, 프로그램 정보의 1백만은 정말 작은 수다. 그래서 너무나 길어진다면, 16진법을 활용해 표현하기도 한다. 특히 네트워크에서 보이는 IPv6 주소가 이에 해당한다.
소수: 부동 소수점
개발을 공부하면서 우리는 아래와 같은 문제를 많이 본다.
0.1 + 0.2 == 0.3
>>> False
우리의 뇌는 0.1은 0.1 이고, 0.1 에 0.2을 더하면 당연히 0.3 이다. 그런데 왜 이런 결과가 나오고, 이게 맞다고 하는 걸까?
정수는 이진법으로 쉽고 명확하게 표현이 가능하다. 하지만, 0.01과 같은 소수는 이진법으로 정확하게 표현하기 어렵다. 만약 0.1을 이진법으로 표현한다면, 0.0001100110011… 으로 무한대로 표현될 것이다. 그렇기 때문에 CPU는 소수점을 저장할 때 근사값으로 저장을 하게된다. 숫자는 정확한게 중요하지만, 표현 할 수 있는 것이 이진법이니까 이러한 방식을 택할 수 밖에 없는 것이다.
근사값? 부동 소수점!
부동 소수점은 무엇일까? 부동 소수점은 소수점을 고정하지 않고 필요에 따라 유동적으로 바꾸어 표현하는 표현 방식이다. 부동 소수점은 다음과 같은 구성 요소로 나눈다.
- 부호(sign bit): 양수인지 음수인지
- 지수(Exponent): 소수점 위치를 결정함 (Bias 추가 저장)
- 가수(Fraction): 실제 숫자 값
즉, CPU는 소수점을 지수와 가수로 나누어 저장을 한다. 부호 비트로 양수와 음수를 나누고, 지수로 소수점 위치를 결정하고, 가수로서 실제 수를 저장한다. 이 구성 요소 중 지수는 특징이 하나 있는데, 이진법으로 표현할 때, 부호비트가 없어서 음수 지수를 표현할 수가 없다.
그렇다면 음수를 표현할 수 없으면 -0.1은 표현할 수 없는 것일까? 아니 그렇지 않다. 음수를 표현하지 못한다면, Bias를 더해버리면 그만이다 () 이걸 지수에 더해서 양수처럼 저장해버린다. 실제 지수가 필요하게 되면 저장된 지수에서 Bias를 빼면 나오니 말이다.
부동 소수점을 예로 들어 소수를 이진법으로 변환하면, 은, 아래와 같이 계산된다.
-
정수 부분:
-
소수 부분:
- 정수:
- 정수:
-
전체 이진수:
-
정규화: 소수점 왼쪽에 만 남도록 (가수/지수 분할)
(: 지수, : 가수) -
부호비트: (양수)
-
지수 계산: (지수,
float
은 8bit 지수)
저장할 지수: -
가수 계산:
정규화 비트 =
가수 23bit로 채우면, (맨 앞 생략) -
조합:
부호 비트 (1bit) =
지수 비트 (8bit) =
가수 비트 (23bit) = 근사값
위 예처럼, 가수를 이진법으로 변경 시 23bit, 근사값으로 설정하기 때문에 오차가 발생하는 것이고, 이에 코드에서도 False
가 나오는 것이다.
하지만 소수는 실제 소수와 오차가 존재할 수 있다는 점이다. 컴퓨터는 소수점을 나타내기 위해 부동 소수점 표현 방식을 사용한다. 소수점이 고정되어 있지 않은 소수 표현 방식으로, 필요에 따라 소수점의 위치가 유동적으로 변한다는 의미이다. CPU는 소수점을 지수와 가수로 나누어 저장하는데, 지수를 양수로만 표한하기 위해 저장할 때 바이어스값이 더해져 저장된다. (). 이러한 방식에 더해 10진수 소수를 2진수로 표현할 때, 10진수 소수와 2진수 소수의 표현이 딱 맞지 않다는 것이고, 무한히 많은 소수점을 저장할 수 없는 한계점으로 오차가 발생할 수 있다는 것이다.
더 정확한 방법?
가수 계산 할 때, 우리는 가수를 23bit로서 변환했다. 이를 52비트로 바꾸어 변환하면 근사값이 더욱 줄 것이고, 그렇다면 조금 더 정밀한 표현이 된다.
즉, 위 예로는 32bit로서 표현한 것이고, 더 정밀하게는 64bit로 표현할 수 있다.
문자는 어떤 방식으로 인식될까
컴퓨터는 인간의 언어를 이해하지 못하는 깡통이다. 우리가 영어를 공부할 때를 생각해보자. 영어의 주어가 무엇이고, 목적어, 동사, 부사 등을 판단해서 한국어로 번역한다. 번역을 하지 않으면 우리는 이해를 하지 못한다. 컴퓨터도 마찬가지다. 인간의 언어를 이해하려면 0과 1로 언어를 번역해야 이해할 수 있다. 인간의 언어가 컴퓨터 언어로 번역하는 것을 우리는 인코딩 (Encoding) 이라고 한다. 반대로 머리 속의 한국어를 다시 영어로 말하는 것, 컴퓨터가 0과 1을 다시 인간의 언어로 변환하는 것을 디코딩 (Decoding) 이라고 한다. 머리 속의 한국말이 영어로 나오는거.. 그냥 그렇다 하자 그런데, 인간도 영어를 공부해서 이해하는 거지, 아예 접하지 않은 언어를 마주치면 이해할 수가 없다. 컴퓨터도 마찬가지다. 이해할 수 있는 문자는 문자 집합이라고 한다.
아스키 문자열
컴퓨터가 이해할 수 있는 가장 기본적인 문자 집합이 아스키 문자 집합이다. 하나의 아스키 문자는 8bit로 구성되어 있는데, 정확하게는 1bit의 패리티 비트와 7bit의 표기 비트로 구성되어 있다. 즉, 실질적으로는 개의 문자를 표기 할 수 있다.
근데 아스키 문자는 알파벳, 아라비아 숫자, 일부 특수 문자만 포함하기 때문에 우리의 자랑 한글을 표시 할 수 없다. 따라서 한글 인코딩 방식인 EUC-KR이라는 16진수 인코딩 방식을 거쳐서 한글을 표시한다.
유니코드
빌어먹을 아스키 문자 집합의 단점을 벗어나기 위해, EUC-KR 인코딩 방식을 사용해도, 갉밝뷁뾹 등 여러 한글 조합을 표현할 수 가 없다. 그래서 Unicode 방식이 등장했다.
유니코드 는 한글을 포함해 EUC-KR보다 훨씬 많은 언어랑 특수 문자, 이모티콘 까지 코드로 표현할 수 있는 통일된 (Uni) 문자 집합 (Code)다. 이래서 유니코드가 전 세계에서 가장 많이 사용하는 표준 문자 집합이 되었다. python 코드를 작성할 때, 한글이 깨지지 말라고 UTF-8, UTF-8-SIG
이게 바로 유니코드다.
유니코드의 UTF-8, 16, 32
이 코드들은 인코딩 방식을 의미하는데, 이 인코딩 방식은 가변 길이 인코딩방식이다. 인코딩 방식이 달라짐에 따라, 한글을 인코딩하면 각기 다른 코드 값이 나오는 것은 뭐 기정사실이다.
Base64
base64, 말그대로 64진법 인코딩 방식이다. 데이터를 아스키 형식으로 인코딩 하는 방법인데, 이진법으로 표현한 데이터를 64개의 아스키 문자열로 변환하는 것이다. 아스키, 유니코드가 있는데 이걸 왜 쓰는가? 이미지, PDF, Zip 파일을 보자. 텍스트 시스템으로 이걸 볼 수 있을까? 당연히 깨질 것이다. 그래서 이걸 텍스트로 변환해서 보내야 하는데, 각기 다른 언어로 이루어진 데이터를 각각의 유니코드로 사용할 수 없으니까, 차라리 Base64를 이용해서 이진 데이터를 텍스트로 바꿔 보내면 아주 안전하게 전달 할 수 있게 된다.
명령어는 어떻게 구성되고 실행될까
CPU는 명령으로 동작한다. 대상을 어떤 동작으로 처리하라고 하니, 명령어는 동작과 대상으로 구성된다. 물론, 대상은 데이터 자체가 될 수도 있고, 동작은 데이터가 저장된 위치를 의미할 수도 있을 것이다.
그렇기에 명령어로 수행해야 할 동작은 연산을 해야 하니, 연산 코드라고 불린다. 그렇다면 동작에 사용될 대상, 혹은 저장 위치는 오퍼랜드라고 한다. 즉, 명령어는 동작 명령인 연산 코드와, 그에 대상, 오퍼랜드는 0개 이상으로 구성된다.
오퍼랜드가 담긴 영역은 오퍼랜드 필드라고 하는데, 여기에는 대상이 명시되지 않고, 데이터가 저장된 위치를 저장하는 경우가 많다. 그래서 주소 필드라고도 한다. 따라서, 오퍼랜드 필드는 메모리 주소나, 레지스터 이름이 명시된다. 물론, 메모리 주소가 명시되면 메모리 접근을 추가로 필요하게 되기도 한다.
명령어의 동작, 연산 코드는 다양하게 존재한다. 대표적으로 데이터 전송, 산술/논리 연산, 제어 흐름 변경, 입출력 제어 로 구성되어 있다.
연산코드 종류
유형 | 연산코드 | 설명 |
---|---|---|
데이터 전송 | MOVE | 데이터 옮기기 |
STORE | 메모리 저장 | |
LOAD(FETCH) | 메모리를 CPU로 가져오기 | |
PUSH | 데이터 스택에 저장 | |
POP | 스택 최상단 데이터 추출 | |
산술/논리 연산 | ADD/SUBSTRACT/ MULTIPLY/DIVIDE | , , , 수행 |
INCREMENT DECREMENT | 오퍼랜드에 1을 더해라 오퍼랜드에 1을 빼라 | |
AND/OR/NOT | AND/OR/NOT 연산 수행 | |
COMPARE | 두 개의 숫자, TRUE/FALSE 값 비교 | |
제어 흐름 변경 | JUMP | 특정 주소로 실행 순서 변경 |
CONDITIONAL JUMP | 조건 부합시 특정 주소로 실행 순서 변경 | |
HALT | 프로그램 실행 중지 | |
CALL | 되돌아 올 주소 저장 후 특정 주소로 실행 순서 변경 | |
RETURN | CALL을 호출할 때 저장했던 주소로 복귀 | |
입출력 제어 | READ(INPUT) | 특정 입출력장치로 부터 데이터 읽기 |
WRITE(OUTPUT) | 특정 입출력장치로 데이터 쓰기 | |
START IO | 입출력장치를 시작 | |
TEST IO | 입출력장치의 현재 상태 확인 |
기계어와 어셈블리어
기계어는 말 그대로, 0과 1로 이해할 수 있는 언어를 기계어라고 한다. 영화 매트릭스에 보면 동일한 장면을 보인다. 하지만 기계어로는 무슨 말을 하는지 어렵기에 어셈블리어로서 통신이 가능하다.
명령어는 어떤 주기로 실행될까
메모리 안에는 프로그램이 저장되어 있고, 프로그램은 명령어로 구성되어 있다. CPU는 이 메모리에서 명령어를 인출-실행을 반복하는데, 이 과정에는 정형화된 흐름이 존재한다. 즉, 일정한 주기를 반복하여 실행되는 것을 명령어 사이클이라고 의미한다. (인출 사이클 - 실행 사이클)
하지만 메모리 주소가 명시되는 경우 메모리에 접근해야 하기 때문에, 메모리에 추가적으로 접근하는 간접 사이클 단계를 거친다.
참고자료
※ 이 글은 『이것이 컴퓨터 과학이다』 책을 기반으로, 다양한 자료를 참고해 작성했습니다.