고급 언어와 저급 언어
Java, C, C++, C#, Python, Ruby, Javascript ... 모든 프로그램을 만드는 소스코드는 명령어로 변환된 이후에 실행될 수 있습니다.
위에서 언급한 프로그래밍 언어는 공통점이 있습니다. 바로 사람이 이해할 수 있는 '고급 언어(High-level programming language)'라는 것입니다. 컴퓨터 구조를 배울 때나 특정 환경에서는 간혹 '저급 언어(Low-level programming language)를 사용하기도 합니다. 저급 언어는 두 종류로 나뉘며 이는 어셈블리어와 기계어를 말합니다.
기계어는 0과 1의 비트들로 이루어진 명령어들입니다. 이는 이진수로 표현될 수 있으며, 너무 길게 표현되어 가독성이 떨어져서 16진수로 표현하는 경우가 많습니다. 어셈블리어는 push mov add pop ret 등의 니모닉들을 포함하여 구성됩니다. 이러한 기계어와 어셈블리어는 하드웨어와 프로그램이 아주 밀접하게 닿아있는 경우에 주로 사용됩니다. 예를 들면 임베디드 개발이나 게임 개발, 정보 보안 분야 등이 있습니다.
기계어를 직접 사용하는 경우는 거의 없으나 어셈블리어는 현재도 활발하게 사용됩니다. 아직도 그것을 다룰 수 있을 때의 이점이 강하게 남아있기 때문입니다. 프로그램이 실제로 어떤 명령어로 이뤄져 컴퓨터에게 어떤 명령을 내리는지를 알 수 있기 때문입니다. 가령 무언가를 더한다고 할 때, 앞서 기록했던 CPU 내부의 코어에서 ALU, CU, Register 간 어떤 일들이 일어나는지를 어셈블리어를 통해 알 수 있습니다.
사람이 이해하기 쉬운 고급 언어도 결국 컴퓨터에게 어떤 명령을 처리하게 하려면 결국 저급 언어로 변환되어야 합니다. 고급 언어를 저급 언어로 변환시키는 과정은 어떻게 수행될까요? 두 가지 접근 방법이 있습니다. 그것은 컴파일 방식과 인터프리트 방식입니다. (Compile or Interpret)
가장 큰 두 방식의 차이는 다음과 같습니다. 한번에 변환하느냐, 필요한 한 줄씩 변환하느냐 입니다. 컴파일 방식은 전체 소스코드를 한번에 변환하여 목적 코드로 만들고, 인터프리트 방식은 한 줄씩 변환합니다.
이로 인해 다음과 같은 특징이 생기게 됩니다. 컴파일 방식은 고급 언어로 짜여진 프로그램을 실행하기까지 컴파일 시간이 비교적 길고, 또 중간에 오류가 있다면 컴파일을 할 수가 없습니다. 인터프리트 방식은 한 줄마다 변환하므로 실행 자체는 빠르지만 중간에 계속 변환과정이 있으므로 프로그램의 실행 속도가 컴파일 방식보다 떨어집니다. 그리고 인터프리트 방식은 한 줄마다 변환하므로 중간에 오류가 있어도 변환이 가능합니다.
현대 고급언어들의 변환은 컴파일, 인터프리트 방식을 딱 하나만 선택해서 하지 않는 경우가 많습니다. 둘 다의 장점을 취하는 경우가 많습니다. 이러한 컴파일, 인터프리트 두 방식은 언어를 분류하기 위한 방법이 아니라, 고급언어가 저급언어로 변환되는 방법은 크게 두 가지로 나뉜다고 이해하기 위한 것으로 받아들여 주시기 바랍니다.
컴파일러가 고급 언어를 컴파일하여 저급 언어로 변환한 코드를 따로 부르는 용어가 있습니다. 이를 '목적 코드(Object code)'라고 부릅니다. 또한 이러한 목적 코드로 이루어진 파일을 '목적 파일(Object file)'이라고 부릅니다. 목적 파일은 바로 실행할 수 있을까요? 일반적으로 그렇지는 않습니다. 절대 다수의 프로그램은 여러 개의 소스코드 파일을 통해 만들어지며, 목적 코드들 또한 각각의 고급 언어의 소스 코드들을 컴파일하여 만들어집니다. 이러한 다수의 목적 코드들은 서로간의 의존성이 있는데, 이러한 의존성을 해결해 주기 위한 연결 작업인 '링킹(Linking)'이 필요합니다. 이렇게 링킹이 끝난 경우의 목적파일들은 '실행 파일(Execution File)'이라고 할 수 있습니다.
명령어의 구조
앞서 '고급 언어와 저급 언어'에서 컴퓨터가 이해할 수 있는 것은 저급 언어라는 것을 알아보았습니다. 이러한 저급 언어는 명령어들로 구성되어 있다고 하였는데, 그러면 명령어는 어떠한 구조를 가지고 있을까요.
사람들이 말하는 언어의 명령어를 생각해보면, '무엇을 어떻게 해라' 라는 것이 기본 구조입니다. 컴퓨터가 받아들이는 명령어도 다르지 않습니다.
- 무엇을 = 오퍼랜드(피연산자)
- 어떻게 해라 = 연산 코드(연산자)
명령어는 연산 코드와 오퍼랜드로 구성되어 있습니다. 어떤 연산을 수행해야 하는지를 '연산 코드(Operation code)'라고 하며, 연산에 사용할 데이터나 데이터의 위치를 '오퍼랜드(Operand)'라고 합니다. 다른 이름으로, 연산코드를 연산자, 오퍼랜드를 피연산자라고 한글 명칭으로 부르기도 합니다.
연산 코드는 하나로 충분한 경우가 대부분이나, 오퍼랜드는 그렇지 않습니다. 예를 들면 더하는 명령어의 경우 두 가지의 오퍼랜드가 필요합니다. 그래서 우리가 어셈블리어를 보고, 표현하는 것을 편하게 하기 위해서 두 가지의 필드를 나눕니다. 연산 코드가 들어가는 부분은 '연산 코드 필드(Operation code field)'라고 부르며, 오퍼랜드들이 들어가는 부분은 '오퍼랜드 필드(Operand field)'라고 부릅니다.
우리가 어떤 명령을 컴퓨터에게 내리기 위해서는 하나 이상의 연산 코드가 필요하겠지만, 오퍼랜드는 0개 일 수도 있고, 무수히 많을 수도 있습니다. 필요한 오퍼랜드의 개수(n 개)에 따라 해당 명령어를 n-주소 명령어라고 합니다.
연산 코드는 굉장히 다양합니다. 기본적인 연산 코드의 유형은 네 개로 나눌 수 있습니다.
- 데이터 전송
- 산술/논리 연산
- 제어 흐름 변경
- 입출력 제어
각각의 유형별 명령어의 예시는 다음과 같습니다.
- 데이터 전송: MOVE, STORE, LOAD(FETCH), PUSH, POP (스택 데이터 구조의 푸시, 팝이 맞습니다.)
- 산술/논리 연산
- 산술 연산: ADD, SUBTRACT, MULTIPLY, DIVIDE
- 산술 연산(최적화): INCREMENT (+1), DECREMENT (-1)
- 논리 연산: AND, OR, NOT
- 논리 연산: COMPARE
- 제어 흐름 변경
- JUMP, CONDITIONAL JUMP (특정 주소로 실행 순서 변경)
- HALT
- CALL (되돌아 올 주소 저장하여 JUMP)
- RETURN (CALL 호출할 때 저장한 주소로 실행 순서 변경)
- 입출력 제어 : READ, WRITE, START IO, TEST IO
명령어의 오퍼랜드에는 데이터를 직접 담기보다는 메모리나 레지스터의 주소를 담는 경우가 대부분입니다. 이는 명령어 하나의 최대 크기가 제한되어 있기 때문입니다. 특히 연산 코드가 명령어에서 차지하는 길이가 클 수록 오퍼랜드 필드는 사용할 수 있는 길이가 줄어들게 됩니다. (64비트 길이의 명령어라고 하더라도 2^64 크기의 메모리 용량의 모든 주소를 오퍼랜드에서 지정할 수 없습니다.)
연산 코드의 대상이 되는 데이터가 저장된 주소를 유효 주소(Effective address)라고 합니다. 오퍼랜드에 주소를 담는(=지정하는) 방식도 이러한 명령어 크기 제한 하에서 최대한 효율성을 높이기 위해 여러 방식들이 고안되었습니다. 이러한 방식들은 주소 지정 방식(Addressing mode)라고 부르며, 이는 오퍼랜드에 명시된 데이터 위치를 찾는 방법을 말합니다.
주소 지정 방식은 주로 다섯가지가 사용됩니다.
- 즉시 주소 지정 방식(Immediate addressing mode): 오퍼랜드 필드에 연산에 사용할 데이터를 '직접' 명시합니다. 레지스터나 메모리를 참조할 필요가 없어 가장 빠르나 표현할 수 있는 데이터의 크기에 한계가 발생합니다.
- 직접 주소 지정 방식(Direct addressing mode): 오퍼랜드 필드에 유효 주소를 직접적으로 명시하는 방식을 말합니다.
- 간접 주소 지정 방식(Indirect addressing mode): 유효주소의 주소를 오퍼랜드 필드에 명시하는 방식입니다. 메모리에 유효주소가 적혀 있으니 간접적으로 주소가 지정된 방식입니다. 직접 주소 지정 방식보다는 표현 가능한 유효 주소의 범위가 넓어지지만 메모리를 두 번 참조해야 하는 단점이 생깁니다.
- 레지스터 주소 지정 방식(Register addressing mode): 시스템 버스를 통해 접근해야 하는 메모리와는 달리 CPU 내부에 있는 레지스터는 훨씬 빠르게 접근이 가능하나 그 용량이 메모리와 비교할 수 없이 작습니다. 레지스터에 데이터가 저장되어 있는 경우 레지스터의 주소를 오퍼랜드에서 사용하는 것이 레지스터 주소 지정 방식입니다.
- 레지스터 간접 주소 지정 방식(Register indirect addressing mode): 레지스터에 유효주소를 저장한 상태에서 그 주소를 오퍼랜드에 명시하는 방식입니다. 간접 주소 지정 방식과 유사하나 유효주소의 주소가 메모리가 아닌 레지스터에 있는 것입니다.