강민철님의 "혼자 공부하는 컴퓨터 구조+운영체제"을 기반으로 학습한 게시물입니다.
📕 목차
1. 0과 1로 숫자 표현
2. 0과 1로 문자 표현
3. 소스코드와 명령어
4. 명령어 구조
1. 0과 1로 숫자 표현
📌 정보 단위
- 1bit는 0(off), 1(on) 2개의 정보를 나타낼 수 있다. → n-bit는 2^(n)개
- bit - byte - kB - MB - GB - TB (단, 1byte=8bit이고 나머지는 1:1000)
- 1kB는 1,024byte, 1MB는 1,024KB...로 표현하는 것은 잘못된 관습
- 1,024개 묶어 표현한 단위는 KiB, MiB, GiB, TiB이다.
- 워드(word) : CPU가 한 번에 처리할 수 있는 데이터 크기
- 현대 컴퓨터 워드 크기는 대부분 32비트 또는 64비트
- 인텔 x86 CPU는 32비트 워드 CPU, 인텔 x64 CPU는 64비트 워드 CPU에 해당한다
- 정의된 워드의 절반 크기(하프워드), 1배 크기(풀워드), 2배 크기(더블워드)로 나눌 수 있다.
📌 이진법 (binary)
- 모든 수를 0과 1만으로 숫자를 표현한다.
- Sign and magnitude, one's complement, two's complement 등의 부호 체계가 존재한다.
- 1의 보수는 비트 뒤집기, 2의 보수는 1의 보수에서 +1을 한 결과라고 보면 된다. (음수)
- 컴퓨터는 음수를 판단하기 위해 플래그(flag)를 사용한다.
- 11의 2의 보수(0101)와 십진수 5인 0101을 컴퓨터는 플래그로서 구분한다.
- 2의 보수 체계는 한계가 존재한다.
- 0000(2) → 1 0000(2) : carry값을 버린다
- 1000(2) → 1000(2) : 자기 자신으로 돌아오는 문제
- 즉, n비트로 -2^(n)과 2^(n) 수를 동시에 표현할 수 없다.
📌 십육진법
- 한 글자로 0~F의 정보를 표현하므로, 이진수에 지해 더 적은 자릿수로 많은 정보를 표현할 수 있다.
- 15(16)은 수학적 표기 방식이고, 0x15는 코드상 표기 방식이다.
- 이진수 ↔ 십육진수 사이의 변환히 쉽다.
- 16진수는 2^(4)bit가 필요.
- 즉, 십육진수 한 글자가 4비트 이진수로 취급할 수 있다.
- 0001 1010 0010 1011(2) ↔ 1A2B(16)
- 하드웨어와 밀접하게 맞닿아 있는 개발 분야에서는 코드에 십육진수를 직접 쓰는 경우도 많다.
#define BCM2711_PERL_BASE 0xFE000000
2. 0과 1로 문자 표현
📌 문자 집합과 인코딩
- 문자 집합 (character set)
- 컴퓨터가 인식하고 표현할 수 있는 문자의 모음
- 컴퓨터는 문자 집합에 속해 있지 않은 문자를 이해하지 못한다.
- 문자 인코딩 (character encoding)
- 문자 집합에 속한 문자를 0과 1로 변환하는 과정
- 0과 1로 이루어진 결과가 문자 코드가 된다.
- 문자 디코딩 (character decoding)
- 인코딩의 반대 과정, 0과 1을 사람이 이해할 수 있는 문자로 변환하는 과정
📌 아스키 코드 (ASCII code)
- 초창기 문자 집합 중 하나로, 영어 알파벳과 아라비아 숫자, 일부 특수 문자를 포함한다.
- 아스키 코드는 8비트 중 오류 검출을 위한 1비트를 제외한 7비트로 표현한다. (2^(7) = 128개 문자)
- 0~127까지의 숫자가 하나의 아스키 문자와 일대일 대응된다. (code point; 글자에 부여된 고유한 값)
- 8비트의 확장 아스키가 등장했지만, 모든 문자를 표현하는 것이 불가능하다.
📌 EUC-KR
- 한글 인코딩 방식은 영어와 다르다
- 한글은 각 음절 하나하나가 초성, 중성, 종성의 조합으로 구성된다.
- 완성형 인코딩 : 완성된 하나의 글자에 고유한 코드를 부여한다. ('가'는 1, '나'는 2, '다'는 3,...)
- 조합형 인코딩 : 초성, 중성, 종성을 위한 각각의 비트열을 할당하여 글자 코드를 완성
- EUC-KR은 완성형 인코딩 방식이며, 초성/중성/종성 모두 결합된 한글 단어에 2바이트 크기 코드 부여
- 한글은 한 글자에 2바이트 코드가 부여되며, 2,350개 정도의 한글 단어를 표현 가능 (모든 수 표현 불가능)
- EUD-KR 확장 버전인 마이크로소프트의 CP949 또한 한글 전체를 표현하기에 넉넉하지는 않다.
📌 유니코드와 UTF-8
- 유니코드 : 현대 문자를 표현하기 위한 표준 문자 집합
- UTF는 글자에 부여된 값 자체를 인코딩된 값으로 삼지 않는다.
- UTF-8
- 1~4 바이트까지의 인코딩 결과를 만들어낸다.
- 0~007F(16) 1바이트, 0080(16)~17FF(16) 2바이트, 0800(16)~FFFF(16) 3바이트, 10000(16)~10FFFF(16) 4바이트
- 한(D55G), 글(AE00)을 UTF-8로 인코딩하면 3바이트로 표현된다.
start 코드 포인트 | end 코드 포인트 | 1바이트 | 2바이트 | 3바이트 | 4바이트 |
0000 | 007F | 0XXXXXXX | - | - | - |
0080 | 07FF | 110XXXXX | 10XXXXXX | - | - |
0800 | FFFF | 1110XXXX | 10XXXXXX | 10XXXXXX | - |
10000 | 10FFFF | 11110XXX | 10XXXXXX | 10XXXXXX | 10XXXXXX |
3. 소스코드와 명령어
프로그래밍 언어로 만든 소스 코드는 컴퓨터 내부에서 명령어로 변환된다.
📌 고급 언어와 저급 언어
- 고급 언어(high-level programming language)
- 사람이 이해하고 작성하기 쉽게 만들어진 언어 (그나마 C언어가 low-level에 가깝다)
- 컴퓨터는 이해하지 못한다.
- 저급 언어(low-level programming language)
- 컴퓨터가 직접 이해하고 실행할 수 있는 언어
- 고급 언어로 작성된 소스코드는 반드시 저급 언어, 즉 명령어로 변환되어야 함
- 기계어(machine code)와 어셈블리어(assembly language) 두 가지 종류가 있다.
- 기계어 : 0과 1의 명령어 비트로 이루어진 언어
- 어셈블리어 : 기계어로 표현된 명령어를 읽기 편한 형태로 번역한 언어
어셈블리어란 '작성의 대상'일 뿐만 아니라 '관찰의 대상'이 되기도 한다.
프로그램이 어떤 절차로 작동하는지를 가장 근본적인 단계에서 추적할 수 있기 때문이다.
📌 컴파일 언어와 인터프리터 언어
고급 언어가 저급 언어로 변환되는 방식에는 컴파일 방식과 인터프리트 방식이 있다.
1️⃣ 컴파일 언어
- 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어
- 컴파일(compile) : 컴파일 언어로 작성된 소스 코드를 저급 언어로 변환하는 과정
- 컴파일러(compiler) : 컴파일을 수행해주는 도구
- 목적 코드(object code) : 컴파일을 통해 저급 언어로 변환된 코드
- 컴파일러가 소스 코드 내에서 오류를 하나라도 발견하면 해당 소스 코드는 컴파일에 실패한다.
2️⃣ 인터프리터 언어
- 인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어
- 인터프리터(interpreter) : 소스 코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구
- 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없다
- N번째 줄에 오류가 있어도, N-1번째 줄까지는 올바르게 수행된다.
- 컴파일 언어보다 느리다 (한 줄씩 저급 언어로 해석하고 실행하기 때문)
💡 대부분 고급 언어의 경우 칼로 자르듯이 컴파일/인터프리터 언어를 구분하기는 힘들다.
📌 목적 파일 vs 실행 파일
- 목적 파일: 목적 코드로 이루어진 파일
- 실행 파일: 실행 코드로 이루어진 파일
4. 명령어 구조
명령어의 종류와 생김새는 CPU마다 다르기 때문에 공통으로 이해하는 대표 몇 가지만 알아보자.
📌 연산 코드와 오퍼랜드
- 명령어는 연산 코드와 오퍼랜드로 구성되어 있다.
- "더해라" "a와" "b를"
- "빼라" "메모리 10번지의 값과" "메모리 30번지 값"
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 1
...
add eax, edx
...
pop rbp
ret
- 연산 코드(operation code)
- 명령어(CPU)가 수행할 연산 → 연산자
- 크게 4가지로 구성된다.
- 데이터 전송 : MOVE, STORE, LOAD, PUSH, POP, ...
- 산술/논리 연산 : ADD, SUBTRACT, MULTIPLY, DIVIDE, AND, OR, NOT, ...
- 제어 흐름 변경: JUMP, CONDITIONAL JUMP, HALT, CALL, ...
- 입출력 제어 : READ, WRITE, START IO, TEST IO, ...
- 오퍼랜드(operand) :
- 연산에 사용할 데이터, 또는 연산에 사용할 데이터가 저장된 위치 → 피연산자
- 오퍼랜드 필드는 데이터(숫자, 문자 등) 또는 주소(메모리, 레지스터)가 올라올 수 있다.
- 오퍼랜드 필드는 주소 필드라고도 부른다.
- 명령어 안에 하나도 없을 수도, 한 개 혹은 여러 개 있을 수도 있다.
- 오퍼랜드 개수(n)에 따라 "n-주소 명령어"라고 부른다.
- 오퍼랜드 필드에는 연산에 사용할 데이터를 직접 명시하기 보다, 데이터가 저장된 위치를 명시한다.
📌 주소 지정 방식
오퍼랜드 필드에 메모리나 레지스터의 주소를 담는 이유는 명령어 길이의 제약 때문이다.
- 명령어 크기가 16bit, 연산 코드 필드가 4bit인 2-주소 명령어에서 오퍼랜드 필드는 필드당 6bit 밖에 남지 않는다.
- 하나의 오퍼랜드 필드로 표현 가능한 정보 가짓수는 2^(6)개의 제한이 있다.
- 오퍼랜드 필드에 메모리 주소를 담는다면, 데이터 크기는 하나의 메모리 주소에 저장할 수 있는 공간만큼 커진다. 이를 주소 지정 방식(addressing mode)라고 한다.
- 유효 주소(effective address) : 연산의 대상이 되는 데이터가 저장된 위치
- 종류
- 즉시 주소 지정 방식 : 연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시 (크기 제한)
- 직접 주소 지정 방식 : 유효 주소를 직접적으로 명시
- 간접 주소 지정 방식 : 유효 주소의 주소를 오퍼랜드 필드에 명시 (범위 확장, 속도 느림)
- 레지스터 주소 지정 방식 : 연산에 사용할 데이터를 저장한 레지스터를 직접 명시 (크기 제한)
- 레지스터 간접 주소 지정 방식 : 연산에 사용할 데이터를 메모리에 저장하고, 유효 주소를 저장한 레지스터를 명시