본문으로 건너뛰기 C++ 컴파일 과정 | 전처리·컴파일·어셈블·링킹 완벽 가이드

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

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

이 글의 핵심

C++ 소스 코드에서 실행 파일까지 전처리 → 컴파일 → 어셈블 → 링킹 4단계입니다. name mangling은 컴파일 단계에서, 심볼 해결은 링킹 단계에서 이루어지며, Makefile·인클루드 경로는 이 과정을 자동화할 때 씁니다.

들어가며

C++ 컴파일 과정소스 코드에서 실행 파일까지 전처리 → 컴파일 → 어셈블 → 링킹 4단계로 이루어집니다. 비유로 말씀드리면, 전처리레시피 준비 (재료 모으기), 컴파일요리 (재료를 음식으로), 어셈블포장 (음식을 용기에), 링킹배달 (여러 요리를 한 세트로)에 가깝습니다.

이 글을 읽으면

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

컴파일 4단계

전체 흐름

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

단계별 설명

단계입력출력역할
전처리.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

다음 단계

참고 자료

  • “Linkers and Loaders” - John R. Levine
  • GCC 문서: https://gcc.gnu.org/onlinedocs/
  • “C++ Primer” - Stanley Lippman 한 줄 정리: C++ 컴파일은 전처리·컴파일·어셈블·링킹 4단계로 이루어지며, 각 단계를 이해하면 빌드 에러를 효과적으로 해결할 수 있다.

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

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


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ 소스 코드에서 실행 파일까지 전처리 → 컴파일 → 어셈블 → 링킹 4단계입니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, compilation, linking, preprocessing, 컴파일 등으로 검색하시면 이 글이 도움이 됩니다.