[컴퓨터구조] GCC(GNU Compiler Collection)
![[컴퓨터구조] GCC(GNU Compiler Collection)](https://firebasestorage.googleapis.com/v0/b/cruz-lab.firebasestorage.app/o/images%2Fheroes%2Fhero-1766482298725.webp?alt=media&token=3adb0691-11d2-4c8a-862d-6658fba98c6d)
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 같은 전문가들을 순서대로 호출하여 작업을 지시한다.