[컴퓨터 구조] 3. 명령어
이번 포스트에서는 컴퓨터 구조의 명령어에 대해 정리하겠습니다.
내용은 "혼자 공부하는 컴퓨터 구조+운영체제" 책을 참고하였습니다!
소스 코드와 명령어
[ 고급 언어와 저급 언어 ]
- 고급 언어 -> 사람이 이해하고 작성하기 쉽게 만들어진 언어. 대부분의 프로그래밍 언어가 고급 언어에 속한다.
- 저급 언어 -> 컴퓨터가 직접 이해하고 실행할 수 있는 언어.
그래서 고급 언어로 작성된 소스 코드가 실행되려면 반드시 저급 언어, 즉 명령어로 반환되어야 한다.
저급 언어에는 두 가지 종류가 있다. 바로 기계어와 어셈블리어이다.
기계어 -> 0과 1의 명령어 비트로 이루어진 언어이다. 다시 말해 기계어는 0과 1로 이루어진 명령어의 모음이다. 아래 그림은 컴퓨터는 0과 1로 이루어진 이 기계어를 이해하고 실행한다. 다만, 기계어를 이진수로 나열하면 너무 길어지기 때문에 가독성을 위해 십육진수로 표현하기도 한다.
어셈블리어 -> 0과 1로 표현된 명령어(기계어)를 읽기 편한 형태로 번역한 언어가 어셈블리어다. 기계어는 오로지 컴퓨터만을 위해 만들어진 언어이기 때문에 사람이 읽으면 그 의미를 이해하기 어렵기 때문이다. 아래 그림의 어셈블리어 한 줄이 명령어이다. 어셈블리어는 0과 1로 이루어진 기계어를 읽기 편하게 만든 저급 언어일 뿐이므로, 개발자가 어셈블리어를 이용해 복잡한 프로그램을 만들기란 쉽지 않다.
[ 컴파일 언어와 인터프리터 언어 ]
- 고급 언어가 컴퓨터가 이해할 수 있는 저급 언어로 변환하는 방식을 크게 컴파일 방식과 인터프리트 방식 두가지로 나눌 수 있다.
[ 컴파일 언어 ]
- 컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어이다. 대표적으로 C언어가 있다.
컴파일 -> 컴파일 언어로 작성된 소스 코드 전체가 저급 언어로 변환되는 과정을 말한다.
컴파일러 -> 컴파일을 수행해 주는 도구를 컴파일러라고 한다. 개발자가 소스 코드 전체를 쭉 훑어보며 소스 코드에 문법적인 오류는 없는지, 실행 가능한 코드인지, 실행하는 데 불필요한 코드는 없는지 등을 따지며 소스 코드를 처음부터 끝까지 저급 언어로 컴파일한다. 이때 컴파일러가 소스 코드 내에서 오류를 하나라도 발결하면 해당 소스 코드는 컴파일에 실패한다.
목적 코드 -> 컴파일이 성공적으로 수행되면 개발자가 작성한 소스 코드는 컴퓨터가 이해할 수 있는 저급 언어로 변환된다. 이렇게 컴파일러를 통해 저급 언어로 변환된 코드를 목적 코드라고 한다.
[ 인터프리터 언어 ]
- 인터프리터 언어는 인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어이다. 대표적으로 python이 있다.
인터프리터 -> 소스 코드 전체가 저급 언어로 변환되는 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 차례로 실행한다. 그리고 소스 코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구이다. 인터프리터 언어는 컴퓨터와 대화하듯 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없다. 그리고 소스 코드 내에 오류가 하나라도 있으면 컴파일이 불가능했던 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 N번째 줄에 문법 오류가 있더라도 N-1번째 줄까지는 올바르게 수행된다.
인터프리터 언어가 컴파일 언어보다 빠르다고 생각할 수도 있지만, 일반적으로 인터프리터 언어는 컴파일 언어보다 느리다. 컴파일을 통해 나온 결과물, 즉 목적 코드는 컴퓨터가 이해하고 실행할 수 있는 저급 언어인 반면, 인터프리터 언어는 소스 코드 마지막에 이를 때까지 한 줄씩 저급 언어로 해석하여 실행해야 하기 때문이다.
목적 파일 -> 목적 코드로 이루어진 파일(이미지로 이루어진 파일은 이미지 파일(.jpg), 텍스트로 이루어진 파일은 텍스트 파일(.txt), 실행 코드로 이루어진 파일은 실행 파일(.exe))
링킹 -> 목적 코드가 실행파일이 되기 위해서 거치는 작업. 컴퓨터가 이해하는 저급 언어로 만들어준다. 즉, main.c의 목적 파일은 main.o가 되는데, 이 저급 언어로 변환된 파일은 목적 파일에 없는 외부 기능들을 연결 짓는 과정이 필요하다. (main.o 자체만으로는 실행 불가능하고 링킹 과정을 거치면 실행 파일이 만들어진다.)
명령어의 구조
[ 연산 코드와 오퍼랜드 ]
- 명령어 -> 연산 코드와 오퍼랜드로 구성되어 있다.
- 연산 코드 -> 명령어가 수행할 연산. 연산자라고도 부른다.
- 오퍼 랜드 -> 연산에 사용할 데이터가 저장된 위치. 피연산자라고도 부른다.
기계어와 어셈블리어 또한 명령어이기 때문에 연산 코드와 오퍼랜드로 구성되어 있다.
[ 오퍼랜드 ]
- 오퍼랜드는 연산에 사용할 데이터 혹은 연산에 사용할 데이터가 저장된 위치를 말한다.
- 그래서 오퍼랜드 필드에는 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올 수 있다.
- 다만, 오퍼랜드 필드에는 수자나 문자와 같이 연산에 사용할 데이터를 직접 명시하기보다는, 연산에 사용할 데이터가 저장된 위치, 즉 메모리 주소나 레지스터 이름이 담긴다. 그래서 주소 필드라고 부른다.
오퍼랜드는 명령어 안에 하나도 없을 수도 있고, 한 개만 있을 수도 있고, 두 개 또는 세 개 등 여러개가 있을 수도 있다.
0-주소 명령어 -> 오퍼랜드가 하나도 없는 명령어
1-주소 명령어 -> 오퍼랜드가 한 개인 명령어
2-주소 명령어 -> 오퍼랜드가 두 개인 명령어
3-주소 명령어 -> 오퍼랜드가 세 개인 명령어
[ 연산 코드 ]
- 연산 코드는 명령어가 수행할 연산을 말한다. ex) '더해라', '빼라'
[ 연산 코드의 유형 ]
- 데이터 전송
- 산술/논리 연산
- 제어 흐름 변경
- 입출력 제어
데이터 전송
- MOVE -> 데이터를 옮겨라
- STORE -> 메모리에 저장하라
- LOAD(FETCH) -> 메모리에서 CPU로 데이터를 가져와라
- PUSH -> 스택에 데이터를 저장하라
- POP -> 스택의 최상단 데이터를 가져와라
산술/논리 연산
- ADD / SUBTRACT / 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 -> 입출력 장치의 상태를 확인하라
주소 지정 방식
- 주소 지정 방식은 연산에 사용할 데이터 위치를 찾는 방법이다. 다시 말해 주소 지정 방식은 유효 주소를 찾는 방법이다. 오퍼랜드 필드에 데이터가 저장된 위치를 명시할 때 연산에 사용할 데이터 위치를 찾는 방법을 말한다.
- 오퍼랜드 필드에 메모리나 레지스터의 주소를 담는 경우가 많은데 명령어의 길이가 길어질 수 있기 때문이다.
- 오퍼랜드 필드 안에 메모리 주소가 담긴다면 표현할 수 있는 데이터의 크기는 하나의 메모리 주소에 저장할 수 있는 공간만큼 커진다.
유효 주소 -> 연산 코드에 사용할 데이터가 저장된 위치, 즉 연산의 대상이 되는 데이터가 저장된 위치
첫 번째 그림의 경우 유효 주소는 10번지, 두 번째 그림의 경우 유효 주소는 레지스터R1이 된다.
[ 즉시 주소 지정 방식 ]
- 즉시 주소 지정 방식(immediate addressing mode)은 연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시하는 방식. 즉, 가장 간단한 형태의 주소 지정 방식이다.
- 앞에서 언급했듯이 이런 방식은 표현할 수 있는 데이터의 크기가 작아지는 단점이 있지만, 연산에 사용할 데이터를 메모리나 레지스터로부터 찾는 과정이 없기 때문에 이하 설명할 주소 지정 방식들보다 빠르다.
[ 직접 주소 지정 방식 ]
- 직접 주소 지정 방식(direct addressing mode)은 오퍼랜드 필드에 유효 주소를 직접적으로 명시하는 방식이다.
- 오퍼랜드 필드에서 표현할 수 있는 데이터의 크기는 즉시 주소 지정 방식보다 더 커졌지만, 여전히 유효 주소를 표현할 수 있는 범위가 연산 코드의 비트 수만큼 줄어들었다.
- 다시 말해 표현할 수 있는 오퍼랜드 필드의 길이가 연산 코드의 길이만큼 짧아져 표현할 수 있는 유효 주소에 제한이 생길 수 있다.
[ 간접 주소 지정 방식 ]
- 간접 주소 지정 방식(indirect addressing mode)은 유효 주소의 주소를 오퍼랜드 필드에 명시한다.
- 직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 넓어진다.
- 다만, 두 번의 메모리 접근이 필요하기 때문에 앞서 설명한 주소 지정 방식들보다 일반적으로 느린 방식이다.
때때로 연산에 사용할 데이터가 레지스터에 저장된 경우도 있다. 이 경우 레지스터 주소 지정 방식 또는 레지스터 간접 주소 지정 방식을 사용할 수 있다.
[ 레지스터 주소 지정 방식 ]
- 레지스터 주소 지정 방식(register addressing mode)은 직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방법이다.
- 일반적으로 CPU 외부에 있는 메모리에 접근하는 것보다 CPU 내부에 있는 레지스터에 접근하는 것이 더 빠르다.
- 그러므로 레지스터 주소 지정 방식은 직접 주소 지정 방식보다 빠르게 데이터에 접근할 수 있다.
- but, 레지스터 주소 지정 방식은 직접 주소 지정 방식과 비슷한 문제를 공유한다. 표현할 수 있는 레지스터 크기에 제한이 생길 수 있다는 점이다.
[ 레지스터 간접 주소 지정 방식 ]
- 레지스터 간접 주소 지정 방식(register indirect addressing mode)은 연산에 사용할 데이터를 메모리에 저장하고, 그 주소(유효 주소)를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법이다.
- 유효 주소를 찾는 과정이 간접 주소 지정 방식과 비슷하지만, 메모리에 접근하는 횟수가 한 번으로 줄어든다는 차이이자 장점이 있다.
- 메모리에 접근하는 것이 레지스터에 접근하는 것보다 더 느리다고 했는데, 그래서 레지스터 간접 주소 지정 방식은 간접 주소 지정 방식보다 빠르다.
정리하면 아래와 같다.
- 즉시 주소 지정 방식 -> 연산에 사용할 데이터
- 직접 주소 지정 방식 -> 유효 주소(메모리 주소)
- 간접 주소 지정 방식 -> 유효 주소의 주소
- 레지스터 주소 지정 방식 -> 유효 주소(레지스터 이름)
- 레지스터 간접 주소 지정 방식 -> 유효 주소를 지정한 레지스터
References
혼자 공부하는 컴퓨터 구조 + 운영체제, 강민철 (2022)