LLVM과 Swift 빌드 과정

Xcode를 사용하면서 프로젝트를 빌드하고 실행한 경험은 많았지만 실제로 어떤 일이 일어나는지 정리해 본 경험은 없었던 것 같습니다.

대학생 때 컴파일러 수업을 들었으면… 😅


참조

[Understanding Xcode Build System]

[Apple 공식 문서: Build System]

[stack overflow: Learning and Understanding the Xcode Build System


Xcode Build System

image

Xcode Build System이란, 빌드 과정에서 필요한 작업을 처리하여 코드와 리소스 파일 등을 완성된 앱으로 만들 수 있게 해주는 시스템입니다.

위 사진처럼 설정을 통해 빌드 프로세스를 수정하거나 작업 집합을 변경해주는 것도 가능합니다.

빌드 시스템에 빌드 지시를 내리면, 다양한 도구 및 프로세스를 조율하여 소스 코드를 실행 가능한 프로그램으로 변환하는데 구체적인 과정은 다음과 같습니다.

image


LLVM

여기서 LLVM이 등장하는데, LLVM은 다양한 프로그래밍 언어를 컴파일할 수 있는 모듈식 컴파일러 인프라입니다.
GCC 등의 전통적인 정적 컴파일러는 일반적으로 프론트엔드 + 최적화 + 백엔드 과정으로 나누어지게 되는데,

image

  • 프론트엔드 : 소스 코드 구문 분석 및 AST(추상 구문 트리) 생성

  • 미들엔드 : 코드 실행 시간 개선을 위한 최적화

  • 백엔드 : 코드를 대상 명령어 세트에 매핑하여 Machine Code 생성


기존 컴파일러에서는 동일한 디자인을 사용하더라도 일부 모듈만 재사용하는 것이 어려웠고, 각 단계가 독립적이지 않고 의존적인 부분이 일부 존재했습니다. 따라서, 하나의 컴파일 인프라스트럭처를 통해 여러 언어에서 동일한 컴파일러 기능을 공유하기 위해 LLVM이 등장하게 됩니다.

image

LLVM의 주요한 특징은 중간 표현(IR)으로, 다양한 언어를 지원하는 프론트엔드에서 처리된 결과를 언어 독립적인 중간 표현(Intermediate representation)으로 나타낼 수 있습니다. 이는 곧, 어떤 언어로 작성된 코드라도 IR로 변환되므로 동일한 옵티마이저(Optimizer)를 사용할 수 있다는 것을 의미합니다.
따라서 프론트엔드만 구현하면, 기존에 존재하던 옵티마이저와 백엔드를 그대로 사용하여 최적화된 실행 파일을 생성할 수 있습니다.

기존의 Objective-C가 C언어 기반이었기 때문에, GCC 컴파일러를 사용하고 있었지만 Objective-C의 비중이 크지 않아 개발 상 여러 문제점이 발생하게 되었고, 이에 애플은 LLVM 기반의 C/C++ 프론트엔드 컴파일러인 Clang을 릴리스하여 GCC에 대한 의존성을 완전히 버리게 됩니다.

다양한 언어와 플랫폼을 지원하는 LLVM의 특성에 따라, Xcode상에서 C/C++/Objective-C는 Clang으로 컴파일되며, Swift는 LLVM 기반의 Swift 프론트엔드 컴파일러 swiftc를 통해 컴파일됩니다.
이러한 LLVM기반 컴파일러는 소스 코드를 분석하여 추상 구문 트리(AST)를 생성하고, 이를 LLVM 중간 표현(IR)로 변환합니다. Swift의 경우 Swift 특화(옵셔널, ARC, 프로토콜, 제네릭 등) 컴파일 및 최적화 과정을 적용시키기 위해서 추가적으로 SIL(Swift Intermediate Language)로 변환되지만, 이는 곧 LLVM IR로 변환되어 추가 최적화 과정을 거친 뒤 백엔드로 넘어가게 됩니다.


어샘블러

img (1)

프로젝트를 빌드하고 빌드 로그를 살펴보면, 위와 같이 작성한 소스 파일이나 라이브러리들의 컴파일 및 링크가 이루어지는 것을 알 수 있습니다.
앞서 생성된 LLVM IR은 어샘블러를 통해 기계어로 변환되는데, 이때 Object File이 생성됩니다. 이는 바이트 스트림으로, 이후 링크하여 실행 파일을 만드는 데 사용됩니다.

image

실제 파일 시스템에서 확인해보면 다음과 같이 작성한 소스 코드에 대한 다양한 중간 산출물이 생긴 것을 확인할 수 있습니다. 이 파일들은 의존성 정보, 디버그 정보, 문자열 리소스 관련 정보, 상수 값 정보 등 컴파일 및 최적화 과정에 필요한 정보를 담고 있으며, 이 중에 .o 확장자의 Object File도 존재합니다.

image

이 Object File은 기계어로 변환된 코드를 담고 있으며, 파일을 열어보면 그마저도 일부 바이너리 데이터만 문자로 변환되어 보여집니다. 실제 기계어는 이진 코드로 저장됩니다.


링커

위와 같이 생성된 객체 파일(Object File)과 라이브러리(.dylib, .tbd, .a)를 조합하여 링커는 단일 Mach-O 실행 파일을 생성합니다. 만약 어샘블리 단계에서 생성된 객체 파일에서 다른 라이브러리를 참조하고 있다면, 이 심볼과 실제 함수가 구현된 라이브러리를 연결하는 것은 링커의 책임인 것입니다.

이러한 링커는 컴파일 과정에서 생성된 객체 파일의 심볼 테이블을 사용하여 참조를 해결하는데, 예를 들어 다음과 같이 심볼 테이블을 직접 확인해볼 수 있습니다.

image

예를 들어 다음과 같은 소스 파일이 있을 때 심볼 테이블을 확인하면,

image

위와 같이 심볼 정보를 확인할 수 있습니다.

위 예시에서는 확인한 Talkingview.o 파일에 TalkingView가 정의되어 있고, then, BaseView, setup, addSubviews, setupConstraints 등은 다른 객체 파일 또는 라이브러리에 정의되어 참조되는 함수를 나타냅니다.

따라서 객체 파일에 포함된 심볼 테이블을 통해 링커가 기계어 파일을 연결하여 최종 실행 파일을 생성한다는 것을 추측할 수 있습니다.

image

이후 최종적으로 생성된 실행 파일은 Mash-O 파일 포맷을 따르게 됩니다. Mach-O 파일은 코드와 데이터의 모음으로, iOS 및 macOS 운영체제에서 객체 파일, 실행 파일, 라이브러리 등에 사용되는 특별한 파일 형식입니다.

image

따라서 위와 같이 헤더를 확인하거나

img (2)

로드 명령을 확인하여 파일이 메모리에 로드되고 실행되는 과정을 확인할 수 있습니다.