![컴파일링의 각 과정](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjshYt%2FbtsF2PJXbub%2FAqyAryR6Msvcw099xfvXk1%2Fimg.jpg)
![HYUNJZZANG](https://tistory1.daumcdn.net/tistory/3136792/skin/images/main_img.gif)
★KEYWORD★
컴파일 / CPU가 이해할 수 있는 언어로 번역하는 것 / 4가지 과정 / 전처리 / 컴파일 / 어셈블리 / 링킹
컴파일(Compile) 이란?
컴파일은 인간이 이해할 수 있는 언어로 작성된 소스 코드(고수준 언어 : C, C++, Java 등)를 CPU가 이해할 수 있는 언어(저수준 언어 : 기계어)로 번역(변환)하는 작업을 말한다.
우리가 C, C++, Java로 작성하는 소스 코드는 컴퓨터가 이해할 수 없다. 컴퓨터는 0, 1로 이루어진 기계어만 이해할 수 있기 때문이다.
따라서 우리가 작성한 소스 코드를 컴퓨터가 이해할 수 있게 0, 1로 이루어진 기계어로 번역하는 컴파일 과정이 필요하다.
소스 코드는 컴파일을 통해 기계어로 이루어진 실행 파일이 된다. 이 파일을 실행하면 실행 파일 내용이 운영체제의 Loader를 통해 메모리에 적재되어 프로그램이 동작한다.
컴파일의 과정
1. 전처리(Pre-processing) 과정
전처리(Pre-processing) 과정은 전처리기(Preprocessor)를 통해 소스 코드 파일(*.c)을 전처리된 소스 코드 파일(*.i)로 변환하는 과정이다. 이 과정에서는 대표적으로 세가지 작업을 수행한다.
- 주석 제거 : 소스 코드에서 주석을 전부 제거한다. 주석은 사람들이 알아볼 수 있게 남긴 내용이지 컴퓨터가 알 필요는 없기 때문이다.
- 헤더 파일 삽입 : #include 지시문을 만나면 해당하는 헤더 파일을 찾아 헤더 파일에 있는 모든 내용을 복사해서 소스 코드에 삽입한다. 즉, 헤더 파일은 컴파일에 사용되지 않고 소스 코드 파일 내에 전부 복사된다. 헤더 파일에 선언된 함수 원형은 후에 링킹 과정을 통해 실제로 함수가 정의되어 있는 오브젝트 파일(컴파일된 소스 코드 파일)과 결합한다.
- 매크로 치환 및 적용 : #define 지시문에 정의된 매크로를 저장하고 같은 문자열을 만나면 #define 된 내용으로 치환한다. 간단하게 말해 매크로 이름을 찾아서 정의한 값으로 전부 바꿔준다.
2. 컴파일(Compilation) 과정
컴파일(Compilation) 과정은 컴파일러(Compiler)를 통해 전처리된 소스 코드 파일(*.i)을 어셈블리어 파일(*.s)로 변환하는 과정이다.
이 과정에서 우리가 일반적으로 컴파일하면 생각하는 언어의 문법 검사가 이루어진다. 또한 Static한 영역(Data, BSS 영역)들의 메모리 할당을 수행한다.
컴파일러 구조
컴파일러는 세 단계(프론트엔드 — 미들엔드 — 백엔드)로 구성되어 있다
프론트엔드(Front-end)
- 언어 종속적인 부분을 처리한다. 소스 코드가 해당 언어로 올바르게 작성되었는지 확인(어휘/구문/의미 분석)하고 미들엔드에 넘겨주기 위한 GIMPLE 트리(소스 코드를 트리 형태로 표현한 자료 구조)를 생성한다. 이 과정에서 C, C++, Java와 같은 다양한 언어들이 각 언어에 맞게 처리된 후 공통된 중간 표현(IR : Intermediate representation)인 GIMPLE 트리로 변환되므로 언어 종속적인 부분을 처리할 수 있다.
미들엔드(Middle-end)
- 아키텍쳐 비종속적인 최적화를 수행한다. 아키텍쳐 비종속적인 최적화란 CPU 아키텍쳐가 무엇이든(arm, x86 등) 상관없이 할 수 있는 최적화를 말한다. 넘겨받은 GIMPLE 트리를 이용해 아키텍쳐 비종속적인 최적화를 수행한 후 백엔드에서 사용하는 RTL(Register Transfer Language : 고급 언어와 어셈블리 언어의 중간 형태)를 생성한다.
백엔드(Back-end)
- 아키텍쳐 종속적인 최적화를 수행한다. 아키텍쳐 종속적인 최적화란 아키텍쳐 특성에 따라 최적화를 수행하는 것을 말한다. 같은 기능을 수행하는 명령어여도 CPU 아키텍처별로 더욱 효율적인 명령어로 대체하여 성능을 높이는 작업을 예를 들 수 있다. 미들엔드에서 넘겨받은 RTL을 이용해 아키텍쳐 종속적인 최적화를 수행하고 최적화가 완료되면 어셈블리 코드를 생성한다. 아키텍쳐 종속적인 최적화를 수행하면 해당 아키텍쳐만 이해할 수 있는 언어가 되기 때문에 아키텍쳐가 맞지 않으면 어셈블리 코드를 해석할 수 없다.
어셈블리어 정의
기계어는 다른 말로 명령어(Machine Instruction)이라고 부르는데 명령어는 0101010과 같은 이진수로 이뤄진 숫자로 CPU 종류마다 고유한 내용을 가지고 있다. 어셈블리어는 이런 명령어를 사람이 이해할 수 있게 부호화한 것으로 CPU 명령어(기계어)와 1대1로 매칭된다.
많은 컴파일러가 앞서 설명한 세 단계의 구조를 따르고 있지만, 컴파일러마다 차이가 존재한다. GNU에서 만든 C 컴파일러인 gcc는 프론트엔드/미들엔드/백엔드 단계가 깔끔하게 분리되어 있지 않고 의존성이 존재한다. 그에 비해 오픈 소스 C 컴파일러인 Clang(프론트엔드) + LLVM(미들엔드, 백엔드)는 단계가 잘 분리되어 있다.
3. 어셈블리(Assembly) 과정
어셈블리(Assembly) 과정은 어셈블러(Assembler)를 통해 어셈블리어 파일(*.s)을 오브젝트 파일(*.o)로 변환하는 과정이다.
➡️ 오브젝트 파일 : 어셈블리 코드는 이제 더 이상 사람이 알아볼 수 없는 기계어로 변환되는데 이를 오브젝트 코드라 부른다. 오브젝트 코드로 구성된 파일을 오브젝트 파일(Object File)이라 부르며 이 오브젝트 파일은 특정한 파일 포맷을 가진다.
※ 오브젝트 파일 포맷의 종류는 Windows의 경우 PE(Portable Executable), Linux의 경우 ELF(Executable and Linking Format)로 나눠진다.
4. 링킹(Linking) 과정
링킹(Linking) 과정은 링커(Linker)를 통해 오브젝트 파일(*.o)들을 묶어 실행 파일로 만드는 과정이다.
이 과정에서 오브젝트 파일들과 프로그램에서 사용하는 라이브러리 파일들을 링크하여 하나의 실행 파일을 만든다. 이때 라이브러리를 링크하는 방법에 따라 정적 링킹(Static Linking)과 동적 링킹(Dynamic Linking)으로 나눌 수 있다.
링커의 역할
링커의 역할은 크게 심볼 해석과 재배치로 나눌 수 있다.
심볼 해석(Symbol Resolution)
심볼 해석은 각 오브젝트 파일에 있는 심볼 참조를 어떤 심볼 정의에 연관시킬지 결정하는 과정이다.
여러 개의 오브젝트 파일에 같은 이름의 함수 또는 변수가 정의되어 있을 때 어떤 파일의 어떤 함수를 사용할지 결정한다.
재배치(Relocation)
재배치는 오브젝트 파일에 있는 데이터의 주소나 코드의 메모리 참조 주소를 알맞게 배치하는 과정이다.
링커가 컴파일러가 생성한 오브젝트 파일을 모아서 하나의 실행 파일을 만들 때, 각 오브젝트 파일에 있는 데이터의 주소나 코드의 메모리 참조 주소가 링커에 의해 합쳐진 실행 파일에서의 주소와 다르게 때문에 그것을 알맞게 수정해줘야 한다. 이를 위해 오브젝트 파일 안에 재배치 정보 섹션(Relocation Information Section)이 존재한다. 링킹 과정에서 같은 세션끼리 합쳐진 후 재배치가 일어난다.
출처: 위키백과, 나무위키, https://bradbury.tistory.com/226
![HYUNJZZANG](https://tistory1.daumcdn.net/tistory/3136792/skin/images/main_img.gif)
HyunZzang의 프로그래밍 공간 / 함께 공부해요!!
도움이 되셨다면 "좋아요❤️" 또는 "구독👍🏻" 부탁드립니다 :)