Mach-O 파일이란?
위키피디아에서 가져온 간단한 메모입니다:
Mach-O는 실행 파일, 객체 코드, 공유 라이브러리, 동적으로 로드된 코드 및 코어 덤프용 파일 형식인 Mach Object File Format의 약자입니다. a.out 형식의 대안으로 Mach-O는 확장성이 뛰어나고 심볼 테이블의 정보에 대한 액세스 속도를 향상시킵니다. Mach 커널을 기반으로 하는 대부분의 시스템은 Mach-O를 사용합니다. GNU Mach를 마이크로커널로 사용하는 GNU Hurd 시스템도 표준 바이너리 파일 형식으로 Mach-O가 아닌 ELF를 사용합니다.
Mach-O형식
일반적인 Mach-O 파일은 세 가지 영역으로 구성됩니다.
- Mach-O 헤더: 바이너리에 대한 정보(바이트 순서, CPU 유형, 로드 명령 수 등)를 포함합니다.
- 로드 명령: 세그먼트, 심볼 테이블, 동적 심볼 테이블 등의 위치를 설명하는 일종의 카탈로그입니다. 각 로드 명령에는 명령 유형, 이름, 바이너리 파일 위치 등과 같은 메타 정보가 포함되어 있습니다.
- 데이터: 일반적으로 대상 파일의 가장 큰 부분으로, 심볼 테이블, 동적 심볼 테이블 등과 같은 코드와 데이터를 포함합니다.
다음은 단순화된 그래픽 표현입니다.
OS X에는 Mach-O 파일과 Fat 파일이라고도 하는 일반 바이너리의 두 가지 유형의 오브젝트 파일이 있습니다. 차이점은 Mach-O 파일에는 하나의 아키텍처(i386, x86_64, arm64 등)에 대한 객체 코드만 포함되어 있는 반면, Fat 바이너리에는 여러 개의 객체 파일이 포함되어 있으므로 아키텍처가 다릅니다.
Fat 파일의 구조는 매우 간단합니다. Fat 헤더와 Mach-O 파일로 이루어져 있습니다:
Fat_headers 구조는 다음과 같이 명령 도구 도구로 확인할 수 있습니다:
otool -fv Safari
이 작업은 다음 명령으로 수행할 수 있습니다.
$ otool -fv
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture x86_64
cputype CPU_TYPE_X86_64
cpusubtype CPU_SUBTYPE_X86_64_ALL
capabilities 0x0
offset 16384
size 115611136
align 2^14 (16384)
architecture arm64
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64_ALL
capabilities 0x0
offset 115638272
size 104958176
align 2^15 (32768)
Mach-O header
모든 Mach-O 파일은 시작 부분에 헤더 구조가 있습니다. 파일을 Mach-O 파일로 식별하는 데 사용되는 헤더에는 바이너리 파일 정보도 포함되어 있습니다.
struct mach_header_64 {
uint32_t magic;/*매직넘버를 사용하면 64비트인지 32비트인지 빠르게 찾을 수 있습니다.*/
cpu_type_t cputype; /*cpu유형, 예: ARM*/
cpu_subtype_t cpusubtype;/*머신 디스크립터*/
uint32_t filetype;/*파일 유형(예: "EXECUTE", DYLIB, BUNDLE.*/
uint32_t ncmds; /*load command */
uint32_t sizeofcmds;/*모든 로드 명령의 크기*/
uint32_t flags;/*식별자 비트는 주로 로딩 및 연결과 관련된 바이너리가 지원하는 기능을 식별합니다.*/
uint32_t reserved;/*예약 필드*/
}
특히 주목해야 할 것은 파일 유형을 설명하는 파일 유형 멤버입니다. mach-o/loader.h 파일에서 다음 값을 볼 수 있습니다.
| MH_OBJECT | 재배치 가능한 대상 파일 |
| MH_EXECUTE | 표준 Mach-O 실행 파일 |
| MH_DYLIB | Mach-O DLL |
| MH_BUNDLE | Mach-O 패키지 |
| MH_DSYM | 심볼 파일 |
| MH_KEXT_BUNDLE | 표준 Mach-O 실행 파일 |
Mach-O 파일의 내용을 보려면 /usr/bin/otool 명령줄을 사용합니다(예: Mach-O의 헤더 헤더를 구문 분석하려면 -hv를 사용하여 otool을 실행합니다).
$ otool -hv testOtool
testOtool:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 21 2264 NOUNDEFS DYLDLINK TWOLEVEL PIE
UI를 선호하는 경우 MachOView 프로그램을 사용하여 볼 수 있습니다.
Load commands
Mach-O 헤더 다음에는 동적 로더에 메모리에서 바이너리를 로드하고 레이아웃하는 방법을 지시하는 바이너리 로드 명령이 있습니다.
로드 명령을 지정할 수 있습니다:
- 가상 메모리에 있는 파일의 초기 레이아웃
- 기호 테이블의 위치
- 프로그램 메인 스레드의 초기 실행 상태
- 메인 스레드에 의해 정의된 공유 라이브러리의 이름을 포함합니다.
Mach-O 바이너리는 load 명령과 함께 -l 플래그를 사용하여 otool을 통해 볼 수 있습니다.
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
flags 0x0
Load command 1
cmd LC_SEGMENT_64
cmdsize 712
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000004000
fileoff 0
filesize 16384
maxprot 0x00000005
initprot 0x00000005
nsects 8
flags 0x0
Section
sectname __text
segname __TEXT
addr 0x0000000100003e94
size 0x000000000000009c
offset 16020
align 2^2 (4)
reloff 0
nreloc 0
flags 0x80000400
reserved1 0
reserved2 0
....
로드 명령은 모두 mach-o/loader.h에 정의된 load_command 구조체로 시작됩니다.
struct load_command {
uint32_t cmd; /*로드 명령 유형*/
uint32_t cmdsize; /* 명령 크기*/
};
여기서 load_command.cmd는 로드 명령의 유형을 설명하며, 로드 명령의 크기는 load_command.cmdsize에 지정됩니다. load 명령의 데이터는 load_command 구조체 바로 뒤에 따라옵니다. 일반적인 명령 유형은 다음과 같습니다:
| LC_SEGMENT_64 | 파일의 세그먼트를 메모리에 매핑하기 |
| lc_dyld_info_only | 동적 링커가 주소를 리디렉션하는 데 사용하는 동적 링크 정보입니다. |
| LC_SYMTAB | 심볼 테이블 오프셋, 심볼 수, 문자열 테이블 오프셋, 문자열 테이블 크기를 포함하여 파일에서 사용하는 심볼 테이블 |
| LC_DYSYMTAB | 동적 링커에서 사용하는 심볼 테이블, 발견 시 간접 심볼 테이블 오프셋을 가져옵니다. |
| lc_load_dylinker | 기본 로더 경로 |
| LC_UUID | 해당 크래시 위치를 분석하는 데 사용되는 파일의 UUID는 dSYM 심볼 파일과 크래시 스택에 존재합니다. |
| lc_version_min_macosx | 가장 낮은 운영 체제 버전 지원 |
| lc_source_version | 바이너리 소스 코드 버전 빌드 |
| LC_MAIN | 프로그램의 진입점입니다. 동적 링커가 이 주소를 가져오고 프로그램이 이 주소로 이동하여 |
| LC_LOAD_DYLIB | 동적 라이브러리 경로, 현재 버전, 호환 버전 등 종속된 동적 라이브러리를 포함합니다. |
| LC_RPATH | 프레임을 찾기 위해 동적 링커가 검색할 경로 목록을 지정하는 @rpath에 경로를 추가합니다. |
| lc_function_starts | 함수 시작 주소 테이블 |
| lc_code_signaturt | 코드 서명 |
LC_SEGMENT/LC_SEGMENT_64 명령: 세그먼트를 설명합니다.
Apple은 다음과 같은 방식으로 세그먼트를 정의합니다:
세그먼트는 동적 링커에 의해 애플리케이션이 로드될 때 Mach-O 파일의 바이트 범위와 이 바이트가 매핑되는 가상 메모리의 주소 및 메모리 보호 속성을 정의합니다.
다음 그림과 같이 LC_SEGMENT/LC_SEGMENT_64 load 명령에는 동적 로더가 세그먼트를 메모리에 매핑하는 데 필요한 모든 관련 정보가 포함되어 있습니다:
Mach-O 바이너리를 분석할 때 다음과 같은 세그먼트가 발생할 수 있습니다:
- 페이지 제로: 정적 링커는 가상 메모리에서 위치와 크기가 0이고 읽거나 쓸 수 없으며 실행할 수 없고 널 포인터를 처리하는 데 사용되는 실행 파일의 첫 번째 세그먼트로 __PAGEZERO를 생성합니다. 개발자가 NULL 포인터에 액세스하려고 할 때 EXC_BAD_ACCESS 오류가 발생합니다.
- 텍스트 세그먼트: 실행 코드와 읽기 전용 데이터가 포함되어 있으며, 정적 링커는 이 세그먼트의 가상 메모리 권한을 읽기 및 실행 가능으로 설정하고 프로세스는 이 코드를 실행할 수 있지만 수정할 수는 없습니다.
- 데이터 세그먼트: 쓰기 가능한 데이터를 포함하며, 정적 링커는 이 세그먼트의 가상 메모리 권한을 읽기/쓰기로 설정합니다.
- ___LINKEDIT 세그먼트: 기호, 문자열, 재배치 테이블 항목 등 동적 링크 라이브러리의 원시 데이터를 포함합니다.
바이너리가 Objective-C로 작성된 경우 Objective-C 런타임에 대한 정보가 포함된 __OBJC 세그먼트가 있을 수 있지만, 이 정보는 __DATA 세그먼트에서도 찾을 수 있습니다.
참고: 섹션은 0개 이상의 섹션을 포함할 수 있으며, 각 섹션은 동일한 유형의 코드 또는 데이터를 포함합니다.
이 로드 명령은 세그먼트_명령: 형식의 구조입니다.
/*
* The segment load command indicates that a part of this file is to be
* mapped into the task's address space. The size of this segment in memory,
* vmsize, maybe equal to or larger than the amount to map from this file,
* filesize. The file is mapped starting at fileoff to the beginning of
* the segment in memory, vmaddr. The rest of the memory of the segment,
* if any, is allocated zero fill on demand. The segment's maximum virtual
* memory protection and initial virtual memory protection are specified
* by the maxprot and initprot fields. If the segment has sections then the
* section structures directly follow the segment command and their size is
* reflected in cmdsize.
*/
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; //현재 로드 명령의 유형
uint32_t cmdsize; //현재 로드된 명령의 크기를 나타냅니다.
char segname[16]; //세그먼트 이름, 16바이트 차지
uint32_t vmaddr; //세그먼트의 가상 메모리 주소
uint32_t vmsize; //세그먼트의 가상 메모리 크기
uint32_t fileoff; //파일 내 세그먼트 오프셋
uint32_t filesize; //파일 내 세그먼트 크기
vm_prot_t maxprot; //세그먼트 페이지의 최대 메모리 보호(R, W, X)
vm_prot_t initprot; //세그먼트 페이지에 대한 초기 메모리 보호(r, w, x).
uint32_t nsects; //세그먼트의 섹션 수입니다. 세그먼트는 0개 이상의 섹션을 포함할 수 있습니다.
uint32_t flags; //세그먼트 심볼 정보
};
시스템은 파일오프에서 파일사이즈 크기의 콘텐츠를 vmsize 크기의 가상 메모리 vmaddr로 로드합니다. 세그먼트 페이지의 권한은 initprot로 초기화되며 이러한 권한은 수정할 수 있지만 maxprot의 값을 초과할 수 없습니다.
Apple이 지적했듯이 각 섹션에는 동일한 유형의 코드 또는 데이터 :.마하-O 바이너리는 세그먼트로 구성되며, 각 세그먼트는 하나 이상의 섹션을 포함합니다. 같이 동일한 유형의 코드 또는 데이터를 포함합니다:
텍스트 섹션에는 실행 코드와 읽기 전용 데이터가 포함됩니다.
이 섹션의 일반적인 섹션에는 다음이 포함될 수 있습니다.
- 텍스트: 컴파일된 바이너리 코드
- 스텁, 스텁_헬퍼: 동적 링커가 심볼을 바인딩하는 데 사용됩니다.
- __const: const 키워드로 수정된 상수
- __cstring: 문자열 상수
- __objc_methname: OC 메서드 이름
- __objc_classname: OC 클래스 이름입니다.
- 객체 메서드 유형: OC 메서드 유형
- __gcc_except_tab, __ustring, __unwind_info: 예외 발생 시 스택에 해당하는 정보를 확인하기 위해 GCC 컴파일러가 자동으로 생성합니다.
데이터 세그먼트에는 쓰기 가능한 데이터가 포함되며, 이 섹션의 공통 섹션에는 다음이 포함될 수 있습니다.
- __got: 전역 비지연 바인딩 심볼릭 포인터 테이블.
- __la_symbol_ptr: 지연 바운드 심볼 포인터 테이블.
- __mod_init_func: C++ 클래스를 위한 생성자.
- __const: 초기화되지 않은 상수입니다.
- __cfstring: 핵심 파운데이션 문자열입니다.
- __objc_classlist: OC 클래스 목록입니다.
- __objc_nlclslist: +load 메서드를 구현하는 Objective-C 클래스 목록입니다.
- __objc_catlist: OC 카테고리 목록입니다.
- 오브젝트 프로토콜 리스트: OC 프로토콜 목록입니다.
- 오브젝트-C 1.0과 2.0을 구분하는 데 사용할 수 있는 이미지 정보입니다.
- __objc_const: OC 초기화 상수입니다.
- __objc_selrefs: OC 선택기 참조 목록입니다.
- __objc_protorefs: OC 프로토콜 참조 목록.
- 오브젝트 클래스 레퍼런스 목록.
- 오브젝트 슈퍼레퍼런스: OC 슈퍼클래스 레퍼런스 목록입니다.
- __objc_ivar: OC 클래스의 인스턴스 변수입니다.
- __objc_data: OC에 의해 초기화된 변수입니다.
- __데이터: 실제 초기화 데이터 세그먼트입니다.
- 공통: 초기화되지 않은 심볼 선언.
- __bss: 초기화되지 않은 전역 변수입니다.
프로그램 시작 속도를 높이기 위해 iOS는 Mach-O의 범위를로드하기 위해 동적 링커에 속하는 "지연 바인딩"과 "비 지연 바인딩"의 개념을 도입했으며, 독자는 대략적으로 이해할 수 있습니다.
- 비래지 바인딩: 동적 링커가 애플리케이션을 로드할 때 실제 호출 주소를 바인딩한 다음 직접 사용합니다. 이는 "액티브 바인딩"으로 해석할 수 있습니다.
- 지연 바인딩: 메서드가 호출될 때만 해당 호출 주소를 찾은 다음 실행합니다. 이는 "수동 바인딩"으로 해석할 수 있습니다.
이 데이터 구조는 section/section_64 구조로, mach-o/loader.h에 정의되어 있습니다.
struct section_64 { /* for 64-bit architectures */
char sectname[16]; //섹션의 이름, 16바이트를 차지합니다.
char segname[16]; //섹션이 속한 섹션의 이름으로, 16바이트를 차지합니다.
uint64_t addr; //가상 메모리에 매핑된 주소
uint64_t size; //섹션의 파일 오프셋 주소
uint32_t offset; //섹션의 파일 오프셋 주소
uint32_t align; //섹션의 바이트 정렬 크기
uint32_t reloff; //재배치 항목에 대한 파일 오프셋
uint32_t nreloc; //재배치할 항목 수
uint32_t flags; //섹션 유형 및 속성
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
LC_MAIN 명령: 메인 스레드 항목 주소와 스택 크기를 설정합니다.
바이너리가 메모리에 로드되면 바이너리의 진입점에서 실행이 시작되는데, dyld는 어떻게 진입점을 찾을 수 있을까요? LC_MAIN 명령!!!!
이 로드 명령은 entry_point_command: 유형의 구조입니다.
/*
* The entry_point_command is a replacement for thread_command.
* It is used for main executables to specify the location (file offset)
* of main(). If -stack_size was used at link time, the stacksize
* field will contain the stack size need for the main thread.
*/
struct entry_point_command {
uint32_t cmd; /* LC_MAIN MH에만 해당_EXECUTE */
uint32_t cmdsize; /* 24 */
uint64_t entryoff; /* main() __TEXT)오프셋은*/
uint64_t stacksize;/* 0이 아니면 스택 크기를 초기화합니다.*/
};
LC_MAIN 로드 명령의 가장 중요한 멤버는 바이너리 진입점의 오프셋을 포함하는 entryoff입니다. 로드 시 dyld는 단순히 바이너리 메모리의 기본 주소에 오프셋을 더한 다음 해당 명령어로 점프하여 바이너리 코드의 실행을 시작합니다.
Mach-O파일에는 LC에 정의될 생성자가 하나 이상 포함될 수 있습니다._MAIN생성자 함수의 오프셋은 생성자 함수의__DATA_CONST세그먼트의 섹션(__mod_init_func)
LC_LOAD_DYLIB 명령: 링크된 동적 라이브러리 로드
LC_LOAD_DYLD 로드 명령은 로더가 해당 라이브러리를 로드하고 연결하도록 지시하는 동적 라이브러리 종속성을 설명하며, Mach-O 파일이 종속하는 각 라이브러리에 대해 LC_LOAD_DYLIB 로드 명령이 있습니다.
이 명령의 구조는 dylib_command 유형입니다.
struct dylib_command {
uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
LC_REEXPORT_DYLIB */
uint32_t cmdsize; /* 명령 크기, 포함된 라이브러리의 경로명*/
struct dylib dylib; /* 동적 라이브러리 심볼*/
};
struct dylib {
union lc_str name; //라이브러리 경로명
uint32_t timestamp; //의존성 라이브러리 빌드 타임스탬프
uint32_t current_version; //라이브러리의 현재 버전
uint32_t compatibility_version; //라이브러리 호환성 버전
};
Mach_O 파일이 종속된 라이브러리를 확인하려면 otool -L view 또는 MachOView 프로그램을 사용하면 됩니다.
$ otool -L testOtool
testOtool:
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version , current version 1858.112.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version )
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.100.3)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version , current version 1858.112.0)
이 구조에 해당하는 명령은 LC_LOAD_WEAK_DYLIB일 수도 있으며, 둘 다 동적 라이브러리를 로드해야 함을 나타냅니다. 선택 사항이며 누락되어도 아무런 영향을 미치지 않는 LC_LOAD_WEAK_DYLIB를 통해 선언된 종속 라이브러리와 달리 메인 프로그램은 계속 실행되고 종속 라이브러리를 찾지 못하면 로더는 프로세스를 포기하고 종료합니다.
데이터 데이터 영역
로드 명령 뒤에 나오는 Mach-O 바이너리 파일의 데이터 영역은 주로 실제 바이너리 코드로 구성되며, LC_SEGMENT/LC_SEGMENT_64 로드 명령으로 설명되는 세그먼트로 구성되며 여러 섹션을 포함할 수 있습니다.
Symbol Table
심볼 테이블은 주소와 심볼을 연결하는 다리 역할을 합니다. 심볼 테이블은 심볼을 직접 저장하는 것이 아니라 문자열 테이블에 심볼이 있는 위치를 저장합니다.
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
이 명령은 링커에게 심볼 테이블과 문자열 테이블의 위치를 알려주며, 심볼 테이블의 위치와 항목을 나타내는 symoff와 nsyms, 문자열 테이블의 위치와 길이를 나타내는 stroff와 strsize로 구성된 비교적 간단한 symtab_command 구조입니다.
각 심볼 항목의 길이는 고정되어 있으며, 그 구조는 커널에 의해 정의됩니다.
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
nlist_64 구조체는 심볼의 기본 정보를 설명합니다. xnu는 5개의 필드로 심볼 정보를 설명하는데, 이 중 n_un, n_sect, n_value가 이해하기 쉽습니다:
n_un,심볼의 이름
n_sect, 심볼이 위치한 섹션 인덱스
bit[5:8]: 0이 아닌 경우 디버그 관련 기호이며, 값 유형에 대한 자세한 내용은 mach-o/stab.h를 참조하세요.
bit[4:5]: 1이면, 심볼은 비공개입니다.
bit[1:4]: 심볼 유형
- N_UNDF :
- N_ABS : 기호 주소는 절대 주소를 가리키며, 링커는 나중에 변경하지 않습니다.
- N_SECT: 로컬 심볼, 즉 현재 마하-O에 정의된 심볼입니다.
- N_PBUD : 미리 바인딩된 기호
- N_INDR: 해당 심볼이 다른 심볼과 동일함을 나타내며, n_값은 문자열 테이블, 즉 동일한 이름을 가진 심볼의 이름을 가리킵니다.
bit[0:1]: 외부 심볼임을 나타냅니다. 즉, 심볼이 외부에서 정의되었거나 로컬에서 정의되었지만 외부에서 사용할 수 있는 심볼입니다.




