컴퓨터는 Java나 C/C++, Python, JavaScript 등의 언어로 작성된 소스코드를 바로 이해해서 실행하는 것이 아니다.
컴퓨터는 데이터를 재료삼아 명령어를 이해한다.
따라서 소스코드는 실행되기 전에 명령어 + 데이터로 변환되어 실행된다.
소스코드: 사람(개발자)가 이해하기 편한 언어 -> 고급 언어
명령어 & 데이터: 컴퓨터가 이해하기 편한 언어 -> 저급 언어
즉, 고급 언어로 작성된 소스코드는 내부적으로 저급 언어로 구성된 명령어와 데이터로 변환된다.
기계어: 컴퓨터가 직접 이해하는 언어

어셈블리어

C/C++, Java로 작성한 동일한 코드를 기계어나 어셈블리어로 변환했을 때 CPU의 종류나, 컴파일러 종류에 따라 다르게 변환될 수 있다.

(모든 언어가 위와 같은 방식으로 변환되는 것은 아니다)

소스코드 전체가 컴파일러에 의해 검사되고, 목적코드(object code)로 변환된다.
컴파일러의 종류: gcc, clang, Visual Studio 등..
인터프리터에 의해서 소스코드 한 줄씩 검사되고, 목적코드(object code)로 변환된다.
한줄 한줄 인터프리터에 의해서 검사하는 것보다 한번에 쭉 검사해서 기계어로 바꾼다면 소스코드 실행 시 컴퓨터가 직접적으로 이해할 수 있기에 더 빠르다.
즉, 소스코드가 컴파일이 된 상태라면 컴파일 방식이 빠르다.
하지만 컴파일러는 n번째 줄에 오류가 있다면 처음부터 실행이 되지 않으며, '컴파일 에러'가 발생한다.
컴파일 방식과 인터프리트 방식은 소스코드가 저급 언어로 변환되는 대표적인 방식일 뿐 딱 떨어지게 구분되는 개념은 아니다.
컴파일 언어의 특성과 인터프리트 언어의 특성을 모두 갖춘 언어도 있다. (ex. Java, Kotlin 등..)
프로그램을 이루는 두 정보(0과 1로 이루어진 정보)는 두 가지로 구성되어 있다.
즉, 오퍼랜드로 연산 코드를 수행해라.
| 명령의 동작 | 명령의 대상 | 명령의 대상 |
|---|---|---|
| 더해라 | 100과 | 120을 |
| 빼라 | 메모리 32번지 값과 | 메모리 33번지 값을 |
| 저장해라 | 무엇을 | 메모리 128번지에 |
| 출력해라 | 무엇을 | 모니터에 |
연산코드(Op-code)
연산코드(Op-code) | 피연산자(Operand)
연산코드(Op-code) | 피연산자(Operand) | 피연산자(Operand)
연산코드(Op-code) | 피연산자(Operand) | 피연산자(Operand) | 피연산자(Operand)
피연산자(Operand)의 갯수는 유동적일 수 있다.
| 연산코드(Op-code) | 피연산자(Operand) | 피연산자(Operand) |
|---|---|---|
| 옮겨라 | A를 | R1으로 |
| 더해라 | B를 | R1과 |
| 곱해라 | C를 | R1과 |
| 옮겨라 | R1을 | X로 |
| 연산코드(Op-code) | 피연산자(Operand) | 피연산자(Operand) | 피연산자(Operand) |
|---|---|---|---|
| 더해라 | 결과 R1에 저장 | A | B |
| 곱해라 | 결과 X에 저장 | R1 | C |
이처럼 명령어에서 사용되는 Operand가 몇 개 있는지에 따라서, CPU가 얼마나 복잡한 명령어를 지원하는 지에 따라서 명령어의 갯수가 달라질 수 있다.
연산코드의 종류는 CPU마다 다를 수 있다. CPU의 종류와 관계없이 대표적으로, 공통적으로 사용되는 연산코드의 종류는 정해져 있다.
| 연산코드 | 설명 |
|---|---|
| MOVE | 데이터를 옮겨라(레지스터에서 레지스터로 ~ ) |
| STORE | 메모리에 저장해라 |
| LOAD(FETCH) | 메모리에서 가져와라(CPU 내부의 레지스터로 ~ ) |
| PUSH | 스택 최상단에 데이터를 저장해라 |
| POP | 스택 최상단의 데이터를 가져와라 |
| 연산코드 | 설명 |
|---|---|
| ADD / SUBTRACT / MULTIPLY / DIVIDE | 덧셈 / 뺄셈 / 곱셈 / 나눗셈을 수행해라 |
| INCREMENT / DECREMENT | 1 증가 / 감소 시켜라 |
| ADD / OR / NOT | AND / OR / NOT 연산을 수행해라 |
| COMPARE | 두 숫자 또는 TRUE / FALSE 값을 비교해라 |
| 연산코드 | 설명 |
|---|---|
| JUMP | 특정 주소로 실행 순서를 옮겨라(ex. JUMP 메모리 주소) |
| CONDITIONAL JUMP | 조건에 부합할 경우 특정 주소로 실행 순서를 옮겨라 |
| HALT | 프로그램 실행을 멈춰라 |
| CALL | 되돌아올 주소를 저장한 채 특정 주소로 실행 순서를 옯겨라(함수호출, 반환 시 사용됨) |
| RETURN | CALL 호출 시 지정했던 주소로 돌아가라(함수호출, 반환 시 사용됨) |
| 연산코드 | 설명 |
|---|---|
| READ(INPUT) | 특정 입출력 장치로부터 데이터를 읽어라 |
| WRITE(OUTPUT) | 특정 입출력 장치로 데이터를 써라 |
| START IO | 입출력 장치를 시작해라 |
| TEST IO | 입출력 장치의 상태를 확인해라 |
주소지정이란 명령어의 연산코드의 대상이 되는 데이터를 찾아가는 방법이다.
주소지정은 CPU마다 조금씩 차이가 있다. 그리고 주소지정에는 다양한 방식이 있다.
오퍼랜드(Operand): 명령어를 수행할 대상
오퍼랜드가 담기는 오퍼랜드 필드에는 연산의 대상(데이터)이 직접 명시되기도 하고, 대상의 위치(레지스터 이름, 메모리 주소)가 명시되기도 한다.
Q) 왜 데이터를 직접 명시하지 않고 위치를 명시하는 것일까?
연산코드 오퍼랜드1 오퍼랜드2 더해라 100과 120을 빼라 메모리 32번지 값과 메모리 33번지 값을 저장해라 10을 메모리 128번지에
A) 명령어의 길이는 한정되어 있기 때문에
명령어는 연산코드와 오퍼랜드로 구성되어 있고, 오퍼랜드는 여러개 있을 수 있다.
이 때, 명령어의 총 길이가 한정되어 있다면, 연산코드를 명시하기 위한 공간과 오퍼랜드를 명시하기 위한 공간이 한정되어있게 된다.
예를 들어 명령어의 길이가 16bit라고 가정한다면
오퍼랜드가 2개인 경우(2-주소 명령어)
연산코드(4bit) | 오퍼랜드(6bit) | 오퍼랜드(6bit)
==> 하나의 오퍼랜드 필드로 표현할 수 있는 데이터 크기: 2^6(64)
오퍼랜드가 3개인 경우(3-주소 명령어)
연산코드(4bit) | 오퍼랜드(4bit) | 오퍼랜드(4bit) | 오퍼랜드(4bit)
==> 하나의 오퍼랜드 필드로 표현할 수 있는 데이터 크기: 2^4(16)
하나의 오퍼랜드에
2500이라는 데이터를 표현해야 한다면, 이를 직접 명시할 수는 없다.
최대 표현가능한 데이터가 16밖에 안되니까
위와 같은 문제를 해결하기 위해 오퍼랜드 필드에 연산코드의 대상이 되는 데이터를 직접 명시하기 보다는,
데이터가 저장되어 있는 공간(레지스터 이름, 메모리 주소)을 명시
메모리의 경우(유효주소 = 10번지)

레지스터의 경우(유효주소 = R1)

유효주소: 연산코드에 사용할 데이터가 저장된 위치. 즉, 연산의 대상이 되는 데이터가 저장된 위치
주소지정: 유효주소를 찾는 방법
연산코드 | 연산코드에 사용될 데이터
특징)

특징)
CPU가 레지스터에 접근하는 속도보다 메모리에 접근하는 속도가 훨씬 느리다.
레지스터에 접근함으로써 처리할 수 있는 작업은 레지스터에 접근해서 처리하는 것이 훨씬 더 빠른 성능을 보장한다.

특징)

특징)

특징)