오늘은 컴파일에 대한 자세한 정의와 그 과정을 알아볼 것이다.
컴파일이란 고수준 언어(high-level programming language | C/C++, C#, Java ...)를 컴퓨터가 이해할 수 있는 기계어(low-level programming language | machine language)로 변환하는 작업
CPU는 이진수(binary code)를 사용하는 기계어밖에 이해하지 못하지만 기계어는 사람이 읽고 작성하기에는 매우 불편하다. 이 때문에 사람이 다루기 쉬울 뿐 아니라 다양한 자료구조나 알고리즘을 효율적으로 표현할 수 있는 고수준 언어가 생겨났고, 컴파일이라는 과정이 필요해진 것이다.
컴파일의 전반적인 과정을 보기 쉽게 그림으로 나타내면 다음과 같다.
이제부터 각 과정에 대해서 자세하게 알아보자.
전처리기 구문(#으로 시작하는 구문)을 처리하고 주석을 제거한다.
전처리 과정(Pre-processing)은 전처리기(Pre-processor)를 통해 소스 코드 파일을 전처리 된 소스 코드 파일로 변환하는 과정이다.
이 때,
이를 확인해보기 위해 "Hello, World!"를 출력하는 간단한 소스 코드를 작성 후 g++로 전처리 과정만 진행시켜 보았다.
전처리 과정만 진행시키기 위해서는 -E 옵션을 주면 되고, 실행 결과로 나온 main.i파일을 vim으로 확인해 본 결과
g++ -E main.cpp -o main.i
위와 같이 헤더 파일의 내용이 그대로 복사되어 소스 코드에 삽입된 것을 볼 수 있다.
단, 여기서 주의할 점은 보통 코드를 작성할 때 헤더파일에는 함수의 원형을 선언하고, 함수의 구현을 담당하는 코드 파일을 따로 작성하므로 전처리 과정을 통해 헤더파일로부터 함수의 원형은 복사되지만 함수를 구현한 내용은 포함되어 있지 않다.
따라서 이 전처리 된 소스코드 파일을 컴파일과 어셈블리 과정을 통하여 오브젝트 파일로 만들더라도 독립적으로 실행할 수 없고 링킹이 필요한 것이다.
소스 코드의 문법을 검사하고 Static 영역의 메모리(text, data, bss) 할당을 수행한다.
컴파일 과정(Compilation)은 컴파일러(Compiler)를 통해 전처리 된 소스 코드 파일을 어셈블리어 파일로 변환하면서 위의 작업을 수행한다.
이를 확인해보기 위해 전처리 된 소스 코드를 g++로 컴파일 시켜보았다.
컴파일한 어셈블리어 파일을 얻기 위해서는 -S 옵션을 주면 되고, 실행 결과로 나온 main.s파일을 vim으로 확인해 본 결과
g++ -S main.i -o main.s
위와 같이 전처리 된 소스 코드 파일이 어셈블리어로 바뀐 것을 볼 수 있다.
gcc, g++ 컴파일러는 .s
확장자와 .S
확장자를 구분한다.
.s
확장자는 전처리가 필요 없는 파일, 컴파일러가 출력한 어셈블리어 코드에 사용되고
.S
확장자는 전처리가 필요한 파일, 개발자가 직접 전처리기 구문(#include
, #define
, #if
...)을 추가하여 작성한 어셈블리어 코드에 사용된다.
(gcc, g++ 컴파일러는 .S
파일을 컴파일 할 때 전처리 코드를 해석할 수 있다.)
어셈블리어 코드 파일을 기계어, 즉 오브젝트 코드로 변환하는 작업
어셈블리 과정(Assembly)은 어셈블러(Assembler)를 통해 어셈블리어 파일을 오브젝트 파일로 변환하는 과정이다.
이를 확인해보기 위해 컴파일 후 얻은 어셈블리어 파일을 어셈블리 과정을 오브젝트 파일로 변환시켜 보았다.
오브젝트 파일을 얻기 위해서는 -c 옵션을 주면 되고, 실행 결과로 나온 main.o파일을 hexdump로 확인해 본 결과
g++ -c main.s -o main.o
hexdump -C main.o
위와 같이 이진수(binary code)의 기계어로 되어있음을 볼 수 있다.
오브젝트 파일들과 라이브러리 파일들을 묶어 하나의 실행 파일로 만드는 작업
오브젝트 파일은 오브젝트 코드, 즉 기계어로 구성된 파일이며 아래와 같은 구조를 가진다.
여기서 주목해야 하는 부분은 Symbol Table Section과 Relocation Information Section이다.
심볼(Symbol) 은 함수나 변수를 식별할 때 사용하는 이름으로 Symbol Table 안에는 오브젝트 파일에서 참조되고 있는 심볼 정보를 가지고 있는데
이 때 오직 해당 오브젝트 파일의 심볼 정보만 가지고 있어야 하기 때문에 다른 파일에서 참조되고 있는 심볼 정보의 경우 심볼 테이블에 저장할 수 없다.
즉, 전처리 과정에서 언급한 것 처럼 오브젝트 파일 안에 함수를 구현한 내용이 없으므로 외부에서 참조하는 함수에 대한 심볼 정보는 가지고 있지 않은 것이다.
따라서 실행 파일을 만들기 위해서는 이 오브젝트 파일을 포함하여 함수를 구현한 오브젝트 파일(혹은 라이브러리)를 연결시키는 작업이 필요한 것이고 이것을 링킹이라고 부르는 것이다.
링커(Linker)는 먼저 각 오브젝트 파일에 있는 심볼 참조를 어떤 심볼 정의에 연관시킬지 결정하는 심볼 해석(Symbol Resolution) 과정을 거친 후
오브젝트 파일에 있는 데이터의 주소나 메모리 참조 주소를 알맞게 재배치하는 재배치(Relocation) 과정을 진행한다.
이는 링커가 오브젝트 파일을 모아서 하나의 실행파일을 만들 때, 각 오브젝트 파일에 있는 데이터나 메모리 참조 주소가 실행 파일에서의 주소와 다르기 때문이다.
이를 위해 오브젝트 파일 안에 Relocation Information Section이 존재하는 것이며 링킹 과정에서 같은 세션끼리 합쳐진 후 재배치가 일어난다.