컴퓨터는 명령어를 처리하는 기계
그렇다면 C, C++, Java, Python과 같은 프로그래밍 언어로 만든 소스 코드는 뭘까?
이러한 프로그래밍 언어를 컴퓨터가 이해할 수 있을까?
정답은 NO!!
모든 소스코드는 컴퓨터 내부에서 명령어로 변환된다
프로그래밍 언어는 컴퓨터가 이해하는 언어가 아닌 사람이 이해하고 작성하기 쉽게 만들어진 언어이다
저급 언어는 명령어로 이루어져 있으며, 컴퓨터가 이해하고 실행할 수 있는 언어는 오직 저급 언어 뿐이다
고급 언어로 작성된 소스 코드가 실행되기 위해선 반드시 저급 언어인 명령어로 변환되어야 한다
기계어 : 0과 1의 명령어 비트로 이루어진 언어
→ 0과 1로 이루어진 명령어 모음, 오직 컴퓨터만을 위해 만들어진 언어이기 때문에 사람이 읽으면 의미를 알 수 없다
어셈블리어 : 0과 1로 표현된 명령어(기계어)를 읽기 편한 형태로 번역한 언어
→ 0과 1로 이루어진 기계어를 읽기 편하게 만든 저급 언어이기 때문에 개발자가 이를 이용하여 복잡한 프로그램을 만드는 것은 쉽지 않다
그래서 고급 언어가 필요한 것!!
- 사람이 읽고 쓰기 편함
- 가독성이 좋음
- 변수나 함수 같은 편리한 문법 제공
➡️ 복잡한 프로그램 구현 가능
하드웨어와 밀접하게 맞닿아 있는 프로그램을 개발하는 임베디드 개발자, 게임 개발자, 정보 보안 분야 등의 개발자는 어셈블리어를 많이 이용한다
→ 어셈블리어가 '작성의 대상'일 뿐만 아니라 매우 중요한 '관찰의 대상'이기도 함
→ 컴퓨터가 프로그램을 어떤 과정으로 실행하는지, 프로그램이 어떤 절차로 작동하는지를 가장 근본적인 단계부터 추적 관찰이 가능하다
고급 언어로 작성한 소스 코드는 결국 저급 언어로 변환된다
변환 방식은 크게 두 가지, 컴파일 방식과 인터프리트 방식이 있다
컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어 (대표적인 컴파일 언어: C)
작성된 소스 코드 전체가 저급 언어로 변환되는 과정을 거친다 = 컴파일
위와 같은 내용들을 체크하며 소스코드를 처음부터 끝까지 저급 언어로 컴파일한다
➡️ 컴파일러를 통해 저급 언어로 변환된 코드를 목적 코드라고 한다
인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어 (대표적인 인터프리터 언어: Python)
소스 코드를 한 줄씩 한 줄씩 차례로 실행된다
➡️ 인터프리터가 소스 코드를 한 줄씩 저급 언어로 변환하여 실행해줌
소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없다
컴파일 언어 | 인터프리터 언어 |
---|---|
소스코드 컴파일 중 오류가 발생하면 소스 코드 전체가 실행되지 않는다 | 소스 코드 인터프리트 중 오류가 발생하면 오류 발생 전까지의 코드는 실행된다 |
인터프리터 언어가 더 빠를 것 같지만 일반적으로 인터프리터 언어가 컴파일 언어보다 느리다
→ 컴파일을 통해 나온 결과물, 즉 목적 코드는 컴퓨터가 이해하고 실행할 수 있는 저급 언어이지만
→ 인터프리터 언어는 소스 코드 마지막에 이를 때까지 한 줄씩 저급 언어로 해석하며 실행해야 하기 때문이다
컴파일 언어와 인터프리터 언어는 명확하게 구분할 수 있는 개념이 아니다
✔️ C, C++과 같이 명확하게 구분할 수 있는 언어도 있지만, 현대의 많은 프로그래밍 언어 중에는 컴파일 언어와 인터프리터 언어 간의 경계가 모호한 경우가 많다
때문에 고급 언어가 저급 언어로 변환되는 대표적인 방법에는 컴파일 방식과 인터프리트 방식이 있다 정도로 이해하는 것이 좋음
목적 코드로 이루어진 파일 : 목적 파일
실행 코드로 이루어진 파일 : 실행 파일 (.exe
)
➡️ 목적 코드가 실행 파일이 되기 위해서는 링킹이라는 작업을 거쳐야 한다
명령어의 구조
→ '무엇을 대상으로 어떤 작동을 수행하라'
연산 코드 | 오퍼 랜드 |
---|---|
더해라 | 100과 \ 120을 |
저장해라 | 10을 \ 메모리 128번지에 |
작동 | 데이터 또는 위치 |
연산 코드 - 명령어가 수행할 연산 (연산자)
오퍼랜드 - 연산에 사용할 데이터 또는 연산에 사용될 데이터가 저장된 위치 (피연산자)
연산 코드가 담기는 영역을 연산 코드 필드라고 부르고, 오퍼랜드가 담기는 부분을 오퍼랜드 필드라고 부른다
연산에 사용될 데이터 또는 연산에 사용할 데이터가 저자오딘 위치
→ 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올 수 있다
하지만 연산에 사용할 데이터를 직접 명시하기 보다 많은 경우에 연산에 사용할 데이터가 저장된 위치, 즉 메모리 주소나 레지스터 이름이 담긴다 (주소 필드라고 부르기도 한다)
mov eax, 0 // → 오퍼랜드가 두 개인 경우
pop rbp // → 오퍼랜드가 한 개인 경우
ret // → 오퍼랜드가 없는 경우
0-주소 명령어
1-주소 명령어
2-주소 명령어
3-주소 명령어
명령어가 수행할 연산
많은 종류가 있지만 크게 네 가지 유형으로 나눌 수 있다
명령어의 종류와 생김새는 CPU마다 다르기 때문에 연산 코드의 종류와 생김새 또한 다 다르다
MOVE, STORE, LOAD(FETCH), PUSH, POP
PUSH
- 스택에 데이터를 저장하라POP
- 스택의 최상단 데이터를 가져와라 ADD, SUBSTRACT, MULTIPLY, DIVIDE
INCREMENT, DECREMENT
- 오퍼랜드에 1을 더하라/빼라AND, OR, NOT
COMPARE
JUMP, CONDITIONAL JUMP
- (특정 주소로) 실행 순서를 옮겨라HALT
- 프로그램의 실행을 멈춰라CALL
- 되돌아올 주소를 저장한 채 특정 주소로 실행 순서를 옮겨라 (함수 호출과 비슷)RETURN
- CALL을 호출할 때 저장했던 주소로 돌아가라 (함수 리턴)READ(INPUT)
WRITE(OUTPUT)
START IO
- 입출력 장치를 시작하라TEST IO
- 입출력 장치의 상태를 확인하라 오퍼랜드 필드에 메모리나 레지스터의 주소를 담는 이유
→ 명령어 길이 때문
하나의 명령어가 n 비트로 구서오디어 있고, 연산 코드 필드가 m 비트라고 가정한다면
오퍼랜드 필드의 길이는 연산 코드의 길이를 뺀 n-m 비트가 된다
→ 2-주소 명령어, 3-주소 명령어는 오퍼랜드 필드의 크기가 더욱 작아질 수밖에 없음
명령어의 크기가 16비트, 연산 코드 필드가 4비트인 2-주소 명령어에서는 오퍼랜드 필드 당 6비트 정도밖에 남지 않는다
→ 하나의 오퍼랜드 필드로 표현할 수 있는 정보의 가짓수가 2⁶개 밖에 되지 않는 것임
연산 코드 | 오퍼랜드 | 오퍼랜드 |
---|---|---|
16비트 ~ 12 비트 | 12비트 ~ 6비트 | 6비트 ~ 0비트 |
➡️ 오퍼랜드 필드 안에 메모리 주소가 담긴다면 표현할 수 있는 데이터의 크기는 하나의 메모리 주소에 저장할 수 있는 공간만큼 커진다!
연산 코드에 사용할 데이터가 저장된 위치, 즉 연산의 대상이 되는 데이터가 저장된 위치를 유효 주소라고 부른다
오퍼랜등 필드에 데이터가 저장된 위치를 명시할 때 연산에 사용할 데이터 위치를 찾는 방법을 주소 지정 방식이라고 한다 (=유효 주소를 찾는 방법)
연산에 사용할 데이털를 오퍼랜드 필드에 직접 명시하는 방식
→ 표현할 수 있는 데이터의 크기가 작아지는 단점
→ 연산에 사용할 데이터를 메모리나 레지스터로부터 찾는 과정이 없기 때문에 이하 설명할 주소 지정 방식들보다 빠르다
오퍼랜드 필드에 유효 주소를 직접적으로 명시하는 방식
→ 표현할 수 있는 데이터의 크기가 즉시 주소 지정 방식보다는 커짐
→ 여전히 유효 주소를 표현할 수 있는 범위가 연산 코드의 비트 수만큼 줄어든다 = 표현할 수 있는 오퍼랜드 필드의 길이가 연산 코드의 길이만큼 짧아져서 표현할 수 있는 유효 주소에 제한이 생길 수 있다
유효 주소의 주소를 오퍼랜드 필드에 명시
→ 직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 넓어짐
→ 하지만 두 번의 메모리 접근이 필요하여 앞선 방식들보다 일반적으로 느린 방식
연산에 사용할 데이터가 레지스터에 저장된 경우
직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방법
→ 일반적으로 CPU 외부에 있는 메모리에 접근하는 것보다 CPU 내부에 있는 레지스터에 접근하는 것이 더 빠르다 = 레지스터 주소 지정 방식은 직접 주소 지정 방식보다 빠르게 데이터에 접근 가능
→ 직접 주소 지정 방식과 비슷한 문제, 표현할 수 있는 레지스터 크기에 제한이 생길 수 있다
연산에 사용할 데이터를 메모리에 저장하고 그 주소(유효 주소)를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법
→ 유효 주소를 찾는 과정이 간접 주소 지정 방식과 비슷하지만 메모리에 접근하는 횟수가 한 번으로 줄어든다는 차이이자 장점이 있다
➡️ 메모리에 접근하는 것이 레지스터에 접근하는 것보다 느리다 = 레지스터 간접 주소 지정 방식이 간접 주소 지정 방식보다 빠르다
정리
- 연산에 사용할 데이터를 찾는 방법 = 주소 지정 방식
- 연산에 사용할 데이터가 저장된 위치 = 유효 주소