C 언어 컴파일 과정 (Compile Process)

한눈에 보기

C 소스 코드가 전처리, 컴파일, 어셈블, 링킹 네 단계를 거쳐 실행 파일이 되는 과정을 단계별 산출물, 목적 파일의 섹션 구조, 정적 링킹과 동적 링킹의 차이와 함께 정리한다.

최종 업데이트 2026.09.17

C 언어 소스 코드는 전처리(Preprocessing), 컴파일(Compile), 어셈블(Assemble), 링킹(Linking)이라는 네 단계를 거쳐 실행 파일로 변환된다. 이 과정을 이해하면 헤더 파일과 매크로가 언제 처리되는지, 목적 파일이 어떻게 하나의 프로그램으로 합쳐지는지, 그리고 링크 오류가 왜 발생하는지를 정확히 설명할 수 있다. 각 단계는 독립적인 산출물을 만들기 때문에 gcc 옵션으로 중간 결과를 직접 확인할 수 있다.

전체 흐름 한눈에 보기

확장자 .c인 소스 파일 하나가 실행 파일이 되기까지의 흐름은 다음과 같다. 전처리기가 소스를 순수한 C 코드로 펼치고, 컴파일러가 이를 어셈블리 코드로 번역하며, 어셈블러가 기계어로 이루어진 목적 파일(Object File)을 만들고, 마지막으로 링커(Linker)가 여러 목적 파일과 라이브러리를 묶어 실행 파일을 완성한다.

단계입력출력gcc 옵션
전처리hello.chello.igcc -E
컴파일hello.ihello.sgcc -S
어셈블hello.shello.ogcc -c
링킹hello.o 및 라이브러리실행 파일gcc

전처리 단계

전처리기(Preprocessor)는 #으로 시작하는 지시문을 텍스트 수준에서 처리한다. #include는 헤더 파일의 내용을 해당 위치에 그대로 복사해 붙여 넣고, #define으로 정의된 매크로는 등장하는 모든 곳에서 치환된다. #ifdef 같은 조건부 컴파일 지시문은 조건에 맞지 않는 코드를 아예 제거한다. 주석도 이 단계에서 사라진다. 결과물인 .i 파일은 지시문이 모두 사라진 순수한 C 코드이며, 헤더가 펼쳐지기 때문에 원본보다 훨씬 길어진다.

#include <stdio.h>
#define SQUARE(x) ((x) * (x))

int main(void) {
    printf("%d\n", SQUARE(5));  /* 전처리 후 ((5) * (5))로 치환된다 */
    return 0;
}

매크로는 단순 텍스트 치환이므로 SQUARE(1 + 2)처럼 인자에 연산이 들어오면 괄호를 빠뜨렸을 때 의도와 다른 결과가 나온다. 위 예시처럼 인자와 전체를 괄호로 감싸는 습관이 필요하다.

컴파일과 어셈블 단계

좁은 의미의 컴파일 단계에서는 전처리된 C 코드를 어셈블리 코드로 번역한다. 이때 어휘 분석(Lexical Analysis), 구문 분석(Syntax Analysis), 의미 분석(Semantic Analysis)을 거치며 문법 오류와 타입 오류가 검출된다. 선언되지 않은 변수 사용 같은 오류는 모두 이 단계에서 잡힌다. 최적화(Optimization)도 대부분 여기서 수행되어 불필요한 연산 제거, 루프 변형 같은 개선이 이루어진다.

어셈블 단계에서는 어셈블러(Assembler)가 어셈블리 코드를 CPU가 이해하는 기계어로 변환해 목적 파일을 만든다. 목적 파일은 리눅스에서는 ELF(Executable and Linkable Format) 형식이며 내부가 섹션(Section)으로 나뉜다. 기계어 명령은 .text 섹션, 초기값이 있는 전역 변수는 .data 섹션, 초기값이 없거나 0인 전역 변수는 .bss 섹션, 문자열 리터럴 같은 읽기 전용 데이터는 .rodata 섹션에 배치된다. .bss는 실제 값이 아니라 크기 정보만 기록되므로 파일 용량을 차지하지 않고, 프로그램 적재 시 0으로 채워진다.

링킹 단계

목적 파일 하나만으로는 실행할 수 없다. printf처럼 다른 파일이나 라이브러리에 정의된 함수의 주소를 아직 모르기 때문이다. 목적 파일은 이런 미해결 참조를 심볼 테이블(Symbol Table)에 기록해 두고, 링커가 여러 목적 파일과 라이브러리를 모아 심볼 해석(Symbol Resolution)과 재배치(Relocation)를 수행한다. 함수를 선언만 하고 정의하지 않으면 컴파일은 통과하지만 링킹에서 정의되지 않은 참조(undefined reference) 오류가 나는 이유가 바로 이것이다.

링킹 방식은 두 가지로 나뉜다. 정적 링킹(Static Linking)은 라이브러리 코드를 실행 파일 안에 복사해 넣으므로 파일이 커지지만 단독 실행이 가능하다. 동적 링킹(Dynamic Linking)은 실행 파일에 참조 정보만 남기고 실행 시점에 공유 라이브러리(.so, .dll)를 메모리에 적재해 연결한다. 여러 프로세스가 같은 라이브러리 코드를 물리 메모리에서 공유할 수 있어 메모리 효율이 좋고, 라이브러리만 교체해도 프로그램 수정 없이 갱신된다는 장점이 있다.

실행 시점의 메모리 배치

완성된 실행 파일을 실행하면 운영체제 로더(Loader)가 각 섹션을 가상 메모리에 사상한다. .text와 .rodata는 읽기 전용 영역으로, .data와 .bss는 쓰기 가능한 데이터 영역으로 올라간다. 여기에 지역 변수와 함수 호출 정보를 담는 스택(Stack), 동적 할당에 쓰이는 힙(Heap)이 추가되어 우리가 흔히 말하는 프로세스 메모리 구조가 완성된다. 컴파일 결과물의 섹션 구조가 곧 실행 중 메모리 배치의 뼈대가 되는 셈이다.

면접에서 이렇게 물어본다

  • C 컴파일 과정 네 단계를 각 단계의 입력과 출력, 하는 일과 함께 설명해 보라.
  • 선언만 있고 정의가 없는 함수를 호출하면 어느 단계에서 어떤 오류가 발생하는가?
  • 정적 링킹과 동적 링킹의 차이와 각각의 장단점은 무엇인가?
  • 초기화된 전역 변수와 초기화되지 않은 전역 변수는 각각 어느 섹션에 배치되며, .bss 섹션이 파일 크기를 차지하지 않는 이유는 무엇인가?
  • #define 매크로 함수와 일반 함수의 차이, 그리고 매크로 사용 시 주의할 점을 설명해 보라.
C / C++ 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기