컴파일이란 무엇입니까?
컴파일이란 인간이 이해할 수 있는 언어로 작성된 소스 코드를 CPU가 이해할 수 있는 언어로 변환하는 과정을 말합니다. 소스코드는 컴파일 과정을 거쳐 기계어로 구성된 실행파일이 되며, 이 파일이 실행되면 실행파일의 내용이 운영체제의 로더에 의해 메모리에 로드되어 프로그램이 실행된다. 참고로 메모리에 로드되는 프로그램을 프로세스라고 합니다.
컴파일 과정
일반적으로 컴파일은 다음 네 단계로 나뉩니다. 이 네 단계를 합쳐서 컴파일링 또는 빌드라고 하며, 컴파일링과 링크를 별도로 부르기도 합니다.

전처리
전처리 단계에서 소스코드(.c)는 전처리기에 의해 전처리된 소스코드(.i)로 변환됩니다. 이때 전처리기는 다음 세 가지 작업을 수행합니다.
주석 제거
컴파일은 사람이 읽을 수 있는 소스 코드를 기계가 읽을 수 있는 기계 코드로 변환하는 프로세스입니다. 주석은 사람들이 코드를 이해하는 데 도움이 되도록 작성되기 때문에 전처리 중에 모든 주석이 제거됩니다.
헤더 파일 삽입
#include 지시문이 발견되면 해당 헤더 파일(.h)을 찾아 헤더 파일의 모든 내용을 복사하여 소스 코드에 붙여넣습니다. 즉, 헤더 파일은 컴파일러에 의해 오브젝트 파일(.o)로 변환되기 전에 소스 코드(.c)에 완전히 복사됩니다. 그런 다음 헤더 파일에 선언된 함수 프로토타입은 연결 과정에서 함수가 실제로 정의된 개체 파일(.o)과 결합됩니다.
매크로 대체 및 적용
#define 지시어에 정의된 매크로는 메모리의 텍스트(코드) 영역에 저장되며 소스 코드에서 위의 문자열이 발견되면 #define에 정의된 내용으로 대체된다.
엮다
컴파일 단계에서 전처리된 소스 코드(.i)는 컴파일러에 의해 어셈블리 코드(.s)로 변환됩니다. 언어의 구문을 확인하고 소스 코드에서 선언한 전역 및 정적 변수를 메모리의 데이터 영역에 할당합니다. 컴파일러는 다음 세 단계를 순서대로 수행합니다.
프런트 엔드
프런트 엔드는 소스 코드를 해석하여 작성된 구문이 올바른지, 문법에 따라 작성되었는지 확인합니다. 즉, 언어 종속 부분을 처리합니다. 또한 옵티마이저(일명 미들 엔드)로 전달할 GIMPLE 트리를 구축합니다.
이때 GIMPLE 트리는 소스코드를 트리 구조로 표현한 것을 의미한다. GIMPLE 트리는 언어에 대한 컴파일러 종속성 문제를 해결하기 위해 만들어졌습니다. GIMPLE 트리의 대략적인 구조는 다음과 같으며, GIMPLE 트리가 생성되는 과정은 추후에 자세히 다루도록 하겠다.

옵티마이저
이 단계에서 최적화는 아키텍처 독립적인 수준에서 수행됩니다. 아키텍처 독립적 최적화는 CPU 아키텍처와 독립적으로 수행할 수 있는 최적화를 의미합니다. 코드의 성능을 최대한 향상그리고 프로그램 최소 이진 크기 감소말한다
옵티마이저는 주로 코드의 의미를 유지하면서 효율성을 높이기 위해 코드의 구조를 변경하거나, 동일한 계산을 두 번 이상 반복하거나 사용하지 않는 변수에 값을 할당하는 코드를 제거합니다.
최적화 후 백엔드는 다음을 사용합니다. RTL(Resister Transfer Language: 고급 언어와 어셈블리 언어의 중간 형태)생성 및 제공
백엔드
백엔드는 아키텍처 종속 최적화를 수행합니다. 이는 CPU 아키텍처의 특성에 따른 최적화를 의미합니다. 명령어가 같은 기능을 수행하더라도 CPU 아키텍처별로 효율적인 명령어로 교체해 성능을 높이는 것이 목적이다.
백엔드 최적화가 완료되면 어셈블리 코드(.s)가 생성됩니다. 아키텍처 종속 최적화를 수행하면 어셈블리 코드는 해당 아키텍처의 CPU에서만 해석할 수 있는 언어가 되기 때문에 다른 아키텍처에서 해석할 수 없습니다.
집회
어셈블리 단계에서 어셈블리 코드(.s)는 어셈블러에 의해 객체 코드(.o)로 변환됩니다. 현재 어셈블리 언어는 사람이 이해할 수 있는 코딩된 기계어로 CPU 명령어와 1:1로 대응된다. 어셈블리 코드를 객체 코드(.o)로 변환한 후 인간이 코드를 이해하는 것은 사실상 불가능합니다.
현재 오브젝트 파일 형식은 윈도우용 PE(Portable Executable)와 리눅스 기반 운영체제용 ELF(Executable and Linking Format)로 구분된다. 오브젝트 파일 형식은 다음과 같은 형식으로 구성됩니다.
- 개체 파일 헤더 : 오브젝트 파일 메타데이터가 포함된 헤더
- 텍스트 섹션 : 코드가 기계어로 변환된 부분
- 데이터 섹션 : 데이터가 있는 섹션(전역 변수, 정적 변수)
- 심볼 테이블 섹션 : 소스코드에서 참조하는 식별자의 이름과 주소를 정의하는 부분.
- 이전 정보 섹션 : 식별자의 위치는 링크를 통해서만 알 수 있으므로, 식별자의 위치가 확정되면 내용을 정의하는 부분이 수정됩니다.
- 디버깅 정보 섹션 : 디버깅에 필요한 정보를 정의하는 섹션
오브젝트 파일 형식에서 가장 중요한 부분은 심볼 테이블 섹션수업 이전 정보 섹션오전. 기호 테이블에는 개체(.o) 파일에서 참조되는 식별자의 이름과 주소가 포함됩니다. 이때, 오브젝트 파일의 심볼 테이블에는 해당 오브젝트 파일의 식별자 정보만 포함되어야 하므로 다른 파일에서 참조하는 식별자 정보는 심볼 테이블에 저장할 수 없다.
이제 오브젝트 파일(.o)을 실행 파일(.out, .exe)로 변환하고 소스 코드를 실행할 수 있습니다. 그러나 오브젝트 파일이 외부에서 함수를 참조하는 경우 독립적으로 실행할 수 없습니다. 오브젝트 파일의 심볼 테이블에는 해당 오브젝트 파일의 식별 정보만 있고 외부 참조 변수나 함수의 식별 정보는 없기 때문이다. 따라서 링크가 필요합니다.
지름길
연결 단계에서는 목적 코드(.o)가 링커에 의해 번들되어 실행 파일(.out, .exe)로 변환됩니다. 프로그램에서 사용하는 라이브러리 파일과 오브젝트 파일을 연결하여 실행 파일을 생성하는 과정입니다.
현재 라이브러리는 라이브러리를 어떻게 연결하느냐에 따라 **Static Linking**과 **Dynamic Linking**으로 나눌 수 있습니다. 동적 라이브러리라고 합니다.
정적 라이브러리
정적 라이브러리는 링커가 라이브러리에서 프로그램 실행에 필요한 정보를 찾아 링크 과정에서 실행 파일로 복사하는 라이브러리입니다.
동적 라이브러리
동적 라이브러리는 링커가 링크 과정에서 라이브러리의 내용을 복사하지 않고 내용의 주소만 가지고 있다가 실행 파일과 라이브러리가 위치하면 해당 모듈의 주소로 가는 라이브러리를 말한다. 런타임을 메모리에 저장하고 필요한 항목을 가져옵니다.
링커의 역할기호를 해석하는 것과 기호를 재정렬하는 것으로 크게 나눌 수 있습니다.
아이콘 해상도
아이콘 해상도는 각 개체(.o) 파일의 해상도입니다. 심볼 테이블있는 식별자의 정보를 연결하는 과정입니다. 동일한 이름의 함수나 변수가 여러 오브젝트 파일에 정의되어 있는 경우 이는 어떤 파일에서 어떤 함수를 사용할지 결정하는 역할을 합니다.
부설
재배치는 오브젝트(.o) 파일의 데이터 주소 또는 소스 코드에서 참조하는 메모리 주소를 정렬하는 과정입니다. 컴파일러가 생성한 오브젝트 파일을 링커에서 모아서 실행 파일을 생성할 때 각 오브젝트 파일에 있는 데이터의 주소나 소스 코드가 참조하는 메모리의 주소가 링커에서 조합한 실행 파일의 주소와 다른 경우 . 해당 개체 파일 내에서 수정해야 합니다. 이전 정보 섹션존재하는
실행 파일(.out, .exe)
마지막으로 위의 4단계를 거쳐 우리가 작성한 소스코드를 실행 파일, 즉 0과 1로 구성된 바이트코드로 변환한다.
정리할 때
코드를 작성하고 컴파일하고 실행하면 시간이 거의 걸리지 않지만 코드 내에서도 이렇게 복잡한 작업을 수행한다는 것을 깨달았습니다. 이 낮은 수준에서 프로그램이 어떻게 작동하는지 살펴봄으로써 이전에 어떤 문제가 있었고 어떻게 해결했는지 이해할 수 있습니다. 아직은 상상하기 어려운 단계이지만, 많은 분들이 겪고 있는 문제를 해결한 사례들을 듣게 된다면 언젠가는 이 단계에 도달할 수 있을 것이라고 믿습니다.