swift의 빌드 과정은 과연 어떻게 되는걸까?

김민준·2022년 2월 13일
9

Swift

목록 보기
1/2

swift로 iOS 개발 공부를 시작하면서, 코드 구현의 결과물을 확인하기 위해 수도 없이 xcode에서 (command + R)을 눌러왔습니다. (command + R)은 "실행(run)"의 단축키로, 우리가 작성한 코드를 빌드(build)하고 이를 특정 타겟(디바이스 or 시뮬레이터)에서 실행시킵니다.

그런데 돌아보니 단 한번도 "swift는 과연 어떤 과정을 거쳐 빌드가 되고 있는가?"에 대한 진지한 고민은 한 번도 해본 적이 없다는 것을 깨달았습니다. 그저 단축키만 무작정 눌러왔는데.. 별로 좋은 습관은 아니겠죠..? 그래서 한번 알아보았습니다!

기초개념: 컴파일? 링크? 빌드? 런?

먼저, 기본적인 개념부터 알아봐야겠죠? 정말 많이 들어봤을 위 개념들에 대해 매우 간단하게 먼저 살펴보죠!

컴파일러
컴파일러는 특정 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 옮기는 언어 번역 프로그램을 말한다.
-위키 백과

사람이 작성한 언어를 기계가 읽을 수 있도록 번역하는 것이 바로 컴파일입니다!

링크
컴파일러의 결과물인 목적(object)코드들을 최종 실행 가능한 실행파일 만들기 위해 연결 및 병합 해주는 작업
-위키 백과

컴파일시, 연관된 파일들을 엮어서 합쳐주는 과정입니다!

빌드
빌드는 소스코드 파일을 컴퓨터나 휴대폰에서 실행할 수 있는 독립 소프트웨어 가공물로 변환하는 과정을 말하거나 그에 대한 결과물을 일컫는다.
-위키 백과

빌드는 코드, 리소스 등을 하나로 합쳐 실행 가능한 파일을 만드는 과정입니다!

런(실행)
어떠한 제품을 실제 장치에서 실행시키는 프로세스이다.
-위키 백과

빌드된 파일을 시뮬레이터나 실제 디바이스에서 실행시키는 과정입니다!

정리하면,

  • 런(실행) 하기 위해서는 빌드된 파일이 있어야 하고,
  • 빌드 안에는 컴파일 과정이 포함되어 있으며
  • 컴파일 과정 안에 링크 과정이 포함되어 있습니다!

빌드 과정

이제부터는 우리의 주 관심인 빌드 과정을 큰 틀을 따라가면서 자세히 살펴봅시다!

많이 들어봤을 전처리기, 컴파일러, 어셈블러, 링커 등이 보입니다. 위 그림의 흐름을 따라가면서 swift의 빌드 과정을 살펴봅시다!

전처리기(preprocessor)

작성한 소스코드를 컴파일러에 제공할 수 있도록, 컴파일 전에 전처리를 먼저 실행합니다.

  • 매크로를 실제 정의로 바꿈(e.g. #define)
  • 파일 간의 참조 관계(종속성)를 파악(e.g. #include)
  • 컴파일 조건문을 파악(e.g. #if~, #if else, #endif)

그러나, swift 컴파일러에는 전처리기가 없습니다!

  • 그래서, swift에서는 매크로를 사용할 수 없음..

🤔 하지만, swift에서도 파일 간의 참조 관계(종속성)나 컴파일 조건문은 분명 존재하는데, 이를 어떻게 처리하고 있는걸까요..?

  • llbuild(하위 레벨 빌드 시스템)을 통해 종속성을 해결함
    • llbuild(low-level build system): 빌드 시스템을 구축하기 위한 라이브러리 세트
      [참고]: apple/swift-llbuild
  • 컴파일 조건문은 아래 그림과 같이 프로젝트의 Build Settings에서 flag를 설정하여 사용 가능
  • Active Compliation Conditions에 RELEASE를 추가하여 DEBUG 모드냐 RELEASE 모드냐에 따라 코드가 다르게 동작하게 할 수 있음

    #if RELEASE
    print("RELEASE에서만 할 코드")
    #elseif DEBUG
    print("DEBUG에서만 할 코드")
    #endif

  • Other Swift Flags에 원하는 flag를 추가하여 사용 가능
    • 단, -D라는 prefix를 반드시 붙여주어야 함

    #if FLAG1
    print("앱 타켓에서만 할 코드")
    #endif

컴파일러(Complier)

소스코드에 대한 정보를 수집하는 symbol table을 작성 및 관리하며, 아직 고수준 언어를 저수준 언어인 어셈블리어로 변환합니다.

그리고, swift의 컴파일 과정은 아래 그림과 같이 LLVM을 거쳐서 실행됩니다.

🤔 LLVM이란?(약자가 아닌 프로젝트명입니다!)

  • 프로그램을 컴파일, 링크, 런타임 등의 상황에서 프로그램의 작성 언어에 상관없이 최적화를 쉽게 구현할 수 있도록 구성되어 있는 컴파일러이자 툴킷
  • 중간 표현(Intermediate Representation, IR), 이진(binary) 코드를 구성, 최적화 및 생성하는데 사용되는 라이브러리

본격적으로 swift의 컴파일 과정을 한번 살펴봅시다!

  • 프론트엔드
    • 소스코드 => swift AST
    • swift AST => swift IL(SIL)
    • swift IL => LLVM IR
  • LLVM IR => 백엔드
  • 백엔드(LLVM)
    • LLVM IR => 어셈블리어
    • 어셈블리어 => 기계어

즉, swift는 총 2번의 최적화 과정을 거칩니다!

  • 프론트엔드: swift AST => SIL
  • 백엔드: LLVM IR => 기계어

🤔 SIL(Swift Intermediate Language)이란?

1. 프론트엔드

  • 두 가지 컴파일러를 지원
    • Swift Compiler: swift 용
    • Clang: C languagues 용
  • 소스 코드에 대한 정보를 수집하는 symbol table를 생성
  • 소스 코드를 분석 및 최적화하고, 이를 LLVM이 어셈블리어로 번역해 줄 중간 언어인 LLVM IR을 생성

2. 백엔드(LLVM)

  • 프론트엔드로부터 넘겨받은 LLVM IR을 어셈블리어로 변환하고, 어셈블러가 이를 기계어로 변환
  • 이 때, 비트코드를 생성하는 것으로 설정하면 비트코드로도 변환
    • LLVM 어셈블러인 llvm-as가 LLVM IR을 LLVM 비트코드로 변환
    • 아래 그림처럼 설정이 가능하고, Yes로 설정하지 않으면 비트코드 파일은 생성되지 않고 기계어만 생성됨

🤔 비트코드(bitcode, .bc)란?

  • IR(Intermediate Representation) 종류 중 하나
  • LLVM에 의해 컴파일된, 소스코드와 기계어의 중간단계로 어떤 아키텍처(디바이스)에서도 실행되기를 준비하는 중간단계

어셈블러

사람이 읽고 해석할 수 있는 어셈블리 코드를 재배치가능한(relocatable) 기계어로 변환합니다. 이 때, Mach-O(Mach Object file format) 파일을 생성합니다.

  • 컴파일 과정에서 언급한 백엔드(LLVM)의 어셈블러가 어셈블리어를 기계어로 변환합니다!

  • 🤔 기계어란?

    • CPU에서 직접 실행할 수 있는 명령들의 집합을 나타내는 이진 언어
  • 🤔 Mach-O(Mach Object file format)란?

    • 의미가 있는 data chunks로 그룹화된 바이너리 바이트 스트림으로 구성됨
    • Darwin 기반 OS(iOS, MacOS, ...)에서 쓰이는 특정한 바이너리 파일 포맷
    • chunks에는 바이트 순서, CPU 타입, chunks 크기 등과 같은 메타 데이터가 포함됨
  • 🤔 재배치가능한 파일이란?

    • 실행가능한 파일이나 공유 오브젝트 파일을 생성하기 위해 다른 오브젝트 파일들과 링킹할 수 있는 코드와 데이터를 갖는 파일
    • 아직 실행가능하진 않지만, 실행가능한 오브젝트 파일을 만들기 위해 재배치가능한 다른 오브젝트 파일들과 링크가 가능
  • 🤔 실행파일이란?

    • 바이너리 코드와 데이터를 가지고 있으며, 메모리로 직접 로드되어 실행될 수 있음

링커

Darwin 기반 OS에서 실행할 수 있는 단일 Mach-O 실행 파일을 만들기 위해 다양한 오브젝트 파일들과 라이브러리들을 병합합니다.

  • input: 오브젝트 파일(어셈블러의 결과물) + 라이브러리 파일(동적, 정적)
  • 어셈블러와의 공통점: 둘 다 Mach-O 파일을 output으로 생성
  • 어셈블러와의 차이점: 어셈블러의 Mach-O 파일들은 불완전한 파일
    • 어셈블러: 재배치가능한 오브젝트 파일을 생성
      • 다른 오브젝트 파일들이나 라이브러리들을 참조하는 부분들이 누락된 상태
    • 링커: 아래 그림처럼 프로젝트의 Build Settings의 Linking에서 지정한 타입의 파일을 생성
      • 컴파일 과정에서 생성된 symbol table을 활용하여 여러 오브젝트 파일들과 라이브러리들에 대한 참조를 확인하고, 이들을 연결시켜 줌

  • archive를 통해 앱을 업로드하는 과정에 링킹이 포함되어 있음
    • Enable Bitcode No 설정: 최종 컴파일 결과인 실행 가능한 바이너리 파일 자체를 업로드
      • 앱이 실행될 수 있는 모든 환경(다양한 아키텍쳐, 디바이스)에 대한 바이너리를 생성하여 하나의 파일로! => "fat binary"
    • Enable Bitcode Yes 설정: fat binary를 만들지 않고, 비트코드를 업로드
      • 앱 스토어는 업로드된 비트코드를 가지고 각 환경별 최적의 바이너리를 생성(앱 시닝)

비트코드의 장점

  • 비트코드는 컴파일된 프로그램의 중간 표현(IR), 따라서 다양한 방식으로 다시 컴파일을 시도할 수 있음
    • 애플은 애플 서버에 제출된 비트코드를 다시 컴파일하여 사용자에게 맞는 최적의 바이너리 파일을 제공할 수 있음
  • 애플은 비트코드로 인해 새로운 CPU에 대한 지원을 앱 스토어의 백엔드에 쉽게 추가가 가능해짐
    • 최신 아키텍쳐로 컴파일하는 방법을 비트코드에 표시하여 이에 맞게 컴파일되게 할 수 있음

비트코드, 단점은 없을까?

crash report를 디버깅하고자 할 때(e.g. Firebase Crashlytics), 비트코드를 사용하면 "unsymbolicated crash from missing dSYMs" 에러가 발생하는 경우가 있음

  • 개발자가 archive시 생성된 바이너리에 포함된 dSYM 파일과, 앱 스토어에서 비트코드로 생성한 바이너리에 포함된 dSYM 파일이 불일치하여 나타나는 현상
  • 해결방법은 앱 스토어에 가서 dSYM 파일을 다운로드 받아, 업로드하여 사용

  • 🤔 dSYM(debug symbol file)이란?
    • 컴파일러가 소스코드를 기계어로 변환할때 생성되고, 역할은 기계어를 다시 소스코드 라인으로 매핑하는 정보를 가진 파일
    • Xcode의 build setting에서 Debug Information Format의 설정에 따라 Debug Symbol을 최종 바이너리에 포함시킬지, 별도의 파일로 추출되도록 할 지 설정할 수 있음

앱 시닝(App Thinning)

서로 다른 각 디바이스에서 필요한 내용만을 선택하여 앱 번들로 만들고 전달하는 과정입니다!

위 그림처럼 범용 앱(universal app) 하나를 업로드 하면, 앱 스토어에서 슬라이스가 발생하게 됩니다!

앱 스토어가 디바이스의 특성을 보고, 아래 그림처럼 필요한 것만 조합해서 별도의 IPA를 만듭니다.

  • 🤔 IPA란?
    • iOS App Store Package의 줄임말로, 패키징이 끝난 압축파일
    • 애플 모바일 스마트기기의 운영체제인 iOS에서 사용하는 앱의 설치 파일 - Xcode에서 앱을 만들면 .app 파일이 되는데, 이 파일을 .zip 형식으로 압축해 확장자만 .ipa로 바꾸는 방식을 사용
profile
trial and error

0개의 댓글