C++

C++ 컴파일 과정

joonior 2025. 3. 24. 15:49
#include "Vector.h"

void Vector::someFunction() {
    // 구현 내용
}​
add:
    mov eax, edi
    add eax, esi
    ret

컴파일은 크게 네 단계로 분류된다.

전처리(Preprocessing) → 컴파일(Compiling) → 어셈블(Assembling) → 링크(Linking)

 

1. 전처리

전처리기는 #include, #define, #ifdef 등의 전처리 지시문을 처리한다.

#include <iostream>

#define PI 3.14

int main() {
    std::cout << "PI: " << PI << std::endl;
    return 0;
}

가령 이런 코드가 있을 때, #include 된 iostream 전체가 iostream 헤더 파일에 있는 내용으로 치환되고, 코드 내의 PI도 전부 3.14로 치환된다.

// <iostream> 헤더의 모든 내용이 여기에 포함됨

int main() {
    std::cout << "PI: " << 3.14 << std::endl;
    return 0;
}

 

2. 컴파일

컴파일러는 전처리된 코드를 어셈블리 코드로 변환한다.

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

가령 이러한 코드는 아래의 어셈블리 코드로 변경된다.

add:
    mov eax, edi
    add eax, esi
    ret

 

3. 어셈블

어셈블러가 어셈블리 코드를 기계어로 변환하여 객체 파일을 생성한다. (.o/.obj 확장자를 가짐)

g++ -c main.cpp -o main.o

 

4. 링크

링커는 여러 개의 객체 파일(.o/.obj 파일)을 연결하여 실행 가능한 실행파일(executable)을 생성한다.

여기서 중요한 동작은 미해결 심볼(undefined symbol)을 찾아서 연결하는 것이다. 

 

가령, 아래 test.cc를 컴파일하는 하는 단계에서 내부에 someFunction()이 호출된다는 심볼 정보가 포함된다. 하지만 정의가 없으므로 "이 함수의 실제 구현은 나중에 제공될 것"이라는 정보만 남기고 넘어간다.

test.o에는 "someFunction()은 어딘가에 정의되어 있어야 한다" 라는 미해결 심볼(undefined symbol)로 기록된다. 

 

test.cc

#include "Vector.h"

int main() {
    Vector v;
    v.someFunction();
    return 0;
}

 

이후, Vector.cc를 컴파일하는 과정에서는 someFunction()의 실제 구현(정의)가 포함되는데, 이때 Vector.o에는 "someFunction()의 실제 정의가 여기에 있다" 라는 심볼 정보가 포함된다.

 

Vector.cc

#include "Vector.h"

void Vector::someFunction() {
    // 구현 내용
}

 

컴파일이 끝난 후, 실행 파일을 만들기 위해 링커(linker) 가 각 .o 파일을 조합한다.

링커는 test.o의 미해결 심볼을 확인하고, 다른 객체 파일(Vector.o)에서 해당 심볼을 찾는다.

  1. test.o 내부에서 someFunction()이 호출되었지만 정의가 없음을 확인.
  2. Vector.o를 확인하여 someFunction()의 정의가 있음을 발견.
  3. test.o에서 someFunction()을 호출하는 부분을 Vector.o의 정의로 채움.

결과적으로 test.o와 Vector.o가 결합되어 실행 가능한 test 실행 파일이 생성된다.