소스 코드와 명령어
‘컴퓨터는 명령어를 처리하는 기계’ 명령어는 컴퓨터를 실질적으로 작동시키는 매우 중요한 정보이다.
하지만 컴퓨터를 작동시키는 정보가 명령어라면 C, C++, Java, Python과 같은 프로그래밍 언어로 만든 소스 코드는 무엇일까요?
결론적으로는 모든 소스 코드는 ‘컴파일러’ 라는 것을 통해 컴퓨터 내부에서 명령어로 변환되어 실행됩니다.
고급 언어와 저급 언어
컴퓨터는 C, C++, Java, Python 과 같은 프로그래밍 언어를 이해할 수 있을까? 답은 “No!”
우리가 프로그램을 만들 때 사용하는 프로그래밍 언어는 컴퓨터가 이해하는 언어가 아닌 사람이 이해하고 작성하기 쉽게 만들어진 언어입니다. 컴퓨터는 이 언어를 이해하지 못합니다. 이렇게 ‘사람을 위한 언어’를 고급 언어(high-level programming language) 라고 합니다.
우리가 흔히 사용하는 대부분의 프로그래밍 언어는 모두 고급 언어에 속합니다.
반대로 컴퓨터가 직접 이해하고 실행할 수 있는 언어를 저급 언어(low-level programming language) 라고 합니다.
컴퓨터가 이해하고 실행할 수 있는 언어는 오직 저급 언어뿐입니다. 그래서 고급 언어로 작성된 소스 코드가 실행되려면 반드시 저급 언어, 즉 명령어로 변환되어야 합니다.
저급 언어는 두 가지 종류가 있습니다.
기계어(machine code) : 0과 1의 명령어 비트로 이루어진 저급 언어입니다. 즉, 기계어는 0과 1로 이루어진 명령어 모음입니다.
기계어를 이진수로 나열하면 너무 길어지기 때문에 가독성을 위해 아래와 같이 16진수로 표현하기도 합니다.
하지만 기계어는 오로지 컴퓨터만을 위해 만들어진 언어이기 때문에 사람이 읽으면 그 의미를 이해하기 어렵습니다. 그래서 등장한 저급 언어가 어셈블리어(assembly language)입니다.
즉 0과 1로 표현된 명령어(기계어)를 읽기 편한 형태로 번역한 언어가 어셈블리어입니다.
어셈블리어(assembly language) : 0과 1로 이루어진 기계어를 읽기 편한 형태로 번역한 저급 언어
어셈블리어 한 줄 한 줄이 명령어이다.
프로그래밍 언어와 어셈블리어는 우리가 아는 C, C++, Java, Python과는 사뭇 다르게 생겼다는 사실을 아실 겁니다. 0과 1로 이루어진 기계어를 읽기 편하게 만든 어셈블리어라고 할 지라도 저급 언어 이므로 여전히 우리가 보기 어렵고 어셈블리어를 이용해 복잡한 프로그램을 만들기란 쉽지 않습니다.
그래서 우리는 고급 언어와 같이 사람이 쉽게 이해 할 수 있도록 만들어진 언어를 사용하는 것입니다. 하지만 하드웨어와 밀접하게 맞닿아 있는 프로그램을 개발하는 임베디드 개발자, 게임 개발자, 정보 보안 분야 등의 개발자는 어셈블리어를 많이 사용합니다.
개발자들에게 어셈블리어란 ‘작성의 대상’일 뿐만 아니라 매우 중요한 ‘관찰의 대상’ 이기도 합니다. 어셈블리어를 읽으면 컴퓨터가 프로그램을 어떤 과정으로 실행하는지, 즉 프로그램이 어떤 절차로 작동하는지를 가장 근본적인 단계에서부터 하나하나 추적하고 관찰할 수 있습니다.
컴파일 언어와 인터프리터 언어
개발자들이 고급 언어로 작성한 소스 코드는 결국 저급 언어로 변환되어 실행되는데 고급 언어는 어떻게 저급 언어로 변환되는지에 대하여 알아봅시다.
고급 언어 → 저급 언어 로 변환되는 방법은 크게 두 가지가 있습니다.
컴파일 언어
컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어입니다. 대표적인 컴파일 언어로는 ‘C’ 가 있습니다.
컴퓨터는 고급 언어를 이해 하지 못합니다. 그렇기에 컴파일 언어로 작성된 소스 코드를 저급 언어로 변환하는 과정을 거칩니다. 이 과정을 컴파일(compile) 이라고 합니다.
그리고 컴파일을 수행해 주는 도구를 **컴파일러(compiler)**라고 합니다.
컴파일러는 개발자가 작성한 소스 코드 전체에 문법적인 오류, 실행 가능한 코드인지, 실행하는데 불필요한 코드는 없는지(주석)을 따지며 소스 코드를 처음부터 끝가지 저급 언어로 컴파일합니다.
이때 소스 코드 내에서 오류를 하나라도 발견하면 해당 소스 코드는 컴파일에 실패하게됩니다.
컴파일이 성공적으로 수행되면 개발자가 작성한 소스 코드는 컴퓨터가 이해할 수 있는 저급 언어로 변환됩니다. 컴파일러를 통해 저급 언어로 변환된 코드를 목적 코드(object code) 라고 합니다.
인터프리터 언어
인터프리터 언어는 인터프리어에 의해 소스 코드가 한 줄씩 실행되는 고급 언어입니다. 대표적인 인터프리터 언어로 Python이 있습니다.
소스 코드 전체가 저급 언어로 변환되는 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 한 줄씩 차례로 실행합니다.
소스 코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구를 인터프리터(interpreter)라고 합니다.
인터프리터 언어는 컴퓨터와 대화하듯 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없습니다.
그리고 소스 코드 내에 오류가 하나라도 있으면 컴파일이 불가능했던 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 N번째 줄에 문법 오류가 있더라도
N-1 번째 줄까지는 올바르게 수행이 됩니다.
일반적으로 인터프리터 언어는 컴파일 언어보다 느립니다. 즉 목적 코드는 컴퓨터가 이해하고 실행할 수 있는 저급 언어인 반면, 인터프리터 언어는 소스 코드 마지막에 이를 때까지 한 줄 한 줄씩 저급 언어로 해석하며 실행해야 하기 때문입니다.
컴파일 언어와 인터프리터 언어, 칼로 자르듯이 구분이 될까?
C, C++과 같이 명확하게 구분할 수 있는 언어도 있으나, 현대의 많은 프로그래밍 언어 중에는 컴파일 언어와 인터프리터 언어 간의 경계가 모호한 경우가 많습니다.
가령 대표적인 인터프리터 언어로 알려진 Python도 컴파일을 하지 않는 것은 아니며, Java의 경우 저급 언어가 되는 과정에서 컴파일과 인터프리터를 동시에 수행합니다.
결론적으로는 하나의 프로그래밍 언어가 반드시 둘 중 하나의 방식만으로만 작동한다고 생각하는 것은 오개념입니다. 컴파일 언어와 인터프리터 언어로 칼로 자르듯 구분하기보다는 ‘고급 언어가 저급 언어로 변환되는 대표적인 방법에는 컴파일 방식과 인터프리터 방식이 있다’ 정도로만 이해하는 것이 좋습니다.
핵심 포인트
- 고급 언어는 사람이 이해하고 작성하기 쉽게 만들어진 언어입니다.
- 저급 언어는 컴퓨터가 직접 이해하고 실행할 수 있는 언어입니다.
- 저급 언어는 0과 1로 이루어진 명령어로 구성된 기계어와 기계어를 사람이 읽기 편한 형태로 번역한 어셈블리어가 있습니다.
- 컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 언어입니다.
- 인터프리터 언어는 인터프리터에 의해 소스 코드가 한 줄씩 저급 언어로 변환되어 실행되는 언어 입니다.
Quiz
답
3, 4번
1 , 2번은 고급 언어이다.
답
2번
인터프리터 언어는 소스 코드 마지막에 이를 때까지 한 줄 한 줄씩 저급 언어로 해석하며 실행해야 되기때문에 더욱 느리다.
명령어의 구조
저급 언어의 명령어들의 구조를 한번 살펴보는 챕터입니다.
연산 코드와 오퍼랜드
컴퓨터 속 명령어도 결국에는 ‘무엇을 대상으로, 어떤 작동을 수행하라’ 라는 명령 이라는 구조로 되어 있습니다.
위의 사진에서 색 배경 필드에는 명령의 ‘작동/연산’ , 흰색 배경 필드에는 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 담고 있습니다.
연산 코드(operation code) : 명령어가 수행할 연산
오퍼랜드(operand) : 연산에 사용할 데이터가 저장된 위치
연산 코드는 연산자, 오퍼랜드는 피연산자라고도 부른다.
위의 그림에서 색칠된 부분(연산 코드가 담기는 영역)을 연산 코드 필드라고 부르고,
색칠되지 않은 부분(오퍼랜드가 담기는 영역)을 오퍼랜드 필드라고 합니다.
기계어와 어셈블리어 또한 명령어이기 때문에 연산 코드와 오퍼랜드로 구성되어져 있습니다.
붉은 글씨가 연산 코드(연산자), 검은 글씨가 오퍼랜드(피연산자)입니다.
오퍼랜드
오퍼랜드는 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 의미합니다.
그래서 오퍼랜드 필드에는 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올 수 있습니다. 대체적으로 오퍼랜드 필드에는 숫자나 문자와 같이 연산에 사용할 데이터를 직접적으로 오기 보다는 연산에 사용할 데이터가 저장된 위치(메모리 주소/레지스터 이름)이 담깁니다.
그래서 오퍼랜드 필드를 주소 필드라고 부르기도 합니다.
오퍼랜드는 명령어 안에 하나도 없을 수도 있고, 한 개만 있을 수도 있고, 두 개 또는 세 개 등 여러 개가 있을 수도 있습니다.
오퍼랜드가 하나도 없는 명령어를 0-주소 명령어
오퍼랜드가 하나인 명령어를 1-주소 명령어, 두 개인 명령어를 2-주소 명령어, 세 개인 명령어를 3-주소 명령어 라고 합니다.
연산 코드
연산 코드는 ‘명령어가 수행할 연산을 의미’ = ‘더해라’, ‘빼라’, ‘저장해라’에 해당하는 부분
연산 코드 종류는 매우 많지만, 가장 기본적인 연산 코드 유형은 크게 네 가지로 나눌 수 있습니다.
- 데이터 전송
- 산술/논리 연산
- 제어 흐름 변경
- 입출력 제어
데이터 전송
- MOVE : 데이터를 옮겨라
- STORE : 메모리에 저장하라
- LOAD(FETCH) : 메모리에서 CPU로 데이터를 가져와라
- PUSH : 스택에 데이터를 저장하라
- POP : 스택의 최상단 데이터를 가져와라
산술/논리 연산
- ADD / STBTRACT / 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 비트가 됩니다.
2-주소 명령어, 3-주소 명령어 라면 오퍼랜드 필드의 크기는 더욱 작아집니다.
명령어의 크기가 16비트, 연산 코드 필드가 4비트인 2-주소 명령어에서는 오퍼랜드 필드당 6비트 정도밖에 남지 않습니다. 즉, 하나의 오퍼랜드 필드로 표현할 수 있는 정보의 가짓수는 262^6 개 밖에 되지 않습니다.
만약 3-주소 명령이 일 경우에는 오퍼랜드 필드당 4비트 정도밖에 남지 않아 정보의 가짓수가 $ 2^4$개 정도일것이다.
하지만 만약 오퍼랜드 필드 안에 메모리 주소가 담긴다면 표현할 수 있는 데이터의 크기는 하나의 메모리 주소에 저장할 수 있는 공간만큼 커질것입니다.
한 주소에 16비트를 저장할 수 있는 메모리가 있다고 가정을 하면 이 메모리 안에 데이터를 저장하고, 오퍼랜드 필드 안에 해당 메모리 주소를 명시한다면 표현할 수 있는 정보의 가짓수가 2162^{16} 개로 확 늘어날 것이다.
만약 레지스터 이름을 명시할 때도 표현할 수 있는 정보의 가짓수는 해당 레지스터가 저장할 수 있는 공간만큼 커질것입니다.
연산 코드에 사용할 데이터가 저장된 위치, 즉 연산의 대상이 되는 데이터가 저장된 위치를 유효 주소(effective address)라고 합니다.
위의 그림에서 유효 주소는 10번지 가 됩니다.
이렇게 오퍼랜드 필드에 데이터가 저장된 위치를 명시할 때 연산에 사용할 데이터 위치를 찾는 방법을 주소 지정 방식(addressing mode) 라고 합니다. 즉 주소 지정 방식은 유효 주소를 찾는 방법입니다.
즉시 주소 지정 방식
즉시 주소 지정 방식(immediate addressing mode)는 연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시하는 방법
명령어에 저장할 수 있는 데이터의 크기가 작아지는 단점이 있지만, 연산에 사용할 데이터를 메모리나 레지스터로부터 찾는 과정이 없기에 주소 지정 방식중에서 가장 빠르다.
직접 주소 지정 방식
직접 주소 지정 방식(direct addressing mode)는 오퍼랜드 필드에 유효 주소를 직접적으로 명시하는 방식
오퍼랜드 필드에서 표현할 수 있는 데이터의 크기는 즉시 주소 지정 방식보다 크지만, 여전히 유효 주소를 표현할 수 있는 범위가 연산 코드의 비트 수만큼 줄어들었습니다.
표현할 수 있는 유효 주소에 제한이 생길 수 있습니다.
간접 주소 지정 방식
간접 주소 지정 방식(indirect addressing mode)는 유효 주소의 주소를 오퍼랜드 필드에 명시합니다.
직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 더 넓지만, 메모리에 두번 접근이 필요하기 때문에 다른 주소 지정 방식들보다 일반적으로 조금 느리다.
레지스터 주소 지정 방식
레지스터 주소 지정 방식(register addressing mode)는 직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방법입니다.
일반적으로 메모리보다 CPU 내부에 있는 레지스터가 훨씬 빨라서 앞선 주소 지정 방식들 중에 2번째로 빠르게 접근할 수 있습니다. 다만, 레지스터 주소 지정 방식은 직접 주소 지정 방식과 비슷한 문제를 공유합니다. 표현 할 수 있는 레지스터 크기에 제한이 생길 수도 있습니다.
레지스터 간접 주소 지정 방식
레지스터 간접 주소 지정 방식(register indirect addressing mode)는 연산에 사용할 데이터를 메모리에 저장하고, 그 주소(유효 주소)를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법입니다.
유효 주소를 찾는 과정이 간접 주소 지정 방식과 비슷하지만, 메모리에 접근하는 횟수가 한 번으로 줄어든다는 차이이자 장점이 있습니다. 레지스터가 메모리보다 훨씬 빠르기 때문에 찾아가는 과정에서의 속도가 간접 주소 지정 방식보다 빠릅니다.
정리
연산에 사용할 데이터를 찾는 방법 : 주소 지정 방식
연산에 사용할 데이터가 저장된 위치 : 유효 주소
각각의 방식에 따라 오퍼랜드 필드에 명시되는 값을 정리하여 봅니다.
- 즉시 주소 지정 방식 : 연산에 사용할 데이터
- 직접 주소 지정 방식 : 유효 주소(메모리 주소)
- 간접 주소 지정 방식 : 유효 주소의 주소
- 레지스터 주소 지정 방식 : 유효 주소(레지스터 이름)
- 레지스터 간접 주소 지정 방식 : 유효 주소를 저장한 레지스터
핵심 키워드
- 명령어는 연산 코드와 오퍼랜드로 구성됩니다.
- 연산 코드는 명령어가 수행할 연산을 의미합니다.
- 오퍼랜드는 연산에 사용할 데이터 또는 연산에 사용할 데이터가 저장된 위치를 의미합니다.
- 주소 지정 방식은 연산에 사용할 데이터 위치를 찾는 방법입니다.
소스 코드와 명령어
‘컴퓨터는 명령어를 처리하는 기계’ 명령어는 컴퓨터를 실질적으로 작동시키는 매우 중요한 정보이다.
하지만 컴퓨터를 작동시키는 정보가 명령어라면 C, C++, Java, Python과 같은 프로그래밍 언어로 만든 소스 코드는 무엇일까요?
결론적으로는 모든 소스 코드는 ‘컴파일러’ 라는 것을 통해 컴퓨터 내부에서 명령어로 변환되어 실행됩니다.
고급 언어와 저급 언어
컴퓨터는 C, C++, Java, Python 과 같은 프로그래밍 언어를 이해할 수 있을까? 답은 “No!”
우리가 프로그램을 만들 때 사용하는 프로그래밍 언어는 컴퓨터가 이해하는 언어가 아닌 사람이 이해하고 작성하기 쉽게 만들어진 언어입니다. 컴퓨터는 이 언어를 이해하지 못합니다. 이렇게 ‘사람을 위한 언어’를 고급 언어(high-level programming language) 라고 합니다.
우리가 흔히 사용하는 대부분의 프로그래밍 언어는 모두 고급 언어에 속합니다.
반대로 컴퓨터가 직접 이해하고 실행할 수 있는 언어를 저급 언어(low-level programming language) 라고 합니다.
컴퓨터가 이해하고 실행할 수 있는 언어는 오직 저급 언어뿐입니다. 그래서 고급 언어로 작성된 소스 코드가 실행되려면 반드시 저급 언어, 즉 명령어로 변환되어야 합니다.
저급 언어는 두 가지 종류가 있습니다.
기계어(machine code) : 0과 1의 명령어 비트로 이루어진 저급 언어입니다. 즉, 기계어는 0과 1로 이루어진 명령어 모음입니다.
기계어를 이진수로 나열하면 너무 길어지기 때문에 가독성을 위해 아래와 같이 16진수로 표현하기도 합니다.
하지만 기계어는 오로지 컴퓨터만을 위해 만들어진 언어이기 때문에 사람이 읽으면 그 의미를 이해하기 어렵습니다. 그래서 등장한 저급 언어가 어셈블리어(assembly language)입니다.
즉 0과 1로 표현된 명령어(기계어)를 읽기 편한 형태로 번역한 언어가 어셈블리어입니다.
어셈블리어(assembly language) : 0과 1로 이루어진 기계어를 읽기 편한 형태로 번역한 저급 언어
어셈블리어 한 줄 한 줄이 명령어이다.
프로그래밍 언어와 어셈블리어는 우리가 아는 C, C++, Java, Python과는 사뭇 다르게 생겼다는 사실을 아실 겁니다. 0과 1로 이루어진 기계어를 읽기 편하게 만든 어셈블리어라고 할 지라도 저급 언어 이므로 여전히 우리가 보기 어렵고 어셈블리어를 이용해 복잡한 프로그램을 만들기란 쉽지 않습니다.
그래서 우리는 고급 언어와 같이 사람이 쉽게 이해 할 수 있도록 만들어진 언어를 사용하는 것입니다. 하지만 하드웨어와 밀접하게 맞닿아 있는 프로그램을 개발하는 임베디드 개발자, 게임 개발자, 정보 보안 분야 등의 개발자는 어셈블리어를 많이 사용합니다.
개발자들에게 어셈블리어란 ‘작성의 대상’일 뿐만 아니라 매우 중요한 ‘관찰의 대상’ 이기도 합니다. 어셈블리어를 읽으면 컴퓨터가 프로그램을 어떤 과정으로 실행하는지, 즉 프로그램이 어떤 절차로 작동하는지를 가장 근본적인 단계에서부터 하나하나 추적하고 관찰할 수 있습니다.
컴파일 언어와 인터프리터 언어
개발자들이 고급 언어로 작성한 소스 코드는 결국 저급 언어로 변환되어 실행되는데 고급 언어는 어떻게 저급 언어로 변환되는지에 대하여 알아봅시다.
고급 언어 → 저급 언어 로 변환되는 방법은 크게 두 가지가 있습니다.
컴파일 언어
컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어입니다. 대표적인 컴파일 언어로는 ‘C’ 가 있습니다.
컴퓨터는 고급 언어를 이해 하지 못합니다. 그렇기에 컴파일 언어로 작성된 소스 코드를 저급 언어로 변환하는 과정을 거칩니다. 이 과정을 컴파일(compile) 이라고 합니다.
그리고 컴파일을 수행해 주는 도구를 **컴파일러(compiler)**라고 합니다.
컴파일러는 개발자가 작성한 소스 코드 전체에 문법적인 오류, 실행 가능한 코드인지, 실행하는데 불필요한 코드는 없는지(주석)을 따지며 소스 코드를 처음부터 끝가지 저급 언어로 컴파일합니다.
이때 소스 코드 내에서 오류를 하나라도 발견하면 해당 소스 코드는 컴파일에 실패하게됩니다.
컴파일이 성공적으로 수행되면 개발자가 작성한 소스 코드는 컴퓨터가 이해할 수 있는 저급 언어로 변환됩니다. 컴파일러를 통해 저급 언어로 변환된 코드를 목적 코드(object code) 라고 합니다.
인터프리터 언어
인터프리터 언어는 인터프리어에 의해 소스 코드가 한 줄씩 실행되는 고급 언어입니다. 대표적인 인터프리터 언어로 Python이 있습니다.
소스 코드 전체가 저급 언어로 변환되는 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 한 줄씩 차례로 실행합니다.
소스 코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구를 인터프리터(interpreter)라고 합니다.
인터프리터 언어는 컴퓨터와 대화하듯 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없습니다.
그리고 소스 코드 내에 오류가 하나라도 있으면 컴파일이 불가능했던 컴파일 언어와는 달리, 인터프리터 언어는 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 N번째 줄에 문법 오류가 있더라도
N-1 번째 줄까지는 올바르게 수행이 됩니다.
일반적으로 인터프리터 언어는 컴파일 언어보다 느립니다. 즉 목적 코드는 컴퓨터가 이해하고 실행할 수 있는 저급 언어인 반면, 인터프리터 언어는 소스 코드 마지막에 이를 때까지 한 줄 한 줄씩 저급 언어로 해석하며 실행해야 하기 때문입니다.
컴파일 언어와 인터프리터 언어, 칼로 자르듯이 구분이 될까?
C, C++과 같이 명확하게 구분할 수 있는 언어도 있으나, 현대의 많은 프로그래밍 언어 중에는 컴파일 언어와 인터프리터 언어 간의 경계가 모호한 경우가 많습니다.
가령 대표적인 인터프리터 언어로 알려진 Python도 컴파일을 하지 않는 것은 아니며, Java의 경우 저급 언어가 되는 과정에서 컴파일과 인터프리터를 동시에 수행합니다.
결론적으로는 하나의 프로그래밍 언어가 반드시 둘 중 하나의 방식만으로만 작동한다고 생각하는 것은 오개념입니다. 컴파일 언어와 인터프리터 언어로 칼로 자르듯 구분하기보다는 ‘고급 언어가 저급 언어로 변환되는 대표적인 방법에는 컴파일 방식과 인터프리터 방식이 있다’ 정도로만 이해하는 것이 좋습니다.
핵심 포인트
- 고급 언어는 사람이 이해하고 작성하기 쉽게 만들어진 언어입니다.
- 저급 언어는 컴퓨터가 직접 이해하고 실행할 수 있는 언어입니다.
- 저급 언어는 0과 1로 이루어진 명령어로 구성된 기계어와 기계어를 사람이 읽기 편한 형태로 번역한 어셈블리어가 있습니다.
- 컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 언어입니다.
- 인터프리터 언어는 인터프리터에 의해 소스 코드가 한 줄씩 저급 언어로 변환되어 실행되는 언어 입니다.
Quiz
답
3, 4번
1 , 2번은 고급 언어이다.
답
2번
인터프리터 언어는 소스 코드 마지막에 이를 때까지 한 줄 한 줄씩 저급 언어로 해석하며 실행해야 되기때문에 더욱 느리다.
명령어의 구조
저급 언어의 명령어들의 구조를 한번 살펴보는 챕터입니다.
연산 코드와 오퍼랜드
컴퓨터 속 명령어도 결국에는 ‘무엇을 대상으로, 어떤 작동을 수행하라’ 라는 명령 이라는 구조로 되어 있습니다.
위의 사진에서 색 배경 필드에는 명령의 ‘작동/연산’ , 흰색 배경 필드에는 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 담고 있습니다.
연산 코드(operation code) : 명령어가 수행할 연산
오퍼랜드(operand) : 연산에 사용할 데이터가 저장된 위치
연산 코드는 연산자, 오퍼랜드는 피연산자라고도 부른다.
위의 그림에서 색칠된 부분(연산 코드가 담기는 영역)을 연산 코드 필드라고 부르고,
색칠되지 않은 부분(오퍼랜드가 담기는 영역)을 오퍼랜드 필드라고 합니다.
기계어와 어셈블리어 또한 명령어이기 때문에 연산 코드와 오퍼랜드로 구성되어져 있습니다.
붉은 글씨가 연산 코드(연산자), 검은 글씨가 오퍼랜드(피연산자)입니다.
오퍼랜드
오퍼랜드는 ‘연산에 사용할 데이터’ 또는 ‘연산에 사용할 데이터가 저장된 위치’를 의미합니다.
그래서 오퍼랜드 필드에는 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올 수 있습니다. 대체적으로 오퍼랜드 필드에는 숫자나 문자와 같이 연산에 사용할 데이터를 직접적으로 오기 보다는 연산에 사용할 데이터가 저장된 위치(메모리 주소/레지스터 이름)이 담깁니다.
그래서 오퍼랜드 필드를 주소 필드라고 부르기도 합니다.
오퍼랜드는 명령어 안에 하나도 없을 수도 있고, 한 개만 있을 수도 있고, 두 개 또는 세 개 등 여러 개가 있을 수도 있습니다.
오퍼랜드가 하나도 없는 명령어를 0-주소 명령어
오퍼랜드가 하나인 명령어를 1-주소 명령어, 두 개인 명령어를 2-주소 명령어, 세 개인 명령어를 3-주소 명령어 라고 합니다.
연산 코드
연산 코드는 ‘명령어가 수행할 연산을 의미’ = ‘더해라’, ‘빼라’, ‘저장해라’에 해당하는 부분
연산 코드 종류는 매우 많지만, 가장 기본적인 연산 코드 유형은 크게 네 가지로 나눌 수 있습니다.
- 데이터 전송
- 산술/논리 연산
- 제어 흐름 변경
- 입출력 제어
데이터 전송
- MOVE : 데이터를 옮겨라
- STORE : 메모리에 저장하라
- LOAD(FETCH) : 메모리에서 CPU로 데이터를 가져와라
- PUSH : 스택에 데이터를 저장하라
- POP : 스택의 최상단 데이터를 가져와라
산술/논리 연산
- ADD / STBTRACT / 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 비트가 됩니다.
2-주소 명령어, 3-주소 명령어 라면 오퍼랜드 필드의 크기는 더욱 작아집니다.
명령어의 크기가 16비트, 연산 코드 필드가 4비트인 2-주소 명령어에서는 오퍼랜드 필드당 6비트 정도밖에 남지 않습니다. 즉, 하나의 오퍼랜드 필드로 표현할 수 있는 정보의 가짓수는 262^6 개 밖에 되지 않습니다.
만약 3-주소 명령이 일 경우에는 오퍼랜드 필드당 4비트 정도밖에 남지 않아 정보의 가짓수가 개 정도일것이다.
하지만 만약 오퍼랜드 필드 안에 메모리 주소가 담긴다면 표현할 수 있는 데이터의 크기는 하나의 메모리 주소에 저장할 수 있는 공간만큼 커질것입니다.
한 주소에 16비트를 저장할 수 있는 메모리가 있다고 가정을 하면 이 메모리 안에 데이터를 저장하고, 오퍼랜드 필드 안에 해당 메모리 주소를 명시한다면 표현할 수 있는 정보의 가짓수가 2162^{16} 개로 확 늘어날 것이다.
만약 레지스터 이름을 명시할 때도 표현할 수 있는 정보의 가짓수는 해당 레지스터가 저장할 수 있는 공간만큼 커질것입니다.
연산 코드에 사용할 데이터가 저장된 위치, 즉 연산의 대상이 되는 데이터가 저장된 위치를 유효 주소(effective address)라고 합니다.
위의 그림에서 유효 주소는 10번지 가 됩니다.
이렇게 오퍼랜드 필드에 데이터가 저장된 위치를 명시할 때 연산에 사용할 데이터 위치를 찾는 방법을 주소 지정 방식(addressing mode) 라고 합니다. 즉 주소 지정 방식은 유효 주소를 찾는 방법입니다.
즉시 주소 지정 방식
즉시 주소 지정 방식(immediate addressing mode)는 연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시하는 방법
명령어에 저장할 수 있는 데이터의 크기가 작아지는 단점이 있지만, 연산에 사용할 데이터를 메모리나 레지스터로부터 찾는 과정이 없기에 주소 지정 방식중에서 가장 빠르다.
직접 주소 지정 방식
직접 주소 지정 방식(direct addressing mode)는 오퍼랜드 필드에 유효 주소를 직접적으로 명시하는 방식
오퍼랜드 필드에서 표현할 수 있는 데이터의 크기는 즉시 주소 지정 방식보다 크지만, 여전히 유효 주소를 표현할 수 있는 범위가 연산 코드의 비트 수만큼 줄어들었습니다.
표현할 수 있는 유효 주소에 제한이 생길 수 있습니다.
간접 주소 지정 방식
간접 주소 지정 방식(indirect addressing mode)는 유효 주소의 주소를 오퍼랜드 필드에 명시합니다.
직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 더 넓지만, 메모리에 두번 접근이 필요하기 때문에 다른 주소 지정 방식들보다 일반적으로 조금 느리다.
레지스터 주소 지정 방식
레지스터 주소 지정 방식(register addressing mode)는 직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방법입니다.
일반적으로 메모리보다 CPU 내부에 있는 레지스터가 훨씬 빨라서 앞선 주소 지정 방식들 중에 2번째로 빠르게 접근할 수 있습니다. 다만, 레지스터 주소 지정 방식은 직접 주소 지정 방식과 비슷한 문제를 공유합니다. 표현 할 수 있는 레지스터 크기에 제한이 생길 수도 있습니다.
레지스터 간접 주소 지정 방식
레지스터 간접 주소 지정 방식(register indirect addressing mode)는 연산에 사용할 데이터를 메모리에 저장하고, 그 주소(유효 주소)를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법입니다.
유효 주소를 찾는 과정이 간접 주소 지정 방식과 비슷하지만, 메모리에 접근하는 횟수가 한 번으로 줄어든다는 차이이자 장점이 있습니다. 레지스터가 메모리보다 훨씬 빠르기 때문에 찾아가는 과정에서의 속도가 간접 주소 지정 방식보다 빠릅니다.
정리
연산에 사용할 데이터를 찾는 방법 : 주소 지정 방식
연산에 사용할 데이터가 저장된 위치 : 유효 주소
각각의 방식에 따라 오퍼랜드 필드에 명시되는 값을 정리하여 봅니다.
- 즉시 주소 지정 방식 : 연산에 사용할 데이터
- 직접 주소 지정 방식 : 유효 주소(메모리 주소)
- 간접 주소 지정 방식 : 유효 주소의 주소
- 레지스터 주소 지정 방식 : 유효 주소(레지스터 이름)
- 레지스터 간접 주소 지정 방식 : 유효 주소를 저장한 레지스터
핵심 키워드
- 명령어는 연산 코드와 오퍼랜드로 구성됩니다.
- 연산 코드는 명령어가 수행할 연산을 의미합니다.
- 오퍼랜드는 연산에 사용할 데이터 또는 연산에 사용할 데이터가 저장된 위치를 의미합니다.
- 주소 지정 방식은 연산에 사용할 데이터 위치를 찾는 방법입니다.