<h1><center> Xcode Build System </center></h1> ###### tags: `💻 TIL`, `Xcode`, `Swift` ###### date: `2023-12-12T17:21:33.284Z` > [color=#724cd1][name=데릭] > [김종권의 iOS 앱 개발 알아가기](https://ios-development.tistory.com/1336) > [Enyou's Atelier](https://blog.enyou.net/ko/archives/1282) > [Chaos and Order](https://fjvbn2003.tistory.com/509) > [Swift Complier](https://www.swift.org/swift-compiler/) > [어셈블리어 위키백과](https://ko.wikipedia.org/wiki/%EC%96%B4%EC%85%88%EB%B8%94%EB%A6%AC%EC%96%B4) > [Understanding Xcode Build System](https://www.vadimbulavin.com/xcode-build-system/) ## 개요 > Xcode에서 Build(Cmd + R)를 했을 때 어떤 과정을 거치게 될까? > 면접 질문에서 나왔는데.. 대답하지 못했다.. **Build 단계** ![image.png](https://hackmd.io/_uploads/BJehGl-Ima.png) - Xcode에서는 Build를 하면 아래와 같은 플로우를 거쳐서 빌드가 완성된다.. - Preprocessor -> Complier -> Assembler -> Linker -> Loader ## Preprocessor(전처리) 전처리는 컴파일 하기 전에 실행된다. 전처리 단계에는 아래와 같은 작업을 한다. 1. 매크로를 실제 정의로 바꾼다. - ex) #Define 2. 파일 참조 관계(종속성)을 파악한다. - ex) #include 3. 컴파일 조건문을 파악한다. - ex) #if ~, #if else, #endif 파일 참조 관계는 `llbuild(low-level build system)`이라는 것을 사용한다. - Objc에서는 전처리 단계가 존재하기 때문에 Project도 Swift-llbuild가 있다. **컴파일 조건문 파악 - Xcode** ![스크린샷 2023-11-06 14.59.42.png](https://hackmd.io/_uploads/SksRe-Ump.png) - Xcode > Build Settings > Active Compilation Conditions에서 설정 - 이름이 Active Comiplation Conditions인 이유는 말 그대로 컴파일전 조건문을 미리 정해준다는 의미 `Active Compilation Conditions`에 `DEBUG`가 타겟일 때, `#if DEBUG`와 같은 문구로 특정 코드를 작성할 수 있다. **NOTE** > C언어에서 `#`키워드는 컴파일러가 컴파일을 실행하기 전에 처리하는 전처리기(Preprocessor) 혹은 매크로라고 부른다. > **Swift 매크로도 전처리기에서 생성한다.** ```swift #include <stdio.h> ``` > `stdio.h`라는 헤더파일을 호출하면 scanf, printf 같은 함수를 사용할 수 있게 된다. ## Compiler(컴파일러) ![image.png](https://hackmd.io/_uploads/BJM2wGIXT.png) 컴파일러는 소스코드(Swift)를 어셈블리어로 변경한다. 또한, **Symbol Table**을 생성해서 Property, Function, Class 이름을 각각 저장하고 보관한다. **Xcode의 컴파일러** Xcode에서 컴파일러 단계는 `LLVM`을 거쳐서 실행된다. `LLVM`은 `Frontend -> Middleend -> Backend`의 단계를 거쳐 컴파일이 진행된다. Xcode에서는 LLVM이 어셈블리어를 만들 `목적코드`를 생성하기 위해 Swift Complier와 Clang의 Frontend를 가지고 있다. 이 두 가지는, **Symbol Table**을 가지고 코드가 가진 가진 의미를 잃지 않으면서 LLVM이 어셈블리어로 번역 해줄 **Intermediate Representation(중간 코드)** 을(를) 생성합니다. **Swift Complier** - Parsing: `Parsing`는 직접 코딩한 통합 Lexer가 포함된 간단한 재귀 하강(recursive-descent) 파서이다.(lib/Parse에서 구현되어 있다.) <br>`Parsing`는 의미나 타입 정보 없이 추상 문법 트리(AST)를 생성하는 역할을 하며 입력에 대한 문법적 경고, 에러를 출력할 수 있다. - Semantic analysis: `Semantic analysis`는 `파싱된 AST`를 가져오는 책임을 가진다. 또한, 잘 구성되었는지, 타입인지 확인된 AST 형식으로 변환한다. 그리고 소스 코드의 `Semantic` 문제를 경고하거나 오류를 표시하는 역할을 한다. (lib/Sema에서 구현되어 있다) <br> `Semantic analysis`에는 타입 추론이 포함하는데, 이 과정의 성공케이스는 타입이 확인된 AST에서 안전하게 코드를 생성할 수 있다는 것을 의미한다. - Clang importer: C와 Objc APIfmf Swift에 매핑한다. (Enyou의견: Brige로 Objc의 모듈을 import할 때, Swift API를 쓸 수 있는데 이 과정에 의해 그렇게 된다?) - SIL generation: 타입이 확인된 AST로 Swift를 고수준의 중간 언어(Swift Intermediate Language)를 만든다. SIL은 분석이나 최적화를 위한 Swift 코드이다. - SIL guaranteed transformations: 이 과정에서 프로그램의 정확성에 대한 추가적인 데이터 플로우를 진단할 수 있다. 결과적으로 표준 SIL이 된다. - SIL optimizations: SIL을 이용하여 프로그램에 대한 추가적인 고수준의 Swift 최적화를 수행한다. ARC와 devirtualization, Generic specialization 등을 포함합니다. - LLVM IR generation: SIL가 바로 어셈블리어가 되지 않기 때문에 Backend에 줄 LLVM IR을 먼저 만들게 된다. 그 후, 어셈블리어 생성을 LLVM Backend에 위임한다. **apple swift Github: https://github.com/apple/swift/blob/main/docs/SIL.rst** **NOTE** > devirtualization은 상속에 따른 V-Table 참조에 대한 최적화이다. <br> > 예를 들어, `final`키워드를 이용하여 더 이상 상속이 불가능함을 명시하면 이 절차에 의해 클래스임에도 더 이상의 서브클래싱은 없기 때문에 function, variable 등을 직접 참조하는 최적화를 할 수 있게 된다. 또한, Generic spetialization은 프로토콜의 Witness-Table의 최적화로 보인다.(Enyou) ## Assembler(어셈블러) 어셈블리는 어셈블리어로 쓰여진 명령들을 기계어 명령어로 바꿔주는(번역하는) 프로그램이다. 기계코드와 바로 대응된다. 어셈블러는 코드와 데이터를 포함한 `Darwin 기반 OS`(MacOS, iOS, WatchOS 등)의 바이너리 파일이다. 이때, Xcode 어셈블러는 절대 주소에 대한 기계어가 아니라 상대 주소를 지원할 수 있는 기계어 파일을 만든다. - Object 파일로 변경(기계어 코드로 변경) - Xcode에서는 대표적으로 `.o`파일이 있고 이를 `Mach-O`타입이라고 명칭 - 오브젝트 파일 `.o` - 동적 라이브러리 `.dylib` - 동적 라이브러리 `.a` - 번들 `.bundle` **직접 확인하는 방법** 1. 빌드 후 DerivedData 폴더로 이동한다. ```swift cd ~/Library/Developer/Xcode/DerivedData ``` 2. 프로젝트 이름으로 된 폴더를 찾고 Build 폴더로 이동하면 .o파일을 찾을 수 있다. ![스크린샷 2023-11-06 23.17.21.png](https://hackmd.io/_uploads/BJnOB_8Qp.png) **file 명령어로도 `Mach-O`타입인지 확인할 수 있다!** - 어셈블러 단계에서 오브젝트 파일(.o)이 생성된다. 이 파일의 타입은 `Mach-O`이다. 위치는 `DerivedData` 내부에 있다. ```swift % file Alamofire.o Alamofire.o: Mach-O 64-bit object arm64 ``` **어셈블러에 의해 만들어진 오브젝트 파일(.o) 읽는 방법** ```swift % hexdump Alamofire.o 044c9e0 6663 3155 005f 245f 3973 6c41 6d61 666f 044c9f0 7269 3165 4437 7461 5361 7274 6165 526d 044ca00 7165 6575 7473 3043 7238 7365 6f70 736e 044ca10 4365 3530 7375 6e69 3267 6e6f 7336 7274 044ca20 6165 416d 5843 7844 535f 316f 4f37 5f53 044ca30 6964 7073 7461 6863 715f 6575 6575 7943 044ca40 4341 4330 5630 5f79 3631 6553 6972 6c61 044ca50 7a69 6465 624f 656a 7463 7a51 4141 4137 044ca60 4546 7272 726f 474f 634b 4174 3041 4365 ... ``` **NOTE** > 어셈블리어: 하드웨어를 직접 제어할 수 있는 저수준 언어이다. 기계어는 아니고 사람이 이해할 수 있을 만한 텍스트 형식의 언어이다. 기계어를 사람이 읽기 쉬운 단어와 1대 1로 대응 시킨 것이라고 한다. - 어셈블리어 ![image.png](https://hackmd.io/_uploads/BysuZbU7T.png) ## Linker(링커) 어셈블러 단계에서 만든 **오브젝트 파일(.o)과 라이브러리**를 연결시켜 실행파일로 만드는 역할을 한다. **NOTE** > Static Framework일 경우, 링커가 실행 파일 안에 해당 프레임워크를 넣는다.<br> > Dynamic Framework일 경우, 링커가 실행 파일 안에 해당 프레임워크를 **포함시키지 않고** 오브젝트 파일(.o)을 실행파일로 변환한다. --> `Linker`의 역할 때문에 Static Framework를 사용할 때, 코드 중복이 있는지 의존관계에 대해 고려해봐야 한다. **의존 관계 예제** > Linker 타임에 Framework의 의존 관계에 따라 Static, Dynamic인지 결정한다. ## 1. A Static + B Static → 둘 다 C Static 을 의존 ``` App Binary ├─ A(obj 파일들) ├─ B(obj 파일들) └─ C(obj 파일들) ← 딱 1개만 ``` ``` [ A Static ]---\ -> (Linker) -> [ App Binary ] [ B Static ]---/ └ C Static (1개) \ C Static(중복 제거) ``` - 정상 ### 📌 이유 > Static Framework는 App Binary에 최종적으로 하나의 링커 실행 단계에서 합쳐진다. 따라서 A도 C를 포함하고, B도 C를 포함하더라도 ✔ 링커는 동일한 심볼(같은 C 프레임워크 심볼)들을 자동으로 “단 하나로” 합친다. ## 2. A Static + A Dynamic → 둘 다 C Static 의존 ``` [ A Static ]----\ -> (Linker) -> [ App Binary ] [ A Dynamic ] (런타임 로딩) \ C Static (중복 없음) ``` - 정상 ### 📌 이유 동적 프레임워크라도 자체적으로는 Static을 포함하지 않는다. Static Framework는 항상 App Binary에만 합쳐진다. 따라서: A Static → C를 포함 (App으로 들어감) A Dynamic → C를 포함하지 않음 (dynamic은 static 라이브러리를 자체 포함할 수 없음) 최종적으로 C는 앱 바이너리에만 1개 존재. ## 3. A Dynamic + B Dynamic → 둘 다 C Static 을 의존 ``` A.dynamic → C의 코드 포함 B.dynamic → C의 코드 포함 ``` 앱 실행 시: - A도 C의 사본 로드 - B도 C의 사본 로드 즉, C Static Framework의 코드가 2번 포함되고 2번 로드됨 (중복) → 심볼 충돌 또는 패키지 크기 증가 문제 발생 가능. - Static Framework(**코드 중복발생**) ### 📌 이유 Dynamic Framework는 독립된 바이너리(.framework) 파일이다. 이 Dynamic Framework가 C Static을 포함해야 하는 경우: ✔ Static Framework의 코드가 각각의 Dynamic Framework 안에 통째로 포함된다. ``` 앱 실행 시: A도 C의 사본 로드 B도 C의 사본 로드 🔥 즉, C Static Framework의 코드가 2번 포함되고 2번 로드됨 (중복) → 심볼 충돌 또는 패키지 크기 증가 문제 발생 가능. ``` ## Loader(로더) 디스크에 만들어진 실행 파일을 메모리에 로드하는 작업을 진행한다.