번역 단계(Phases of translation)
번역 단계(Phases of translation)
C 소스 파일이 컴파일되기까지 어떤 순서로 처리되는지 궁금할 때가 있죠. 컴파일러는 소스 파일을 다음 단계들이 이 정확한 순서로 일어난 것처럼 처리해요. 실제 구현은 동작이 같다면 이 동작들을 합치거나 다르게 처리할 수 있어요.
출처: cppreference
본문
1단계(Phase 1)
- 소스 코드 파일(보통 UTF-8 같은 멀티바이트 인코딩의 텍스트 파일)의 개별 바이트가, 구현이 정의한 방식으로, 소스 문자 집합의 문자로 매핑돼요. 특히 OS에 따라 달라지는 줄 끝 표시(end-of-line indicator)는 개행 문자로 대체돼요.
소스 문자 집합은 멀티바이트 문자 집합으로, 기본 소스 문자 집합을 단일 바이트 부분 집합으로 포함해요. 기본 소스 문자 집합은 다음 96개 문자로 이루어져 있어요:
a) 공백 문자 5개(공백, 가로 탭, 세로 탭, 폼 피드, 새 줄)
b) '0'부터 '9'까지의 숫자 문자 10개
c) 'a'부터 'z'까지와 'A'부터 'Z'까지의 문자 52개
d) 구두점 29개: _ { } [ ] # ( ) < > % : ; . ? * + - / ^ & | ~ ! = , \ " '
- 트라이그래프(trigraph) 시퀀스는 대응하는 단일 문자 표현으로 대체되어요. (C23 이전)
2단계(Phase 2)
- 백슬래시가 줄 끝에(개행 문자 바로 앞에) 나타날 때마다, 백슬래시와 개행이 모두 삭제되어 두 개의 물리적 소스 줄이 하나의 논리적 소스 줄로 결합돼요. 이는 단일 패스(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와 동등해요.
- 이 단계를 거친 뒤 비어 있지 않은 소스 파일이 개행 문자로 끝나지 않으면(원래 개행이 없었든, 백슬래시로 끝났든), 동작은 정의되지 않아요.
3단계(Phase 3)
- 소스 파일은 주석, 공백 문자 시퀀스(공백, 가로 탭, 새 줄, 세로 탭, 폼 피드), 그리고 전처리 토큰으로 분해돼요. 전처리 토큰은 다음과 같아요:
a) 헤더 이름: <stdio.h> 또는 "myfile.h"
b) 식별자
c) 전처리 숫자 - 정수 상수와 부동 상수를 포함하지만, 1..E+3.foo나 0JBK 같은 잘못된 토큰도 포함해요
d) 문자 상수와 문자열 리터럴
e) 연산자와 구두점, 예: +, <<=, <%, ##
f) 다른 어떤 범주에도 맞지 않는 개별 비공백 문자
- 각 주석은 공백 문자 하나로 대체돼요.
- 개행은 유지되고, 비개행 공백 시퀀스가 단일 공백 문자로 축소될 수 있는지는 구현이 정의예요.
입력이 어떤 문자까지 전처리 토큰으로 파싱됐다면, 다음 전처리 토큰은 일반적으로 전처리 토큰을 구성할 수 있는 가장 긴 문자 시퀀스로 취해지는데, 그로 인해 이후 분석이 실패하게 되더라도 그래요. 이를 흔히 최대 먹음(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)
- 전처리기가 실행돼요.
#include지시문으로 도입된 각 파일은 1단계부터 4단계까지 재귀적으로 거쳐요.- 이 단계가 끝나면 모든 전처리 지시문이 소스에서 제거돼요.
5단계(Phase 5)
- 문자 상수와 문자열 리터럴의 모든 문자와 이스케이프 시퀀스가 소스 문자 집합에서 실행 문자 집합으로 변환돼요(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)에서 실행에 필요한 정보를 담아요.