컴파일 과정은 크게는 3가지 과정으로 나뉜다.
Preprocessing(전처리), 컴파일, 링킹이 그것이다.
우선 전처리 과정은 소스 파일의 #include나 #define과 같은 전처리 지시문들을 각각에 맞는 코드들로 바꾸어준다. 매그로를 정의된 코드로 바꾸거나 #if문으로 적절한 분기점의 코드로 교체하거나 하는등의 일을한다. 이것들을 거쳐서 전처리 과정에서는 최종적으로 일련의 연속된 토큰들을 만들어낸다. 또한 컴파일러가 각 코드가 몇번째 라인에 있던 코드인지를 알려주는 특별한 마커들을 추가하기도 한다. 이를 통해 컴파일러는 몇번째 줄에서 에러가 발생했다와 같은 에러 메세지를 생성해낼 수 있다. 물론 전처리 과정에서도 에러를 생성할 수도 있다.
( 참고로 #include는 쉽게 생각하면 그냥 그 파일을 무식하게 복사 붙여 넣기한다고 생각하면 된다. 그 이상 그 이하도 아니다. )
다음으로는 컴파일 과정이다.
컴파일은 각각의 소스코드 파일을 전처리한 후 생성된 일련의 토큰을 가지고 수행한다. 컴파일러는 순수 C++ 소스코드를 파싱해서 어셈블리 코드로 바꾼다. 그리고 이 결과로 실제 바이너리 파일을 생성하는 기계어 코드를 ELF나 COFF, OUT과 같은 포맷으로 만들어낸다. 오브젝트 파일들은 각각의 소스파일마다 각자 별개로 컴파일되어 생성된다 이러한 오브젝트 파일은 바이너리 형태의 컴파일 코드를 가지고 있다. 오브젝트 파일에 있는 여러 심볼들은 이름을 가지고 참조된다.
예를 들면 “HELLO WORLD”라는 상수 문자열이 코드에 있다면 컴파일러는 이것을 상수 데이터 영역에 넣어둔다. 그리고 특별한 문자열 ( $L1 )과 같은 이름을 붙인다. 또 print라는 함수를 호출하면 그 호출 코드를 어셈블리어로 변환해서 코드 영역에 넣거나한다. 또한 괄호문의 사이즈가 N 바이트라거나( 스택 ), printf가 main 문으로부터 M만큼 떨어진 위치에 있다는 등의 정보도 오브젝트 파일에 포함시킨다.
오브젝트 파일들은 아직 정의(definiation) ( <-> 선언(declaration) )가 되지 않은 심볼(함수명, 변수명 등등의 여러 이름)들을 참조할 수도 있다. 이는 흔히 쉽게 볼 수 있는 선언부만 볼 수 있고 그 선언의 정의부분을 보지 못하는 경우가 이 경우다. 컴파일 단계에서 컴파일러는 선언만 보이고 정의가 보이지 않아도 신경쓰지 않는다. 그리고 행복하게 정의부가 없는 여러 선언부를 가지고 있는 오브젝트 파일을 생성해낸다.
컴파일러을 가지고 하나의 소스파일에 대해서만 이 전처리, 컴파일 과정을 수행할 수도 있다. 각각의 소스파일마다 각각의 서로 다른 오브젝트 파일들을 서로 독립적으로 만들어내는 이 특징 덕분에 만약 소스파일이 바뀌지 않으면 다시 컴파일 하지 않고 기존에 있던 오브젝트 파일을 사용할 수 있다.
그리고 생성된 오브젝트 파일들은 정적 라이브러리(Static Library)라고 하는 특별한 보관소에 넣을 수 있다. 이 정적 라이브러리에 대해서는 밑에서 설명하겠다.
이 지점에서 보통의 컴파일러 에러는 syntax 에러나 오버로드 참조 에러 같은 것들이다.
다음으로는 링킹 과정이다.
이 단계에서는 위의 컴파일 과정의 결과물인 오브젝트 파일들을 가지고 exe( 실행파일 )이나 동적 라이브러리 파일등을 생성해낸다.
링커는 컴파일 과정에서 아직 선언만 보이고 정의만 보이지 않았던 심볼들에 대해 그 선언들의 정의를 찾아주는(정의부 코드의 주소) 작업을 한다. 이러한 심볼들은 선언부가 있는 오브젝트 파일이 아닌 다른 오브젝트 파일들 ( 혹은 라이브러리 )에서 찾는다. 링커는 이것들을 찾아서 선언부 심볼( 이름 )들과 정의부를 연결시켜주는 작업을 하는 것이다. 심볼의 선언에 맞는 선언부를 찾고 그 선언부에 또 모르는 심볼이 있으면 또 다시 선언부를 찾아나가는 방식이다.
이러한 각종 심볼들은 심볼 테이블을 만들어서 관리하는데 각각의 심볼들의 정의부가 메모리에 위치할 주소를 심볼과 함께 저장한다. 만약 특정 심볼의 정의를 찾지 못하면 링커는 링킹을 멈추고 에러를 발생시킨다 ( undefined external XXX와 같은 메세지말이다 ). 정의부가 여러 개인 경우에도 ( 같은 심볼의 정의부가 여러 오브젝트 파일 혹은 라이브러리에 존재하는 경우 ) 링커는 에러를 발생시킨다.
그 후 각종 코드들과 데이터 ( 상수 문자열 등등… )들을 연속되게 모두 실행 파일에 넣는다. 만약 여러 오브젝트 파일들이 코드들을 가지면 이들을 연속되게 배치하고, 만약 동일한 문자열 상수가 각각의 오브젝트 파일에 포함되면 그것은 합쳐서 실행파일에 하나만 넣어서 이 유니크한 문자열 데이터를 참조하게 만든다. 이러한 코드와 데이터들을 배치하는 순서는 어떻게 링커를 설정하느냐에 따라 다 다를 수 있다.
다음으로는 로더 과정이다. ( 이 과정은 컴파일 후 실행 파일을 실행한 후 거치는 과정이다. )
로더는 OS의 일부분으로 실행 파일을 로드하는 기능을 한다. 예전에는 ( 예를 들면 MS_DOS의 .com 파일 ) 이러한 로더가 단순이 실행 파일에서 데이터를 읽어 메모리에 올리고 특정 주소에서 명렁어를 실행하게 만드는 기능만을 했다.
그러나 최근의 로더들은 ( 예를 들면 MS-DOS의 .exe 파일 ) 이와 비슷하지만 파일을 메모리에 읽은 후 실행 파일 내의 데이터들에 있는 주소들을 특정 위치로 옮기는 역할까지 한다. 그러니깐 실행 파일에는 시작 명령어가 2000번째 주소에 있다면 로더를 통해 실제 컴퓨터의 로드한 후에는 그 시작 명령어가 4000번째 주소에 올려져 그 시작 명령어의 주소를 4000으로 바꾸는 역할까지 한다는 것이다. 이렇게 실행 파일 내의 각 주소들에 대해 그 실행파일의 데이터가 실제 컴퓨터 메모리에 올라간 후 그 데이터들의 base 주소를 더해주는 역할까지 하는 것이다.
여기에 더해 최신 OS는 동적 오브젝트 파일 ( .so ) 혹은 동적 링킹 라이브러리 ( DLL 파일 )과 같은 것들도 지원한다. 이것은 기본적으로 링커가 하는 일을 로더로 옮긴 것이라고 생각하면 쉽다. 그러니깐 링커 단계에서 아직 정의를 찾지 못한 심볼들을 나두고 실행 파일을 만든 후 로더 단계에서 동적으로 정의부를 로드해서 그 정의부의 주소를 선언부와 연결시켜주는 것이다. 링커 단계에서 아직 정의부를 찾지 못한 심볼에 대해 심볼 테이블에서 주소 대신 XXX.so/DLL 파일에 정의가 있을거야라고 넣어두면 로더 단계에서 로더가 이 XXX.so/DLL 파일을 찾는 것이다.
이러한 동적 오브젝트 파일, 동적 링킹 라이브러리들은 컴퓨터 내 여러 프로세스 ( 프로그램 )들 간에 공유가 되기 때문에 똑같은 모듈 ( 동적 오브젝트 파일, 동적 링킹 라이브러리 )이 이미 컴퓨터 메모리에 올려져 있으면 중복해서 메모리에 로드하지 않고 기존에 메모리에 로드되어 있던 모듈의 코드를 사용하여 심볼의 구현부를 찾아준다. 이를 위해 OS는 현재 로드 된 모듈을의 리스트를 따로 관리한다.
여기서 추가적으로 하나 더 설명하면 이러한 동적 오브젝트 파일, 동적 링킹 라이브러리들이 메모리에 로드될 때 코드 영역은 각각의 프로세스 ( 프로그램 )들끼리 서로 공유해서 사용하지만 데이터 영역 ( 상수 등등… )은 프로세스마다 원본에서 복사를 해가서 각자가 따로 관리한다 ( 어찌보면 당연하다 ).
references : https://stackoverflow.com/q/25826277, https://stackoverflow.com/q/6264249, https://softwareengineering.stackexchange.com/q/103673, https://stackoverflow.com/q/3322911, https://driip.me/2ab5ed83-58ce-4cf2-bee7-51a58bbe21ac