3 minute read

컴파일러(compiler)

-컴파일러란 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 번역하는 프로그램입니다.
컴파일러는 실행 프로그램을 만들기 위해 고급 언어를 저급 언어(ex 어셈블리어, 오브젝트 코드, 바이트코드)로 바꾸는 데 사용됩니다.

  • 어셈블리어(assembly language) : 기계어와 일대일 대응하는 컴퓨터 프로그래밍의 저급 언어
  • 기계어(machine code) : CPU가 직접 해독하고 실행할 수 있는 비트 단위로 쓰인 컴퓨터 언어

인터프리터(interpreter)

-인터프리터란 이전에 컴파일된 기계어 없이 코드를 바로 실행하는 프로그램입니다.
컴파일러는 문서 전체를 읽고 한 번에 번역하는 반면, 인터프리터는 코드를 한 줄씩 읽으며 바로 기계어로 번역해 실행하기 때문에 오브젝트 파일을 만들지 않습니다.

컴파일러와 인터프리터

-컴파일러는 코드 전체를 읽고 기계어로 번역합니다. 따라서 컴파일 타임은 길지만 런타임이 짧습니다.
오브젝트 파일을 생성하므로 메모리를 비교적 많이 사용하며, 코드 전체를 컴파일해야 하므로 코드 수정이 어려운 편입니다.
관련 프로그래밍 언어로는 C, C++ 등이 있습니다.

인터프리터는 런타임 중 코드를 한 줄씩 읽어 실행합니다. 따라서 런타임이 긴 편입니다.
오브젝트 파일을 생성하지 않아 메모리를 비교적 적게 사용하며, 한 줄씩 읽기 때문에 코드 수정이 용이합니다.

관련 프로그래밍 언어로는 HTML, JavaScript, Python, Ruby가 있습니다.

하이브리드 기법

-컴파일 기법과 인터프리터 기법을 혼합한 형태입니다.
고급 언어로 작성된 프로그램을 쉽게 해석할 수 있도록 중간 언어로 번역(컴파일) 후, 프로그램을 해석(인터프리터)하여 실행합니다.
대표적인 하이브리드 언어인 Java로 예로 들겠습니다.

(1) 개발자가 자바 소스 코드(.java)를 작성합니다.
(2) 자바 컴파일러가 소스 코드를 컴파일하여 바이트코드로 작성된 파일(.class)를 생성합니다.
(3) 바이트코드 파일을 JVM(Java Virtual Machine)의 메모리에 올립니다.
(4) 자바 인터프리터가 바이트코드 파일을 한줄씩 읽어가면서 실행합니다.

추가적으로 최근 JVM은 실행 속도가 느린 인터프리터 방식의 문제를 해결하기 위해 JIT컴파일러를 지원합니다.

  • JIT 컴파일(Just-In-Time) : 실행 전 파일 전체를 기계어로 번역하는 정적 컴파일 방식이 아닌, 런타임 중 일부 바이트코드를 기계어로 번역하는 방식. 번역된 기계어를 캐싱에 남겨둬 같은 코드가 반복 시, 다시 컴파일 하지 않고 재사용합니다.
  • 바이트코드(bytecode, p-code) : 특정 하드웨어가 아닌 가상머신에서 돌아가는 바이너리 코드. 하드웨어 의존도가 떨어지며 이식성이 높습니다.

C언어의 컴파일 과정

-C언어로 작성된 소스 코드는 컴파일 과정을 통해 실행할 수 있는 프로그램이 됩니다.
컴파일은 전처리 - 컴파일 - 어셈블 - 링킹, 4단계에 거쳐 진행됩니다.
C로 작성된 HelloWorld.c라는 문서를 예를 들겠습니다.

전처리(preprocessing)

컴파일 전 실행되는 전처리기라는 프로그램으로 문서를 전처리합니다.
전처리기가 실행되면 각 코드 파일에서 지시자를 찾는데, 지시자란 #으로 시작해서 줄바꿈으로 끝나는 코드입니다.

헤더 파일 삽입

#include<파일명></b> 지시자는 그 위치에 파일을 복사합니다.
헤더 파일을 include 하는 것은 함수를 사용하기 전, 함수의 원형을 선언하는 C언어의 특성 때문입니다.

매크로 치환 및 적용

#define 지시자를 통해 매크로를 정의할 수 있습니다.
매크로 상수 : #define Pi 3.1415, 문서 내 모든 Pi를 상수 3.1415로 치환합니다.
매크로 함수 : #define SQUARE(X) XX</b>, 문서 내 SQUARE(X)라는 패턴을 XX라는 수식으로 치환합니다.

전처리 과정을 통해 문서를 컴파일할 준비를 마칩니다.
또한 전처리 후 HelloWorld.c 파일에서 hello.i 파일을 생성합니다.

컴파일(compile)

컴파일 과정은 크게 전단부, 중단부, 후단부로 나눌 수 있습니다.

전단부(Front-end)

전단부에서는 언어 종속적인 부분을 처리합니다.
어휘 분석, 구문 분석, 의미 분석을 통해 소스코드가 해당 언어로 올바르게 작성되었는지 확인하고, 중단부로 넘겨줄 GIMPLE 트리(소스 코드를 트리 형태로 작성한 자료구조)를 생성합니다.

중단부(Middle-end)

중단부에서는 아키텍처 비종속적인 최적화를 수행합니다.
전단부에서 넘겨받은 GIMPLE Tree를 SSA(Static Single Assignment) 형태로 변환 후 아키택처 비종속적인 최적화를 수행 후, 후반부에서 사용할 RTL(Resister Transfer Language, 고급언어와 어셈블리어의 중간 형태)를 생성한다.

*아키텍처 비종속적 최적화 : CPU 아키텍처에 구애받지 않고 공통적으로 수행할 수 있는 최적화.

후단부(Back-end)

후단부에서는 아키텍처 종속적인 최적화를 수행합니다.
중단부에서 넘겨받은 RTL를 이용해 아키텍처 종속적인 최적화를 수행 후 어셈블리 코드를 생성합니다.
아키텍처 종속적인 최적화를 수행하면 해당 아키텍처만 이해할 수 있는 언어가 되기 때문에 다른 아키텍처에서는 어셈블리 코드를 해석할 수 없습니다.
전처리가 끝난 HelloWorld.i 파일을 컴파일하면 어셈블리 코드로 이루어진 HelloWorld.s 파일이 생성됩니다.

*아키텍처 종속적 최적화 : 명령어를 아키텍처 별로 더 효율적인 명령어로 대체하는 등 아키텍처 특성에 따른 최적화.

어셈블(assemble)

컴파일 후 어셈블리어로 작성된 HelloWorld.s 파일은 어셈블러에 의해 기계어로 어셈블됩니다.
어셈블러에 의해 생성되는 오브젝트 파일(hello.o)은 명령어와 데이터가 들어있는 ELF 바이너리 포맷 구조를 가집니다.
이는 링커가 여러 개의 오브젝트 파일을 하나의 실행파일로 묶을 때, 각 파일의 정보를 효과적으로 파악하기 위해 일정한 규칙을 가지도록 형식화 해둔 것입니다.
오브젝트 파일은 바이너리 코드의 형태이지만 아직 완전한 기계어가 아니기에 아직 실행할 수 없습니다.

링크(linking)

오브젝트 파일은 링킹 과정을 통해 실행 가능한 실행파일(HelloWorld.exe)로 만들어집니다.
링커는 오브젝트 파일들과 프로그램에 사용된 표준 C 라이브러리, 사용자 라이브러리들을 링크합니다.
printf() 함수와 같은 표준 라이브러리 함수들은 미리 컴파일 되어 있기 때문에 링킹 과정만 거치면 사용할 수 있습니다.

이러한 과정을 통해 C언어로 작성된 프로그램이 실행됩니다.
HelloWorld.exe 파일을 실행하면, 바이너리 파일의 내용들이 RAM으로 할당되어 동작하게 됩니다.

Categories:

Updated:

Leave a comment