1. 프로그램 생성 과정
프로그램 생성 과정
- 우리는 프로그램을 만들기 위해 프로그래밍 언어를 활용해 “소스코드”를 작성한다.
- 소스코드는 4가지 과정을 거쳐 실행가능한 프로그램을 만든다.

① 전처리 과정
전처리기를 통해 헤더파일 혹은 메크로를 치환하여, 결과를 .i 파일 확장자 명으로 저장한다.
② 컴파일 과정
컴파일러는 저수준의 언어인 어셈블리어로 컴파일 후, 결과를 .s 파일 확장자 명으로 저장한다.
③ 어셈블 과정
어셈블러는 저수준의 언어인 어셈블리어를 링커가 읽을 수 있는 목적파일로 변환해, 결과를 .o 파일 확장자 명으로 저장한다.
④ 링킹 과정
링커는 파일 확장자가 .o 인 목적파일들을 하나로 묶어 실행파일 (a.out)로 생성한다.
위 4가지 동작을 Compiler Collection (컴파일러 모음집)이라고 부르며, 잘 알려진 모음집으로 GCC (GNU Compiler Collection)과 LLVM (Low Level Virtual Machine)이 있다.
2. 컴파일러의 동작과정
다음 그림은 GCC Compiler 기준으로 작성되었으며 LLVM 동작도 이와 비슷하다. 컴파일 과정은 Front-end → Middle-end → Back-end로 나눠져 있다.

1. Front-end
Front-end는 전처리기 동작을 통해 변환된 소스코드가 올바르게 작성되었는지 확인하는 과정이다. 총 4가지 과정(어휘 분석 → 구문 분석 → 의미 분석 → 중간 표현 생성)을 거치게 되고, 올바르다고 판단한 소스코드를 트리형태 (GIMPLE)로 변환한다.

- 4가지 동작 과정
- 어휘 분석
- 소스코드를 의미 있는 최소 단위인 토큰 단위로 나눈다.
- 구문 분석
- 문법적 오류를 검출하기 위해, 토큰을 파서 트리 형태로 변환한다.
- 의미 분석
- 파서 트리를 순회하며, 의미상 오류를 검출한다. 보통 함수의 매개변수를 잘못 사용하거나 자료형 타입을 잘못 사용하는 것들이 검출된다.
- 중간 표현 생성
- 언어의 독립 특성을 제공하는 GIMPLE Tree를 생성한다
- 어휘 분석
GIMPLE Tree [ChatGPT]
"GIMPLE Tree"는 GNU Compiler Collection (GCC) 내부에서 사용하는 중간 표현(IR)의 한 형태입니다. GCC는 다양한 프로그래밍 언어를 컴파일하는 데 사용되는 자유-오픈 소스 컴파일러 모음이며, GIMPLE은 최적화 및 타겟 코드 생성 과정에서 중요한 역할을 합니다.
GIMPLE은 3주소 코드 형태로, 각 명령어가 최대 세 개의 오퍼랜드를 가질 수 있는 단순화된 중간 코드입니다. 이 형태는 프로그램의 복잡성을 줄이고, 컴파일러가 최적화를 더 쉽게 수행할 수 있도록 돕습니다. GIMPLE 형식으로 변환된 코드는 여러 최적화 단계를 거친 후, 타겟 아키텍처의 기계어 코드로 변환됩니다.
GIMPLE의 주요 특징은 다음과 같습니다:
단순화된 표현: 모든 복잡한 표현식과 연산은 더 간단한 연산으로 분해됩니다. 예를 들어, 복잡한 수학적 표현은 여러 단계의 간단한 연산으로 나누어집니다.
SSA 형식: GIMPLE은 종종 정적 단일 할당(Static Single Assignment, SSA) 형태로 변환됩니다. SSA는 각 변수가 단 한 번만 할당되는 형식으로, 이는 최적화 과정에서 변수의 사용과 재할당을 추적하기 쉽게 만듭니다.
최적화 용이: GIMPLE로 변환함으로써, 컴파일러는 다양한 프로그램 최적화 기법을 더 쉽게 적용할 수 있습니다. 예를 들어, 불필요한 코드 제거, 루프 최적화, 인라이닝 등이 있습니다.
GIMPLE은 GCC의 중간 단계에서 주로 사용되며, 개발자가 직접 작업하는 일은 드뭅니다. 그러나 컴파일러의 작동 방식을 이해하고, 성능을 최적화하거나 특정 최적화의 영향을 분석할 때 중요한 역할을 합니다. GCC를 사용하는 개발자는 -fdump-tree-gimple 같은 컴파일러 옵션을 사용하여 소스 코드가 GIMPLE 형태로 어떻게 변환되는지 볼 수 있습니다, 이를 통해 프로그램의 컴파일 과정을 더 깊이 이해할 수 있습니다.
2. Middle-end
Middle-end에서는 Front-end 단에서 도출된 GIMPLE Tree를 SSA 형태로 변환한 후 아키텍처 의존성을 없애는 작업을 진행한다. 이후, 고급 언어와 어셈블리어의 중간단계인 RTL(Register Transfer Language)을 생성한다.
해당 단계에서 Optimization이 일어나며, 컴파일러 모음집에 따라 성능차이가 발생할 수 있는 부분이다.

SSA (Static Single-Assignment)
데이터 흐름분석과 코드 최적화를 위해 사용되는 중간표현(IR)이다. 보통 정적으로 값과 데이터 타입을 결정하기 위해 변수의 배정 또는 흐름을 분리하는 형태에 사용된다.
3. Back-end
Back-end 부분에서는 2가지 최적화가 일어난다.
- RTL Optimizer에 의한 RTL 최적화 (아키텍처 의존성 없음)
- 아키텍처 별 코드 최적화

1~2번 최적화가 완료되면 Code Generator를 통해 .s 확장자를 가진 어셈블리 코드를 만든다. 컴파일의 기본 골격은 동일하며, 컴파일러 모음집 별 최적화 혹은 아키텍처 확장성 등에 따라 다르게 구성될 수 있다.
3. GCC 컴파일러
GCC란 GNU Compiler Collection의 약자이다. 원래는 C언어를 컴파일할 수 있는 컴파일러만 존재해 GNU C Compiler라는 이름이 모음집으로 확장(Java, C++)되어 사용되고 있다.
GCC 컴파일러로 다른 언어를 사용하기 위해서는 라이브러리를 추가해야 된다.
# -lstdc++ 이라는 옵션을 추가해야됨.
$ gcc -o "filename" "filename.cpp" -lstdc++
GCC는 컴파일러 모음집으로 단순한 컴파일 과정을 넘어 전처리 동작, 어셈블 동작, 링킹 동작을 같이 수행해 binary file 즉, 실행 가능한 파일을 만드는 역할을 한다.
또한, GCC는 Front-end부터 Back-end까지 모두 자체적으로 개발해 제공되고 있으며, GPLv3 인증으로 모든 소스가 공개되어 있다. 해당 컴파일러 모음집을 사용할 경우 사용된 소스를 무조건 공개해야 되는 의무를 가지게 된다.
4. LLVM (Low-Level Virtual Machine)
통상적으로 LLVM은 컴파일러라고 불리지 않는다. GCC 같은 경우 원래 컴파일을 전용으로 만들어진 이름 (GNU C Compiler)이라 GCC 컴파일러라고 부르지만, LLVM 같은 경우 하나의 프로젝트 이름 즉, 컴파일을 가능하게 하는 모음집 정도로 생각하면 되며, 보통 LLVM 프로젝트라고 부른다.
GCC 컴파일러와 내부적 최적화 등에서는 차이가 있을 수 있지만, 기본 골격 구조는 동일하다.

- LLVM 구조
- LLVM도 기존 컴파일 구조와 같이 Front-end, Middle-end, Back-end 3개로 구분된다.


- Front-end
- Object-C, Swift, Java, Python 등 고급 언어를 파싱 하여 IR(Intermediate Representation)를 만든다.
- Clang, GNU Compiler Collection 파서를 기반으로 한다.
- Middle-end
- 변환된 IR을 최적화한다.
- Optimizer는 중복 코드, In-line 함수, 데드 루프 삭제, 제어 흐름 그래프 단순화 등을 수행한다.
- Back-end
- IR을 타겟 아키텍처에 맞는 기계어 바이너리 코드로 변환한다.
※ IR (Intermediate Representation)
LLVM에서 사용하는 저수준 중간 표현으로 독립적인 어셈블리 언어라고 볼 수 있다. 이러한 특성으로 모든 프로그래밍 언어(C, C++, Objective-C, Ada, Fortran 등) 및 플랫폼(x86, x86-64, PowerPC, ARM, Thumb, SPARC, Xcore 등)과 호환이 가능하다. 즉, 언어와 플랫폼에서 독립적이라고 할 수 있다.

5. GCC와 LLVM의 차이점
LLVM은 Optimizer가 IR(Intermediate Representaion)을 받아서 처리한다. IR은 RISC(Reduced Instruction Set Computing) 셋으로 표현된 저수준 언어이며, IR을 VM이나 인터프리터가 실행할 수 있기 때문에 GCC가 지원하지 못하는 VM 및 인터프리터 방식 언어도 지원할 수 있다.
또한, LLVM은 IR을 받아서 처리하기 때문에 사용하고 싶은 언어의 프런트엔드만 구현하면 Optimizer와 백엔드는 공유해서 사용할 수 있다. GCC도 프런트엔드를 추가하면 가능하지만 LLVM은 훨씬 쉬운 코드 베이스로 되어 있다.
LLVM은 Front-end로 Clang, Swift 등을 사용할 수 있다.

6. Clang 컴파일러
Clang이란 C, C++, Objective-C, Objective-C++ 언어를 컴파일하기 위한 LLVM에 속한 프런트엔드 컴파일러이다. GCC와 호환되며 더 나은 진단, 더 나은 IDE Integration, 상용 프로덕트와 호환되는 라이센스, 빠르고 사용하기 쉬운 컴파일러를 지향해서 만들게 되었다고 한다.
x86-32, x86-64, ARM 등을 지원하며 실제로 Chrome이나 Firefox 같은 성능에 크리티컬 한 소프트웨어의 빌드에도 사용된다.
GCC와 Clang의 차이
- GCC
기본적으로 GCC는 더 많은 프로그래밍 언어, 아키텍처를 지원한다. Clang에서 지원하는 C/C++/Objective-C/Objective-C++ 외에 Java/Ada/Fortran/Go 등을 지원한다.
- Clang
Clang은 C언어에 특화되어 있다. Clang은 GCC에 비해 컴파일 속도가 빠르고 메모리 사용량이 적다. 라이브러리 기반의 모듈식 디자인으로 IDE 통합이 용이하고 더 나은 오류 진단을 제공한다. → Clang을 개발한 목적이다.
제일 중요한 문제일 수가 있는데, GCC와 Clang은 라이센스가 다르다. Clang은 BSD와 유사한 LLVM Apache 2 라이선스를 사용하고 GCC는 GPLv3이다. GPL을 따르는 GCC를 사용한다면 소스코드를 공개해야 돼서 실제 상용코드에 사용하기 힘들 수 있다.
종합하면, GCC는 더 오래되었고 더 안정적이며 더 많은 언어와 아키텍처에 호환되지만 Clang에 비해 느리고 메모리 사용량이 많을 수 있다. 또한 소스코드를 공개해야 하며 엄격한 규칙을 따라야 하는 GPL을 따르기 때문에 요구조건에 따라 GCC, Clang을 고려하여 사용한다.
출처 및 참고자료
- https://llvm.org/
- https://0xd00d00.github.io/2022/05/29/gcc_clang.html
- https://velog.io/@qwer15417/LLVM을-이해하기-위한-나름의-정리
- https://blog.gopheracademy.com/advent-2018/llvm-ir-and-go/
- https://zeddios.tistory.com/1175
- https://jeha.tistory.com/entry/LLVM
- https://pangyoalto.com/clang-and-optimization/
- https://growingdev.blog/entry/개발-환경-LLVM과-Clang에-대해서
'1. Computer Science' 카테고리의 다른 글
| 프로세스 상태 (0) | 2024.02.11 |
|---|---|
| 우크라이나 정부 사이트 마비 사건으로 알아보는 DDoS 공격 개념 (0) | 2024.02.09 |
| 웹 아티팩트(Web Artifact) (0) | 2024.02.05 |
| GDB 심볼 안 잡힐 경우 (0) | 2024.01.31 |
| 프로세스 메모리 구조 + 스레드 (0) | 2024.01.29 |