빌드 과정은 크게 전처리, 컴파일, 어셈블, 링크 단계로 나뉩니다.
전처리 단계에서 컴파일러는 다음과 같은 일을 합니다:
- 소스 코드 내 #로 시작하는 전처리 지시문 처리 하여 전처리된 코드 생성
#include <iostream> // iostream 이라는 파일 내용을 다시 전처리 후 복붙
#define VALUE 10 // 코드 내 모든 "VALUE" 가 "10" 으로 다시 타이핑됨
const int value = VALUE; // 예) const int value = 10 으로 변경됨
#ifdef, #ifnedf, #endif 등 // 조건부로 일부 코드를 컴파일러가 못 보게 함.
#ifndef HEADER_GUARD_H // 예) 헤더가드
#define HEADER_GUARD_H
#endif // HEADER_GUARD_H
컴파일 단계에서 컴파일러는 다음과 같을 일을 합니다:
- 전처리된 코드를 어셈블리 코드 (.s 또는 .asm) 로 변환
- 어셈블리 코드 구성:
- 명령어와 데이터로 구성됨
- 크게 다음 section 으로 나눔
- .text: 명령어 세션
- .data: 정적 데이터
- .bss: 초기화 안된 공간만 할당된 데이터
- .rodata: 읽기 전용 데이터
- section 에 있는 데이터는 고정된 프로세스 메모리에 위치
- 하드웨어 종속적인 어셈블리 코드 생성 (x86, ARM, RISC-V)
어셈블리 단계에서 컴파일러 (어셈블러) 는 다음과 같은 일을 합니다:
- 어셈블리 코드를 기계어로 변환하여 오브젝트 파일 (.o 또는 .obj) 생성
- 오브젝트 파일 구성:
- CPU 실행 코드 (기계어)
- 심볼 정보 (메타데이터): 추후 링커가 사용하기 위한 정보 기록
- 변수 이름, 주소, 크기, 타입
- 함수 이름, 주소 등
- 클래스 이름, 주소 등
- 외부 심볼 정보
- 오브젝트 파일 형식은 OS 종속적:
- linux는 ELF 형식을 따름
- windows는 COFF 형식을 따름
- 오브젝트 파일 형식이 있고 실행 파일 형식이 존재함
링크 단계에서는 컴파일러 (링커) 는 다음과 같은 일을 합니다:
- 실행 파일의 메모리 레이아웃 구성: 실행 프로그램을 켰을 때 의도된 프로세스 메모리 배치
- 여러 오브젝트 파일 내 각 section 별 크기랑 위치 분석해 section 별 공간 재배치
- 심볼 테이블 병합:
- 여러 오브젝트 파일의 심볼 테이블 병합
- 외부 심볼 정보를 읽어들여서 미해결 심볼 최신화
- 참조 주소 업데이트 (주소 재배치):
- 실제 심볼이 올라온 주소를 기입
- 동적 라이브러리 내용 (.dynamic) 추가:
- 추후 로더에 의해 올라오는 코드 찾아가기 위함
- 엔트리 포인트 기입:
- 추후 로더가 어디서부터 실행해야 할지 알기 위해
- 위 메모리 레이아웃을 기반으로 실행 파일 생성
- 메모리 레이아웃 형식도 OS 종속적:
- linux 로더는 실행 파일을 ELF 형식이라고 가정하고 실행
- 오브젝트 파일과 실행 파일의 차이점:
- 오브젝트 파일:
- 링커에 사용되기 위함
- 재배치 가능
- 미해결 심볼 포함
- 실행 파일:
- 로더에 사용되기 위함
- 심볼 모두 해결
- 오브젝트 파일: