icon안동민 개발노트

컴파일과 실행 과정


C++ 프로그램의 생명 주기

 C++ 프로그램이 소스 코드에서 실행 가능한 프로그램이 되기까지의 과정을 생명 주기라고 합니다.

 이 과정은 다음과 같은 단계로 구성됩니다.

 1. 소스 코드 작성

 2. 전처리 (Preprocessing)

 3. 컴파일 (Compilation)

 4. 어셈블 (Assembly)

 5. 링킹 (Linking)

 6. 로딩 (Loading)

 7. 실행 (Execution)

 각 단계를 자세히 살펴보겠습니다.

전처리 과정

 전처리기는 컴파일 전에 소스 코드를 수정하는 역할을 합니다. 주요 작업은 다음과 같습니다.

 1. 주석 제거

 2. #include 지시문 처리 : 헤더 파일의 내용을 소스 코드에 삽입

 3. 매크로 확장 : #define으로 정의된 매크로를 실제 값으로 대체

 4. 조건부 컴파일 : #ifdef, #ifndef, #endif 등의 지시문 처리

예시
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
 
double area = PI * SQUARE(radius);
전처리 후
double area = 3.14159 * ((radius) * (radius));
전처리 결과를 확인하는 명령어 (GCC 기준)
g++ -E sourcefile.cpp

컴파일 과정

 컴파일러는 전처리된 소스 코드를 어셈블리 코드로 변환합니다. 이 과정에서 다음과 같은 작업이 수행됩니다.

  1. 문법 검사 : 코드의 문법적 오류를 찾아냅니다.
  2. 의미 분석 : 변수 타입 체크, 함수 호출의 유효성 검사 등을 수행합니다.
  3. 중간 코드 생성 : 소스 코드를 컴파일러 내부의 중간 표현으로 변환합니다.
  4. 코드 최적화 : 실행 속도를 높이거나 코드 크기를 줄이는 최적화를 수행합니다.
  5. 목적 코드 생성 : 최종적으로 어셈블리 코드를 생성합니다.

 주요 C++ 컴파일러

  • GCC (GNU Compiler Collection)
  • Clang
  • Microsoft Visual C++ Compiler
어셈블리 코드 생성 명령어 (GCC 기준)
g++ -S sourcefile.cpp

어셈블 과정

 어셈블러는 어셈블리 코드를 기계어로 변환합니다. 이 과정의 결과물을 목적 파일(object file)이라고 하며, 확장자는 보통 .o (Unix) 또는 .obj (Windows)입니다.

목적 파일 생성 명령어 (GCC 기준)
g++ -c sourcefile.cpp

링킹 과정

 링커는 여러 목적 파일과 라이브러리를 하나의 실행 파일로 결합합니다. 주요 작업은 다음과 같습니다.

  1. 심볼 해결 : 외부 참조를 실제 메모리 주소로 연결합니다.
  2. 메모리 주소 할당 : 각 섹션(코드, 데이터 등)에 메모리 주소를 할당합니다.
  3. 재배치 : 코드와 데이터의 주소를 조정합니다.

 링킹의 종류

  • 정적 링킹 : 라이브러리 코드를 실행 파일에 포함시킵니다. 실행 파일의 크기가 커지지만, 배포가 간편합니다.
  • 동적 링킹 : 라이브러리를 별도로 유지하고, 실행 시점에 연결합니다. 실행 파일의 크기는 작아지지만, 배포 시 라이브러리도 함께 제공해야 합니다.

실행 파일 구조

 실행 파일은 다음과 같은 주요 섹션으로 구성됩니다.

  1. 텍스트 섹션 : 실행 가능한 코드
  2. 데이터 섹션 : 초기화된 전역 변수와 정적 변수
  3. BSS 섹션 : 초기화되지 않은 전역 변수와 정적 변수
  4. 힙 : 동적으로 할당되는 메모리 영역
  5. 스택 : 지역 변수와 함수 호출 정보를 저장하는 영역
실행 파일 구조 확인 명령어 (Linux)
objdump -h executable

컴파일 명령어 상세 분석

GCC 컴파일 명령어 예시
g++ -Wall -O2 -std=c++17 -c main.cpp -o main.o
g++ main.o -o myprogram

 옵션 설명

  • -Wall : 모든 경고 메시지를 출력합니다.
  • -O2 : 최적화 레벨 2를 적용합니다. (0부터 3까지 있으며, 숫자가 클수록 더 강력한 최적화를 수행)
  • -std=c++17 : C++17 표준을 사용합니다.
  • -c : 컴파일만 수행하고 링크는 하지 않습니다.
  • -o : 출력 파일의 이름을 지정합니다.

빌드 자동화 도구

 대규모 프로젝트에서는 빌드 과정을 자동화하기 위해 다음과 같은 도구들을 사용합니다.

  1. Make : Makefile을 사용하여 빌드 과정을 정의하고 실행합니다.
  2. CMake : 크로스 플랫폼 빌드 시스템으로, 다양한 환경에서 일관된 빌드 스크립트를 사용할 수 있게 해줍니다.
  3. Ninja : 빠른 빌드 속도에 중점을 둔 빌드 시스템입니다.
Makefile 예시
CXX = g++
CXXFLAGS = -Wall -O2 -std=c++17
 
myprogram: main.o helper.o
    $(CXX) $(CXXFLAGS) main.o helper.o -o myprogram
 
main.o: main.cpp
    $(CXX) $(CXXFLAGS) -c main.cpp
 
helper.o: helper.cpp helper.h
    $(CXX) $(CXXFLAGS) -c helper.cpp
 
clean:
    rm -f *.o myprogram

연습 문제

  1. 다음 코드의 전처리 결과를 예측해보세요.
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
 
int main() {
    int x = 5, y = 10;
    int result = MAX(x, y) + SQUARE(3);
    return 0;
}
  1. 컴파일러 최적화 레벨에 따른 실행 속도 차이를 측정하는 프로그램을 작성하고, -O0, -O1, -O2, -O3 옵션으로 각각 컴파일하여 결과를 비교해보세요.
  2. 여러 개의 소스 파일을 사용하는 간단한 프로젝트를 만들고, 이를 빌드하는 Makefile을 작성해보세요.
  3. 동적 링킹과 정적 링킹의 차이점을 설명하고, 각각의 장단점을 논의해보세요.

 참고자료