- 사전 지식
- Mach-O: 런타임에 실행되는 파일들의 총징
- executable: 애플리케이션의 메인 바이너리
- dylib: 동적 라이브러리(다른 프레임워크에서는 DSO, DLL이라고도 함)
- Bundle: 링크되지 못하는 Dylib. dlopen()으로 런타임에 열어야 함
- Image: executable, dylib, bundle을 개별적으로 통칭하는 말
- Framework: 여기서는 리소스와 헤더들을 가지는 Dylib를 의미
- Mach-O 이미지 파일 구조
- 파일을 세그먼트로 나눠진다. 세그먼트는 대문자료 표기한다.
- 세그먼트는 페이지 사이즈의 배수로 나타난다.
- arm64에서는 16KB
- 다른 곳에서는 4KB
- 섹션은 세그먼트를 나눈것. 소문자로 표시한다.
- 일반적인 세그먼트들
- __TEXT: 헤더, 코드, 읽기 전용 상수들
- __DATA: R/W가 가능한 데이터들. 전역 변수, 정적 변수 등
- __LINKEDIT: 프로그램을 로딩하기 위한 정보가 적혀있는 메타데이터
- Mach-O 유니버셜 파일: 두개 이상의 아키텍쳐용 바이너리가 포함된 Mach-O
- 이를 표현하기 위한 헤더가 추가적으로 들어간다(Fat Header)
- Fat Header는 1페이지 크기이며 아키텍처 종류와 해당 아키텍쳐의 바이너리의 시작지점 오프셋 정보가 들어간다.
- 툴과 런타임이 모두 유니버셜을 지원해야 한다.
- 굳이 페이지 단위로 관리해야되나? 메모리 낭비인데? -> 가상 메모리 때문에
- 가상 메모리
- 물리 메모리를 직접 사용하지 않게 하는 것
- 프로세스 주소(논리 주소)를 물리 주소로 변환하기 위한 테이블(Map)을 가진다.
- 페이지 폴트: 원하는 페이지가 물리 메모리에 없는 현상. 페이지 교체를 일으키며 이 때 스레드가 멈춘다.
- 페이지 공유: 프로세스사이에서 페이지의 내용을 공유할 수 있다.
- 파일의 페이지 지원: mmap을 이용하면 페이지 단위로 파일을 읽을 수 있어서 전체 파일을 한꺼번에 메모리에 올리지 않고도 파일을 읽을 수 있다.
- CoW지원
- 퍼미션 지원
- dylib 보안
- ASLR: Address Space Layout Randomization, 이미지를 랜덤한 곳에 로드한다.
- Code Signing: 각 페이지의 내용을 해싱한다. 이 해시값은 _LINKEDIT에 들어가며, 페이지가 로딩될 때 변조 여부를 체크할 수 있다.
- main() 이전에 무슨 일이 일어나는가?
- exec(): 커널이 어플리케이션을 새로운 주소공간에 매핑해준다.
- 시작 주소는 랜덤이다.(ASLR)
- 시작 주소 이하의 값은 접근 불가능하게 설정된다(rwx 권한 모두 없음)
- 32bit에서는 4KB 이하
- 64bit에서는 4GB이하
- Null포인터 참조, 포인터 잘림 오류 등을 캐치하기 위해 사용
- 처음에는 공유 라이브러리가 없어서 쉬웠으니 이후 공유 라이브러리가 나오면서 상황은 복잡해졌다. -> 공유 라이브러리를 로딩하기 위한 별도 프로그램을 만들어서 로딩한다(dyld, 다른 플랫폼에서는 LD.SO)
- 앱이 로딩되면, dyld가 로딩된다.
- dyld는 프로세스와 같이 돌아가면서 의존성이 있는 dylibs를 로딩해준다.
- dyld는 앱과 동일한 퍼미션을 가져간다.
- dyld의 동작과정
- Load Dylibs
- dylib 리스트를 파싱한다.
- 필요한 dylib를 찾아서 읽는다.
- 이때 dylib을 검증하고, 코드 사인 확인까지 하고 나서야 mmap으로 메모리에 매핑을 하게 된다.
- 직접적으로 dylib를 로딩하면, 각 dylib의 숨겨진 의존성까지 모두 로딩한다.
- 일반적으로 앱 하나가 100~400개 정도의 dylib을 로딩한다.
- 대부분은 OS에서 제공하는 dylib
- OS가 제공해주는 것은 로딩이 최적화가 되어있다.
- Rebase, Bind, ObjC 단계 -> 앱에서 dylib의 코드를 정상적으로 호출할 수 있도록 바인딩해주는 단계
- 그런데 한 dylib에서 다른 dylib를 호출하려고 하면 문제가 된다. -> 코드 사인 때문에 dylib을 변경할 수가 없기 때문에..
- 그래서 code-gen(현재는 dynamic PIC(Position Independent Code))를 사용한다.
- 코드는 어떤 주소에도 로딩될 수 있고, 변경되지 않는다.
- 그래서 데이터 영역에 실제 주소를 담은 테이블을 만든다.
- rebase: 이미지 내에서의 포인터값 조정
- 이미지 로딩 위치에 따른 오프셋 차이를 보정해준다.
- slide(actual_address - preferred_address)값을 더해줘서 다시 쓰는 것
- 이러한 포인터들의 위치는 또 __LINKEDIT영역에 인코딩되어 있다.
- 데이터 영역이 바뀌기 때문에 더티 페이지가 된다. 따라서 IO 비용이 생기는데, 이러한 작업은 순차적으로 일어나기 때문에 프리패칭이 가능하다.
- binding: 포인터를 외부 이미지로 설정하는 것
- Dyld가 심볼 이름을 찾아서 포인터를 설정해준다.
- 심볼을 찾는 과정이 복잡하기 때문에 rebasing보다 계산량이 많다.
- 다만 이미 iO는 거의 끝났고, 페이지 폴트도 거의 일어나지 않는다.
- Objc 설정
- Objc 관련 설정은 대부분 rebasing과 binding이 해결해준다. -> 하지만 몇가지 추가적으로 더 해줘야 될 것이 있다.
- 셀렉터는 유니크해야 한다.
- Initializer: 동적으로 수정하는 과정 -> 정적으로 할 수 있는 것 이외의 모든 작업을 수행한다.
- c++은 생성자를 여기서 만든다.
- Objc은 +load 메소드를 호출한다(deprecated되긴 했지만…)
- 이러한 초기화 과정은 bottom-up으로 이루어진다.
- 실전 -> 결론만 말하면 실행전에 할 일을 줄여야 된다.
- 대략 400ms 정도의 시간이 적당하다. 20초가 넘어가면 OS가 앱을 죽인다.
- 정리: 앱 실행전에 일어나는 작업 -> 참고 영상(iOS App Performance: Responsiveness - WWDC 2012 - Videos - Apple Developer)
- 이미지 파싱
- 이미지 매핑
- 리베이싱
- 바인딩
- 이미지 초기화
- main() 호출
- UIApplicationMain 호출
- applicationWillFinishLaunching호출
- Warm vs Cold Launch
- Warm: 앱과 데이터가 이미 메모리에 올라온 상태
- Cold: 앱이 커널 버퍼 캐시에 없는 상태 -> 측정 대상.
- 재부팅 후, 오랜만에 앱을 킬 경우에 소요되는 시간이기 때문에
- 어떻게 측정할것인가? -> 앱의 코드가 실행되기 전을 측정해야 한다.
- Dyld의 내장 측정 옵션 사용 -> DYLD_PRINT_STATISTICS 환경 변수
- Seed2에서 사용 가능
- Debugger가 dylib 로드 할 때마다 이를 지연시키는데, 이 디버거가 쓴 시간은 자동으로 빼준다.
- 세밀한 시간을 줘서 제대로 측정할 수 있도록 해준다.
- 시작시간 최적화
- dylib 로드 최적화. dylib을 쓰는 것은 비용이 많이 든다.
- 있는 것도 합치고, 가능하면 static하게 쓰자
- dlopen으로 지연 로딩해도 된다. 하지만 미묘한 이슈들이 있고, 일 자체는 줄지 않기 때문에 권하지 않는다.
- rebase/binding 최적화
- data영역의 포인터를 줄여야 한다.
- ObjectiveC의 클래스, ivar, selector, categories 수를 줄여야 한다.
- C++의 virtual 함수를 줄여야 한다.
- Swift struct를 쓰자.
- 자동으로 만들어지는 코드들을 점검하자.
- 포인터 기반이 아닌 오프셋 기반(struct)로 가자
- 가능한 건 읽기 전용으로 쓰자
- Objc 셋업 최소화 -> rebase/binding 최적화를 하면 자동으로 줄어들 것이다.
- Initializer 최적화
- load 메소드를 initialize로 바꾸자 -> 파일이 로딩될 때 초기화가 일어나지 않게 한다. 대신 런타임에 필요할 때 일어난다.
- C/C++에서는 생성자를 암시적이지 않고 명시적으로 만든다
- call site initializer로 대체하라 -> 최초에 전부 초기화하지말고, 쓸 때 그때그때 초기화해라.
- dispatch_once -> 시스템에 최적화되어 있어 추천
- pthread_once
- std::once()
- C++에서 간단한 값(Plain old data, POD)만 쓴다. -> non-trivial 생성자일 때는 초기화 시간이 오래걸린다.
- Wglobal-constructer옵션으로 문제가 있는 부분을 찾아낸다.