C++ 컴파일 과정 | 전처리·컴파일·어셈블·링킹 완벽 가이드

C++ 컴파일 과정 | 전처리·컴파일·어셈블·링킹 완벽 가이드

이 글의 핵심

C++ 컴파일 과정 완벽 가이드. 소스 코드에서 실행 파일까지 전처리 → 컴파일 → 어셈블 → 링킹 4단계를 상세히 설명합니다. name mangling, 심볼 해결, 최적화 옵션까지 다룹니다.

들어가며

C++ 컴파일 과정소스 코드에서 실행 파일까지 전처리 → 컴파일 → 어셈블 → 링킹 4단계로 이루어집니다.

비유로 말씀드리면, 전처리레시피 준비 (재료 모으기), 컴파일요리 (재료를 음식으로), 어셈블포장 (음식을 용기에), 링킹배달 (여러 요리를 한 세트로)에 가깝습니다.

이 글을 읽으면

  • 컴파일 4단계를 이해합니다
  • 각 단계의 역할과 산출물을 파악합니다
  • 컴파일러 옵션과 최적화를 익힙니다
  • 링크 에러와 전처리 문제를 해결합니다

목차

  1. 컴파일 4단계
  2. 실전 구현
  3. 고급 활용
  4. 성능 비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

컴파일 4단계

전체 흐름

graph LR
    A[소스 코드<br/>.cpp] --> B[전처리<br/>.i]
    B --> C[컴파일<br/>.s]
    C --> D[어셈블<br/>.o]
    D --> E[링킹<br/>실행 파일]

단계별 설명

단계입력출력역할
전처리.cpp.i#include, #define 처리
컴파일.i.sC++ → 어셈블리
어셈블.s.o어셈블리 → 기계어
링킹.o실행 파일오브젝트 파일 결합

실전 구현

1) 전처리 (Preprocessing)

// main.cpp
#include <iostream>
#define PI 3.14159
#define SQUARE(x) ((x) * (x))

int main() {
    std::cout << "PI: " << PI << std::endl;
    std::cout << "SQUARE(5): " << SQUARE(5) << std::endl;
    return 0;
}
# 전처리 결과 확인
g++ -E main.cpp -o main.i

main.i (일부):

// ... iostream 내용 전체 ...
int main() {
    std::cout << "PI: " << 3.14159 << std::endl;
    std::cout << "SQUARE(5): " << ((5) * (5)) << std::endl;
    return 0;
}

2) 컴파일 (Compilation)

# C++를 어셈블리로
g++ -S main.i -o main.s

main.s (일부):

main:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     edi, OFFSET FLAT:_ZSt4cout
    call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    ; ...
    ret

3) 어셈블 (Assembly)

# 어셈블리를 기계어로
g++ -c main.s -o main.o

main.o: 바이너리 파일 (기계어)

# 오브젝트 파일 확인 (Linux)
objdump -d main.o

# 심볼 확인
nm main.o

4) 링킹 (Linking)

# 오브젝트 파일을 실행 파일로
g++ main.o -o main

# 실행
./main

전체 과정 한 번에

# 4단계를 한 번에
g++ main.cpp -o main

# 단계별 (학습용)
g++ -E main.cpp -o main.i    # 전처리
g++ -S main.i -o main.s      # 컴파일
g++ -c main.s -o main.o      # 어셈블
g++ main.o -o main           # 링킹

고급 활용

1) 여러 파일 컴파일

// util.h
#ifndef UTIL_H
#define UTIL_H

int add(int a, int b);

#endif

// util.cpp
#include "util.h"

int add(int a, int b) {
    return a + b;
}

// main.cpp
#include <iostream>
#include "util.h"

int main() {
    std::cout << add(3, 5) << std::endl;
    return 0;
}
# 각각 컴파일
g++ -c main.cpp -o main.o
g++ -c util.cpp -o util.o

# 링킹
g++ main.o util.o -o myapp

# 실행
./myapp

2) 라이브러리 링크

정적 라이브러리 (.a)

# 정적 라이브러리 생성
ar rcs libmylib.a util.o

# 링크
g++ main.o -L. -lmylib -o myapp

동적 라이브러리 (.so / .dll)

# 동적 라이브러리 생성 (Linux)
g++ -shared -fPIC util.cpp -o libmylib.so

# 링크
g++ main.o -L. -lmylib -Wl,-rpath,. -o myapp

# Windows
g++ -shared util.cpp -o mylib.dll
g++ main.o -L. -lmylib -o myapp.exe

3) 최적화 옵션

# -O0: 최적화 없음 (디버그)
g++ -O0 -g main.cpp -o main_debug

# -O1: 기본 최적화
g++ -O1 main.cpp -o main_o1

# -O2: 권장 최적화 (릴리스)
g++ -O2 main.cpp -o main_o2

# -O3: 최대 최적화
g++ -O3 main.cpp -o main_o3

# -Os: 크기 최적화
g++ -Os main.cpp -o main_os

4) 경고 옵션

# 기본 경고
g++ -Wall main.cpp

# 추가 경고
g++ -Wall -Wextra main.cpp

# 경고를 에러로
g++ -Wall -Wextra -Werror main.cpp

# 특정 경고 비활성화
g++ -Wall -Wno-unused-variable main.cpp

성능 비교

최적화 레벨 비교

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec(1000000);
    
    for (int i = 0; i < 1000000; ++i) {
        vec[i] = i * 2;
    }
    
    long long sum = 0;
    for (int x : vec) {
        sum += x;
    }
    
    std::cout << sum << std::endl;
    
    return 0;
}

컴파일 및 실행:

# -O0
g++ -O0 main.cpp -o main_o0
time ./main_o0

# -O2
g++ -O2 main.cpp -o main_o2
time ./main_o2

# -O3
g++ -O3 main.cpp -o main_o3
time ./main_o3

결과 (GCC 13):

옵션실행 시간바이너리 크기
-O0150ms18KB
-O150ms16KB
-O220ms16KB
-O315ms20KB

분석: -O2가 성능과 크기의 균형


실무 사례

사례 1: 프로젝트 빌드 스크립트

#!/bin/bash

# 변수 설정
CXX=g++
CXXFLAGS="-std=c++17 -Wall -Wextra -O2"
SOURCES="main.cpp util.cpp math.cpp"
OBJECTS="main.o util.o math.o"
TARGET="myapp"

# 컴파일
for src in $SOURCES; do
    $CXX $CXXFLAGS -c $src
done

# 링킹
$CXX $OBJECTS -o $TARGET

# 실행
./$TARGET

사례 2: Makefile

CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
LDFLAGS = -L./lib -lmylib

SOURCES = main.cpp util.cpp math.cpp
OBJECTS = $(SOURCES:.cpp=.o)
TARGET = myapp

all: $(TARGET)

$(TARGET): $(OBJECTS)
	$(CXX) $(OBJECTS) $(LDFLAGS) -o $(TARGET)

%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

clean:
	rm -f $(OBJECTS) $(TARGET)

.PHONY: all clean

사용:

# 빌드
make

# 클린
make clean

# 재빌드
make clean && make

사례 3: CMake

cmake_minimum_required(VERSION 3.10)
project(MyApp)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")

add_executable(myapp
    main.cpp
    util.cpp
    math.cpp
)

target_link_libraries(myapp mylib)

사용:

# 빌드 디렉토리 생성
mkdir build
cd build

# CMake 실행
cmake ..

# 빌드
make

# 실행
./myapp

사례 4: 조건부 컴파일

// config.h
#ifndef CONFIG_H
#define CONFIG_H

#ifdef DEBUG
    #define LOG(x) std::cout << "[DEBUG] " << x << std::endl
#else
    #define LOG(x)
#endif

#ifdef ENABLE_FEATURE_X
    #define FEATURE_X_ENABLED
#endif

#endif

// main.cpp
#include <iostream>
#include "config.h"

int main() {
    LOG("프로그램 시작");
    
#ifdef FEATURE_X_ENABLED
    std::cout << "Feature X 활성화" << std::endl;
#endif
    
    return 0;
}

컴파일:

# DEBUG 모드
g++ -DDEBUG main.cpp -o main_debug

# RELEASE 모드
g++ main.cpp -o main_release

# Feature X 활성화
g++ -DENABLE_FEATURE_X main.cpp -o main_feature_x

트러블슈팅

문제 1: 헤더 미포함

증상: 컴파일 에러

// ❌ 헤더 없음
int main() {
    std::cout << "Hello";  // 에러: 'cout' was not declared
}

// ✅ 헤더 포함
#include <iostream>
int main() {
    std::cout << "Hello";
}

문제 2: 링크 에러 - undefined reference

증상: 링크 에러

// util.h
int add(int a, int b);

// main.cpp
#include <iostream>
#include "util.h"

int main() {
    std::cout << add(3, 5) << std::endl;  // 링크 에러
    return 0;
}

에러:

undefined reference to `add(int, int)'

해결:

// util.cpp 추가
#include "util.h"

int add(int a, int b) {
    return a + b;
}
# 컴파일 및 링크
g++ -c main.cpp -o main.o
g++ -c util.cpp -o util.o
g++ main.o util.o -o myapp

문제 3: 중복 정의

증상: 링크 에러

// header.h
int globalVar = 10;  // ❌ 중복 정의 (여러 .cpp에서 include 시)

// ✅ 선언만
extern int globalVar;

// source.cpp
int globalVar = 10;  // 정의

문제 4: 순환 의존성

증상: 컴파일 에러

// a.h
#ifndef A_H
#define A_H

#include "b.h"

class A {
    B* b;  // B 사용
};

#endif

// b.h
#ifndef B_H
#define B_H

#include "a.h"  // 순환

class B {
    A* a;  // A 사용
};

#endif

해결: 전방 선언

// a.h
#ifndef A_H
#define A_H

class B;  // 전방 선언

class A {
    B* b;
};

#endif

// b.h
#ifndef B_H
#define B_H

class A;  // 전방 선언

class B {
    A* a;
};

#endif

문제 5: 인클루드 경로

증상: 헤더 파일을 찾을 수 없음

# 에러
g++ main.cpp -o main
# fatal error: myheader.h: No such file or directory

# 해결: -I 옵션으로 인클루드 경로 추가
g++ -I./include main.cpp -o main

문제 6: 라이브러리 경로

증상: 라이브러리를 찾을 수 없음

# 에러
g++ main.o -lmylib -o myapp
# /usr/bin/ld: cannot find -lmylib

# 해결: -L 옵션으로 라이브러리 경로 추가
g++ main.o -L./lib -lmylib -o myapp

마무리

C++ 컴파일 과정전처리 → 컴파일 → 어셈블 → 링킹 4단계로 이루어집니다.

핵심 요약

  1. 컴파일 4단계

    • 전처리: #include, #define 처리
    • 컴파일: C++ → 어셈블리
    • 어셈블: 어셈블리 → 기계어
    • 링킹: 오브젝트 파일 결합
  2. 파일 확장자

    • .cpp: 소스 코드
    • .i: 전처리 결과
    • .s: 어셈블리
    • .o: 오브젝트 파일
    • 실행 파일: a.out (Linux), .exe (Windows)
  3. 최적화 옵션

    • -O0: 디버그
    • -O1: 기본
    • -O2: 권장 (릴리스)
    • -O3: 최대
  4. 링크 에러

    • 함수 구현 누락
    • 라이브러리 미링크
    • 심볼 미정의

컴파일러 옵션 치트시트

# 표준 지정
g++ -std=c++17 main.cpp

# 경고
g++ -Wall -Wextra -Werror main.cpp

# 최적화
g++ -O2 main.cpp

# 디버그
g++ -g main.cpp

# 전처리 정의
g++ -DDEBUG main.cpp

# 인클루드 경로
g++ -I./include main.cpp

# 라이브러리 경로
g++ main.o -L./lib -lmylib -o myapp

# 동적 라이브러리 경로 (Linux)
g++ main.o -L./lib -lmylib -Wl,-rpath,./lib -o myapp

다음 단계

  • 링킹: C++ 링킹
  • Name Mangling: C++ Name Mangling
  • Makefile: C++ Makefile
  • CMake: C++ CMake

참고 자료

한 줄 정리: C++ 컴파일은 전처리·컴파일·어셈블·링킹 4단계로 이루어지며, 각 단계를 이해하면 빌드 에러를 효과적으로 해결할 수 있다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Linking | “링킹” 가이드
  • C++ Name Mangling | “이름 맹글링” 가이드
  • C++ Makefile | “Make 빌드” 가이드
  • C++ Include Path | “인클루드 경로” 가이드

관련 글

  • C++ CMake |
  • C++ Linking |
  • C++ Macro Programming |
  • C++ Name Mangling |
  • C++ One Definition Rule |