블로그로 돌아가기

[컴퓨터구조] GCC(GNU Compiler Collection)

4분 소요
[컴퓨터구조] GCC(GNU Compiler Collection)

GCC란?

코딩을 시작하면 우리는 hello.c와 같은 소스 코드 파일을 만든다.

하지만 컴퓨터는 이러한 C언어로 작성된 코드를 이해하지 못하기 떄문에 컴퓨터가 알아들을 수 있는 언어, 즉 기계어로 번역하는 과정이 반드시 필요하다.


이 때 번역가의 역할을 하는 것이 바로 컴파일러이고, GNU/Linux 환경의 대표적인 컴파일러가 바로

GCC(GNU Compiler Collection)​​이다.

⇒ 처음에는 C 언어 전용 컴파일러(GNU C Compiler)였지만, 지금은 다양한 언어(C, C++, Fortran, Go 등)를 지원하는 컴파일러 컬렉션​​이 되었다.


하지만 GCC를 단순한 “번역기”라고 생각하면 곤란하다.

GCC는 사실 소스 코드를 실행 파일로 만드는 전체 과정을 지휘하는 쉐프에 가깝다.

레시피(소스 코드)를 받아 근사한 요리(실행 파일)로 완성하기 까지, 총 4단계의 정교한 과정을 거친다.

소스 코드가 실행 파일이 되기까지의 4단계

우리가 터미널에 gcc hello.c -o hello 라는 간단한 명령을 내리면,

GCC 내부에서는 사실 아래의 4가지 일이 순식간에 일어난다.

1단계: 전처리 (Preprocessing) – 레시피 준비 및 다듬기

  • 역할​​: 본격적인 요리 전에 레시피를 깔끔하게 다듬고 준비하는 단계이다.

  • 작업 내용​​:

    • 주석 제거​: 코드에 달아놓은 주석들(//, /* */)을 모두 제거

    • 헤더 파일 포함 (#include)​​: #include <stdio.h> 같은 지시문을 만나면, stdio.h 파일의 내용을 그대로 복사해서 코드에 붙여넣음

    • 매크로 치환 (#define)​​: #define PI 3.14 처럼 정의된 매크로를 찾아 전부 실제 값(3.14)으로 바꿈

  • 입력​​: hello.c (원본 소스 파일)

  • 출력​​: hello.i (전처리가 완료된 C 소스 파일)

  • 확인 옵션​​: gcc -E hello.c -o hello.i

2단계: 컴파일 (Compilation) – 레시피를 요리 순서로 번역하기

  • 역할​​: 사람이 이해하는 C언어 레시피를 컴퓨터(CPU)가 이해할 수 있는 어셈블리어(Assembly Language)​​로 번역하는 핵심 과정이다.

  • 작업 내용​​:

    • 전처리가 끝난 hello.i 파일을 받아 문법을 검사하고, 코드 최적화 작업을 수행

    • 최종적으로 어셈블리어 코드를 생성

      ⇒ 어셈블리어는 기계어와 1:1로 대응되는, 가장 로우레벨의 언어

  • 입력​​: hello.i (전처리가 완료된 C 소스 파일)

  • 출력​​: hello.s (어셈블리 코드 파일)

  • 확인 옵션​​: gcc -S hello.c -o hello.s

3단계: 어셈블 (Assembly) – 요리 순서를 기계가 쓸 2진수로 변환

  • 역할​​: 어셈블리어를 실제 컴퓨터가 읽을 수 있는 0과 1의 기계어(Machine Code)​​로 변환

  • 작업 내용​​:

    • hello.s 파일을 기계어로 번역하여 오브젝트 파일(Object File)​​을 생성

    • 이 오브젝트 파일은 기계어를 담고 있지만, 아직은 완전한 실행 파일이 X

      printf 같은 함수가 어떤 라이브러리에 있는지에 대한 정보만 표시해둔 '미완성' 상태

  • 입력​​: hello.s (어셈블리 코드 파일)

  • 출력​​: hello.o (오브젝트 파일)

  • 확인 옵션​​: gcc -c hello.c -o hello.o

4단계: 링킹 (Linking) – 완성된 요리들을 합쳐 최종 상차림하기

  • 역할​​: 만들어진 오브젝트 파일과 필요한 라이브러리들을 모두 연결하여 최종 실행 파일을 만드는 마지막 단계

  • 작업 내용​​:

    • hello.o 파일에 'printf 함수가 필요하다'는 표시를 보고, C 표준 라이브러리에서 실제 printf 함수의 기계어 코드를 가져와 합침

    • 만약 여러 개의 소스 파일(a.c, b.c)로 작업했다면, 각각의 오브젝트 파일(a.o, b.o)을 이 단계에서 하나로 합침

    • 모든 조각들이 합쳐져 드디어 우리가 실행할 수 있는 하나의 파일이 탄생!

  • 입력​​: hello.o (오브젝트 파일), 라이브러리 파일 등

  • 출력​​: hello (또는 지정 안 할 시 a.out, 최종 실행 파일)

  • 확인 옵션​​: gcc hello.o -o hello

정적 링킹 vs. 동적 링킹​

라이브러리를 합치는 방식에는 크게 두 가지가 있다.

  • ​정적 링킹 (Static Linking)​

    • 필요한 라이브러리 코드를 전부 복사하여 실행 파일에 포함시키는 방식.

    • ​장점:​ 실행 파일 하나만으로 모든 환경에서 동작하므로 배포가 편리하다.

    • ​단점:​ 파일 크기가 커지고, 라이브러리 업데이트 시 프로그램을 다시 컴파일해야 한다.

  • ​동적 링킹 (Dynamic Linking)​

    • 실행 파일에는 라이브러리의 위치 정보만 기록하고, 프로그램 실행 시점에 운영체제가 제공하는 공유 라이브러리(.so 등)를 가져와 연결하는 방식. (일반적인 기본 방식)

    • ​장점:​ 실행 파일 크기가 작고, 여러 프로그램이 라이브러리를 공유하여 메모리 효율이 좋다.

    • ​단점: 시스템에 해당 라이브러리가 없으면 실행이 불가능하다. (의존성 문제)


유용한 GCC 옵션들

  • o [파일명]: 출력 파일의 이름을 지정 (o hello)

  • g: 디버깅 정보를 포함시킴 ⇒ 나중에 GDB 같은 디버거를 사용할 때 필수적

  • Wall: 컴파일 시 발생할 수 있는 모든 종류의 경고(Warning) 메시지를 띄워줌

    ⇒ 아주 좋은 습관!

  • l[라이브러리 이름]: 특정 라이브러리를 링크할 때 사용 (예: 수학 라이브러리는 lm)

  • O[숫자]: 컴파일러 최적화 레벨을 지정 (O2가 가장 일반적으로 권장됨)

  • static: 정적 링킹 방식으로 컴파일을 수행


앞서 말했듯이 사실 GCC는 전체 과정을 지휘하는 쉐프에 가깝다.

실제로 GCC가 4가지 작업을 전부 혼자 하지는 않고 전처리, 컴파일, 어셈블, 링킹 각 단계에 맞는 cpp, cc1, as, ld 같은 전문가들을 순서대로 호출하여 작업을 지시한다.