Hacking
home

C언어 초보에서 중수까지2 - 컴파일 파이프라인

컴파일 파이프라인

C 파일을 컴파일 할 때 4 가지 요소로 구성된 파이프라인으로 진입하여 각 작업을 수행한다.
전처리기
컴파일러
어셈블러
링커

1. 전처리기

전처리기는 C 언어 문법을 전혀 모르는 상태에서 오직 전처리기 지시자 텍스트 치환 작업만 수행한다.
#define jjangu 1234 Hello, jjangu
C
복사
해당 소스코드를 gcc -E a.c 커맨드로 전처리하면 다음과 같은 결과가 나온다.
# 1 "a.c" # 1 "<built-in>" # 1 "<command-line>" # 31 "<command-line>" # 1 "/usr/include/stdc-predef.h" 1 3 4 # 32 "<command-line>" 2 # 1 "a.c" Hello, 1234
C
복사

2. 컴파일러

컴파일러는 전처리기가 준비한 변환 단위를 받아 해당하는 어셈블리 명령어를 생성한다. gcc(C 컴파일러)가 특정 아키텍처에 관한 올바른 어셈블리 코드를 생성하기 위해 2단계로 나누어 컴파일을 진행한다.
1.
컴파일러 프런트엔드: 변환 단위 파싱해 AST로 변환
2.
컴파일러 백엔드: AST를 사용해 대상 아키텍처에 맞는 어셈블리 명령어 생성

추상 구문 트리

컴파일러 프런트엔드는 C의 문법에 따라 소스 코드를 분석해 중간 단계 자료 구조를 만들고, 이 결과를 아키텍처에 의존적이지 않은, 트리 모양의 자료 구조에 결과값을 저장하는데, 이를 AST라고 한다.
AST가 일단 만들어지면 컴파일러 백엔드가 AST를 최적화 하고, 대상 아키텍처에 대한 어셈블리 코드를 생성한다.
int main() { int var1 = 1; double var2 = 2.5; int var3 = var1 + var2; return 0; }
C
복사
다음 소스 코드 내에서 clang을 이용해 AST 덤프를 해보자.
clang은 llvm 컴파일러 백엔드를 위한 C 컴파일러 프런트엔드이다.(llvm AST는 llvm IR이라 부른다.)

3. 어셈블러

어셈블러는 어셈블리 코드를 기계 수준 명령어를 포함하는 목적 파일로 만든다.
만약 같은 아키텍처이지만 서로 다른 운영체제일 경우 생성된 목적 파일은 다를 수 있다. 즉 기계 수준 명령어를 목적 파일에 저장할 때, 각 운영체제는 고유한 특정 이진 파일 포맷 또는 목적 파일 포맷을 정의한다. 리눅스에서는 ELF 파일 형식을 사용한다. 즉, 리눅스에서는 어셈블러가 ELF 목적 파일을 만든다.

4. 링커

링커는 만들어진 재배치 가능한 목적 파일로부터 실행 파일, 정적 라이브러리, 동적 라이브러리 또는 공유 목적 파일을 만드는 역할을 한다.
실행 파일: .out, .exe
정적 라이브러리: .a, .lib
동적 라이브러리 또는 공유 목적 파일: .so, .dylib, .dll
실행 가능한 목적 파일은 프로세스로서 실행될 수 있다. 이 파일에는 기계 수준의 명령어가 실행될 진입점이 있어야 하는데, 링커가 main 함수를 플랫폼에 따라 다른 명령어 묶음으로 준비한다.