작성자 프로필
mouuaw
코드 깎는 다람쥐
2023.05.12

03 명령어

03-1 소스 코드와 명령어

컴퓨터는 명령어를 처리하는 기계입니다. 우리가 작성한 소스코드는 모두 컴퓨터 내부에서 명령어로 변환됩니다.

고급 언어와 저급 언어

컴퓨터는 우리가 작성한 소스코드를 이해할 수 있을까요? 아쉽게도 그렇지 않습니다. 우리가 작성한 프로그래밍 언어는 컴퓨터가 이해하는 언어가 아닌 사람이 이해하고 작성하기 쉽게 만들어진 언어입니다.

이렇게 사람을 위한 언어를 고급 언어라고 합니다. 반대로 컴퓨터가 직접 이해하고 실행할 수 있는 언어를 저급 언어라고 합니다.

저급 언어는 명령어로 이루어져 있습니다. 컴퓨터가 이해하고 실행할 수 있는 언어는 오직 저급 언어뿐입니다. 그래서 고급 언어로 작성된 소스 코드가 실행되려면 반드시 저급 언어, 즉 명령어로 변환되어야 합니다.

저급 언어에는 두 가지 종류가 있습니다. 바로 기계어어셈블리어 입니다.

기계어란 0과 1의 명령어 비트로 이루어진 언어입니다. 컴퓨터는 0과 1로 이루어진 기계어를 이해하고 실행합니다. 하지만 사람은 이 기계어를 이해하기 힘듭니다. 그래서 등장한 저급 언어가 어셈블리어 입니다.

어셈블리어는 우리가 아는 프로그래밍 언어들과는 다르게 생겼습니다. 어셈블리어는 단순히 기계어를 읽기 편하게 만든것에 불과합니다. 이런 언어를 이용해서 복잡한 프로그램을 만들기란 쉽지 않습니다.

그래서 고급 언어가 필요합니다. 그럼 왜 저급 언어를 알아야 할까요?

만약 우리가 하드웨어와 밀접한 분야에서 프로그래밍을 해야할 경우 어셈블리어를 알아야 합니다. 예를 들면 임베디드 개발자, 게임 개발자, 정보보안 분야 등의 개발자는 어셈블리어를 많이 사용합니다.

컴파일 언어와 인터프리터 언어

고급 언어는 저급 언어로 변환되어 실행됩니다. 그렇다면 고급 언어는 어떻게 저급 언어로 변환될까요? 크게 두 가지, 컴파일 방식과 인터프리트 방식이 있습니다.

컴파일 방식으로 작동하는 프로그래밍 언어를 컴파일 언어, 인터프리트 방식으로 작동하는 프로그래밍 언어를 인터프리터 언어라고 하지요.

컴파일 언어

컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 언어입니다.

컴파일러에 의해 코드 전체가 저급 언어로 변환되는 과정을 컴파일이라고 합니다. 그리고 컴파일을 수행해주는 도구를 컴파일러 라고 하죠. 컴파일러는 개발자가 작성한 소스 코드 전체를 쭉 훑어보며 소스 코드에 문법적인 오류는 없는지, 실행 가능한 코드인지, 실행하는데 불필요한 코드는 없는지 등을 따지며 소스 코드를 처음부터 끝까지 저급 언어로 컴파일합니다.

이때 컴파일러가 소스 코드 내에서 오류를 하나라도 발견하면 해당 소스 코드는 컴파일에 실패합니다.

이렇게 컴파일러를 통해 저급 언어로 변환된 코드를 목적 코드라고 합니다.

인터프리터 언어

인터프리터 언어는 인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어입니다. 대표적인 인터프리터 언어로 python이 있습니다.

컴파일 언어는 소스 코드 전체를 저급 언어로 변환하지만, 인터프리터 언어는 소스 코드를 한줄 한줄 차례로 실행합니다. 그리고 소스코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구를 인터프리터라고 하지요.

인터프리터 언어는 컴퓨터와 대화하듯 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없습니다.

컴파일 언어는 소스 코드 내에 오류가 있으면 컴파일이 불가능하지만, 인터프리터 언어는 한 줄씩 실행하기 때문에 N번째 줄에 오류가 있더라도 N-1번째 줄까지는 실행이 됩니다.

일반적으로 인터프리터 언어는 컴파일 언어보다 느립니다. 한줄 한줄씩 처리하기 때문입니다.

컴파일 언어와 인터프리터 언어를 구분할 수 있을까?

현대의 많은 프로그래밍 언어는 컴파일 언어와 인터프리터 언어 간의 경계가 모호한 경우가 많습니다. python의 경우도 컴파일 과정이 있으며, java의 경우도 컴파일과 인터프리트를 동시에 수행합니다.

즉 하나의 프로그래밍 언어가 반드시 둘 중 하나의 방식으로만 작동하지 않습니다.

03-2 명령어의 구조

연산 코드와 오퍼랜드

컴퓨터의 명령어는 '무엇을 대상으로, 어떤 작동을 수행하라'라는 구조로 되어 있습니다. 예를 들면

더해라 | 100과 | 120을
빼라 | 메모리 32번지 안의 값과 | 메모리 33번지 안의 값을
저장해라 | 10을 | 메모리 128번지에

이런 구조입니다.

명령어는 연산 코드와 오퍼랜드로 구성되어 있습니다. 명령어가 수행할 연산(더해라, 빼라, 저장해라)을 연산 코드라 하고, 연산에 사용할 데이터 또는 연산에 사용할 데이터가 저장된 위치오퍼랜드라고 합니다.

연산 코드는 연산자, 오퍼랜드는 피연산자라고도 부릅니다. 명령어 안에 연산 코드가 담기는 영역을 연산 코드 필드라고 부르고, 오퍼랜드가 담기는 영역을 오퍼랜드 필드라고 합니다.

오퍼랜드

오퍼랜드는 연산에 사용할 데이터 또는 연산에 사용할 데이터가 저장된 위치를 의미한다고 했습니다. 그래서 오퍼랜드 필드에는 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올 수 있습니다. 다만 오퍼랜드 필드에는 숫자나 문자와 같이 연산에 사용할 데이터를 직접 명시하기 보다는, 많은 경우 연산에 사용할 데이터가 저장된 위치, 즉 메모리 주소레지스터 이름이 담깁니다. 그래서 오퍼랜드 필드를 주소 필드라고 부르기도 합니다.

오퍼랜드는 명령어 안에 하나도 없을 수도 있고, 한 개만 있을 수도 있고, 여러개가 있을 수도 있습니다.

오퍼랜드가 하나도 없는 명령어를 0-주소 명령어, 하나인 경우 1-주소 명령어, 두개인 경우 2-주소 명령어, 3개인 경우 3-주소 명령어라고 합니다.

연산 코드

연산 코드 종류는 많지만, 기본적인 연산 코드 유형은 크게 네 가지로 나눌 수 있습니다.

1. 데이터 전송

2. 산술/논리 연산

3. 제어 흐름 변경

4. 입출력 제어

이 네 가지 유형 각각에 해당하는 대표적인 연산 코드를 알아봅시다.

데이터 전송
move: 데이터를 옮겨라
store: 메모리에 저장하라
load(fetch): 메모리에서 cpu로 데이터를 가져와라
push: 스택에 데이터를 저장하라
pop: 스택의 최상단 데이터를 가져와라
산술/논리 연산
add / substract / 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: 입출력 장치의 상태를 확인하라

주소 지정 방식

앞서 오퍼랜드 필드에 주소를 담는 경우가 있다고 했습니다. 왜 값을 저장하지 않고 주소값을 저장할까요? 이는 명령어의 길이 때문입니다.

하나의 명령어가 n비트로 구성되어 있고, 그중 연산 코드 필드가 m비트라고 가정하면, 1-주소 명령어라 할지라도 사용할 수 있는 오퍼랜드 필드의 크기는 연산 코드만큼의 길이를 뺀 n-m 비트가 됩니다.

가령 명령어의 크기가 16비트, 연산 코드 필드가 4비트인 2-주소 명령어에서는 오퍼랜드 필드당 6비트 정도밖에 남지 않습니다. 즉, 하나의 오퍼랜드 필드로 표현할 수 있는 정보의 가짓수는 2^6개밖에 되지 않습니다.

하지만 오퍼랜드 필드 안에 메모리 주소가 담긴다면 표현할 수 있는 데이터의 크기는 하나의 메모리 주소에 저장할 수 있는만큼 커집니다. 오퍼랜드 필드에 메모리 주소가 아닌 레지스터 이름을 명시할 때도 마찬가지입니다. 이 경우 표현할수 있는 정보의 가짓수는 해당 레지스터가 저장할 수 있는 공간만큼 커집니다.

연산 코드에 사용할 데이터가 저장된 위치, 즉 연산의 대상이 되는 데이터가 저장된 위치를 유효 주소(effective address)라고 합니다. 이렇게 오퍼랜드 필드에 데이터가 저장된 위치를 명시할 때 연산에 사용할 데이터 위치를 찾는 방법을 주소 지정 방식이라고 합니다. 다시 말해 주소 지정 방식은 유효 주소를 찾는 방법입니다.

즉시 주소 지정 방식

즉시 주소 지정 방식은 연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시하는 방식입니다. 표현할 수 있는 데이터의 크기가 작아지는 단점이 있지만, 연산에 사용할 데이터를 메모리나 레지스터로부터 찾는 과정이 없기 때문에 다른 주소 지정방식보다 빠릅니다.

직접 주소 지정 방식

직접 주소 지정 방식은 오퍼랜드 필드에 유효 주소를 직접적으로 명시하는 방식입니다. 표현할 수 있는 데이터의 크기는 즉시 주소 지정 방식보다 커졌지만, 유효 주소를 표현할 수 있는 범위가 연산 코드의 비트 수만큼 줄어들었습니다. 다시말해 유효 주소에 제한이 생길 수 있습니다.

간접 주소 지정 방식

간접 주소 지정 방식은 유효 주소의 주소를 오퍼랜드 필드에 명시합니다. 직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 더 넓어졌지만, 두번의 메모리 접근이 필요하기 때문에 앞의 방식들보다 느린 방식입니다.

레지스터 주소 지정 방식

레지스터 주소 지정 방식은 직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방법입니다. 일반적으로 메모리 보다 CPU내부의 레지스터에 접근하는것이 빠르므로 직접 주소 지정 방식보다 빠르게 데이터에 접근할 수 있습니다.

레지스터 간접 주소 지정 방식

레지스터 간접 주소 지정 방식은 연산에 사용할 데이터를 메모리에 저장하고, 그 주소를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법입니다.

유효주소를 찾는 과정이 간접 주소 지정 방식과 비슷하지만, 메모리에 접근하는 횟수가 한 번으로 줄어든다는 장점이 있습니다. 메모리에 접근하는 것보다 레지스터에 접근하는 것이 더 빠르기 때문에 레지스터 간접 주소 지정 방식은 간접 주소 지정 방식보다 빠릅니다.

스택과 큐

스택이란 한쪽 끝이 막힌 통과같은 저장공간 입니다. 막히지 않은 입구로 데이터를 차곡차곡 저장하고, 저장한 자료를 빼낼때는 마지막으로 저장한 데이터부터 빼냅니다. 이런식으로 마지막에 저장한 데이터부터 빼는 방식을 후입선출(Last In First Out) 이라고 합니다.

이때 스택에 새로운 데이터를 저장하는 명령어는 PUSH, 저장된 데이터를 꺼내는 명령어는 POP 입니다. POP을 실행하면 당연히 마지막에 저장한 데이터부터 나오게 됩니다.

큐는 한쪽으로 데이터를 저장하고 다른쪽으로 데이터를 빼내는 구조를 말합니다. 스택과 달리 먼저 저장된 데이터가 먼저 나오기 때문에 선입선출(First In First Out) 방식으로 동작합니다.

스터디 프로필
코드 깎는 다람쥐
의 다른 카테고리
0
👍0
👏0
🤔
댓글 작성