번역 단계(Phases of translation)

번역 단계(Phases of translation)

C 소스 파일이 컴파일되기까지 어떤 순서로 처리되는지 궁금할 때가 있죠. 컴파일러는 소스 파일을 다음 단계들이 이 정확한 순서로 일어난 것처럼 처리해요. 실제 구현은 동작이 같다면 이 동작들을 합치거나 다르게 처리할 수 있어요.

출처: cppreference

본문

1단계(Phase 1)

  1. 소스 코드 파일(보통 UTF-8 같은 멀티바이트 인코딩의 텍스트 파일)의 개별 바이트가, 구현이 정의한 방식으로, 소스 문자 집합의 문자로 매핑돼요. 특히 OS에 따라 달라지는 줄 끝 표시(end-of-line indicator)는 개행 문자로 대체돼요.

소스 문자 집합은 멀티바이트 문자 집합으로, 기본 소스 문자 집합을 단일 바이트 부분 집합으로 포함해요. 기본 소스 문자 집합은 다음 96개 문자로 이루어져 있어요:

a) 공백 문자 5개(공백, 가로 탭, 세로 탭, 폼 피드, 새 줄) b) '0'부터 '9'까지의 숫자 문자 10개 c) 'a'부터 'z'까지와 'A'부터 'Z'까지의 문자 52개 d) 구두점 29개: _ { } [ ] # ( ) < > % : ; . ? * + - / ^ & | ~ ! = , \ " '

  1. 트라이그래프(trigraph) 시퀀스는 대응하는 단일 문자 표현으로 대체되어요. (C23 이전)

2단계(Phase 2)

  1. 백슬래시가 줄 끝에(개행 문자 바로 앞에) 나타날 때마다, 백슬래시와 개행이 모두 삭제되어 두 개의 물리적 소스 줄이 하나의 논리적 소스 줄로 결합돼요. 이는 단일 패스(single-pass) 연산이에요. (여기서 \는 실제 백슬래시 문자를 뜻해요.)
#include <stdio.h>

#define PUTS p\
u\
t\
s
/* Line splicing is in phase 2 while macros
 * are tokenized in phase 3 and expanded in phase 4,
 * so the above is equivalent to #define PUTS puts
 */

int main(void)
{
 /* Use line splicing to call puts */ PUT\
S\
("Output ends here\\
0Not printed" /* After line splicing, the remaining backslash
               * escapes the 0, ending the string early.
               */
);
}

주석에 적힌 대로, 줄 결합(line splicing)은 2단계에서 일어나고 매크로는 3단계에서 토큰화되어 4단계에서 확장되므로, 위의 PUTS 정의는 #define PUTS puts와 동등해요.

  1. 이 단계를 거친 뒤 비어 있지 않은 소스 파일이 개행 문자로 끝나지 않으면(원래 개행이 없었든, 백슬래시로 끝났든), 동작은 정의되지 않아요.

3단계(Phase 3)

  1. 소스 파일은 주석, 공백 문자 시퀀스(공백, 가로 탭, 새 줄, 세로 탭, 폼 피드), 그리고 전처리 토큰으로 분해돼요. 전처리 토큰은 다음과 같아요:

a) 헤더 이름: <stdio.h> 또는 "myfile.h" b) 식별자 c) 전처리 숫자 - 정수 상수와 부동 상수를 포함하지만, 1..E+3.foo0JBK 같은 잘못된 토큰도 포함해요 d) 문자 상수와 문자열 리터럴 e) 연산자와 구두점, 예: +, <<=, <%, ## f) 다른 어떤 범주에도 맞지 않는 개별 비공백 문자

  1. 각 주석은 공백 문자 하나로 대체돼요.
  2. 개행은 유지되고, 비개행 공백 시퀀스가 단일 공백 문자로 축소될 수 있는지는 구현이 정의예요.

입력이 어떤 문자까지 전처리 토큰으로 파싱됐다면, 다음 전처리 토큰은 일반적으로 전처리 토큰을 구성할 수 있는 가장 긴 문자 시퀀스로 취해지는데, 그로 인해 이후 분석이 실패하게 되더라도 그래요. 이를 흔히 최대 먹음(maximal munch)이라고 불러요:

int foo = 1;
// int bar = 0xE+foo; // error: invalid preprocessing number 0xE+foo
int bar = 0xE/*Comment expands to a space*/+foo; // OK: 0xE + foo
int baz = 0xE + foo; // OK: 0xE + foo
int pub = bar+++baz; // OK: bar++ + baz
int ham = bar++-++baz; // OK: bar++ - ++baz
// int qux = bar+++++baz; // error: bar++ ++ +baz, not bar++ + ++baz
int qux = bar+++/*Saving comment*/++baz; // OK: bar++ + ++baz

최대 먹음 규칙의 유일한 예외는 다음과 같아요:

  • 헤더 이름 전처리 토큰은 #include 또는 #embed(C23부터) 지시문 안, __has_include__has_embed 표현식(C23부터), 그리고 #pragma 지시문 안의 구현이 정의한 위치에서만 형성돼요.
#define MACRO_1 1
#define MACRO_2 2
#define MACRO_3 3
#define MACRO_EXPR (MACRO_1 <MACRO_2> MACRO_3) // OK: <MACRO_2> is not a header-name

<MACRO_2>가 헤더 이름으로 해석되지 않는 건, 헤더 이름 토큰이 #include 등으로 시작하는 지시문 밖에서는 형성될 수 없기 때문이에요.

4단계(Phase 4)

  1. 전처리기가 실행돼요.
  2. #include 지시문으로 도입된 각 파일은 1단계부터 4단계까지 재귀적으로 거쳐요.
  3. 이 단계가 끝나면 모든 전처리 지시문이 소스에서 제거돼요.

5단계(Phase 5)

  1. 문자 상수와 문자열 리터럴의 모든 문자와 이스케이프 시퀀스가 소스 문자 집합에서 실행 문자 집합으로 변환돼요(1단계에서 나열한 기본 소스 문자 집합의 96개 문자가 모두 단일 바이트 표현을 가지기만 하면 UTF-8 같은 멀티바이트 문자 집합일 수 있어요). 이스케이프 시퀀스가 지정한 문자가 실행 문자 집합의 구성원이 아니면 결과는 구현이 정의지만, 널(와이드) 문자가 아니라는 것은 보장돼요.

참고: 이 단계에서 수행되는 변환은 일부 구현에서 명령줄 옵션으로 제어할 수 있어요. gcc와 clang은 -finput-charset으로 소스 문자 집합의 인코딩을, -fexec-charset-fwide-exec-charset으로 인코딩 접두사가 없는(C11부터) 문자열 리터럴과 문자 상수의 실행 문자 집합 인코딩을 지정해요.

6단계(Phase 6)

인접한 문자열 리터럴이 연결돼요.

7단계(Phase 7)

컴파일이 일어나요: 토큰이 구문·의미적으로 분석되고, 번역 단위(translation unit)로 변환돼요.

8단계(Phase 8)

링킹이 일어나요: 외부 참조를 만족시키는 데 필요한 번역 단위와 라이브러리 구성 요소가 프로그램 이미지로 모아지고, 그 이미지는 실행 환경(OS)에서 실행에 필요한 정보를 담아요.

더 알아보기

  • 전처리기: 4단계에서 실행되는 매크로·조건 컴파일 지시문.
  • 매크로 정의(#define): 3단계 토큰화와 4단계 확장으로 이어지는 핵심 기능.
  • 이스케이프 시퀀스: 5단계에서 소스 문자 집합에서 실행 문자 집합으로 변환되는 대상.