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