Mach-O 바이너리 검사기
otool과 lipo가 하는 방식 그대로, 바이트를 직접 읽어 Mach-O 바이너리를 해석합니다. 모든 처리는 브라우저 안에서만 이뤄집니다. macOS·iOS 파일은 아키텍처가 하나인 씬(thin) 파일이거나, 빅 엔디언 아키텍처 표가 각각 완전한 이미지를 가리키는 유니버설 파일입니다. 유니버설 파일이면 각 슬라이스의 오프셋·크기·정렬이 담긴 아키텍처 표를 먼저 보여 주고, 슬라이스를 고르면 나머지 상세를 보여 줍니다. 헤더에서는 CPU 타입과 서브타입(arm64, arm64e, x86_64, i386, ppc), 파일 종류(실행 파일·동적 라이브러리·번들·오브젝트·dSYM), 그리고 MH_PIE, MH_TWOLEVEL, MH_DYLDLINK, MH_ALLOW_STACK_EXECUTION, MH_NO_HEAP_EXECUTION 같은 플래그를 읽습니다. 로드 커맨드에서는 각 세그먼트의 VM 주소·VM 크기·파일 오프셋·파일 크기와 그 안의 섹션(__TEXT,__text 또는 __DATA,__const), LC_LOAD_DYLIB와 LC_LOAD_WEAK_DYLIB의 현재·호환 버전, @rpath가 치환될 LC_RPATH 경로, 라이브러리가 스스로 내세우는 LC_ID_DYLIB 설치 이름, dSYM과 짝을 이루는 UUID, LC_BUILD_VERSION(또는 예전의 LC_VERSION_MIN_MACOSX)의 플랫폼·최소 OS·SDK를 읽어 냅니다. 세그먼트 크기는 “이 바이너리가 왜 40MB인가”에 대한 답이지만, 메모리 크기와 파일 크기는 다릅니다. __bss 같은 제로필 섹션과 4GB짜리 __PAGEZERO는 디스크를 전혀 차지하지 않습니다. 하지 않는 일도 분명합니다. 디스어셈블은 하지 않고, 코드 서명도 검증하지 않습니다. LC_CODE_SIGNATURE는 서명 블록이 들어 있다는 사실일 뿐이며, 그 서명이 유효한지, 누구의 인증서인지, 공증을 받았는지는 Mac의 codesign과 spctl만 답할 수 있습니다. 바이트 순서와 워드 크기는 매직에서 읽어 내므로 20년 전 빅 엔디언 32비트 PowerPC 바이너리도 arm64e만큼 잘 열리며, 로드 커맨드 표가 파일보다 크다고 주장하는 파일은 절반만 읽는 대신 이름이 붙은 오류로 거부합니다.
사용법
- 바이너리를 상자에 놓으세요. 확장자 없는 명령줄 도구, .dylib, .bundle, .o, dSYM 안의 DWARF 파일, .app 안의 실행 파일 모두 됩니다.
- 유니버설 파일이라면 아키텍처 표부터 보세요. “arm64가 들어 있는가”에 대한 답입니다. 그 다음 아키텍처를 눌러 헤더·세그먼트·라이브러리를 확인하세요.
- 보호 설정 칩을 확인하세요. 요즘 빌드라면 PIE는 예여야 하고, 스택 실행이 허용돼 있다면 배포 전에 이유를 설명할 수 있어야 합니다.
- 연결된 라이브러리에서 실행 시 필요한 것들을, 런타임 검색 경로에서 @rpath가 펼쳐질 디렉터리를, 설치 이름에서 라이브러리가 주장하는 경로를 확인하세요.
- 용량을 추적할 때는 섹션 목록을 펼치세요. __TEXT는 코드, __DATA는 쓰기 가능한 데이터, __LINKEDIT는 심볼 테이블과 서명이며, dSYM의 __DWARF 섹션이 보통 가장 큽니다.
자주 묻는 질문
- 앱에 arm64(애플 실리콘) 슬라이스가 들어 있는지 어떻게 확인하나요?
- .app 안의 Contents/MacOS/<이름> 바이너리를 열고 아키텍처 표를 보세요. 유니버설 파일은 빅 엔디언 팻 헤더로 시작해 각 아키텍처와 파일 안 오프셋, 슬라이스가 놓인 정렬을 나열합니다. 거기 없는 아키텍처는 파일에도 없으므로, x86_64만 있는 앱은 네이티브가 아니라 로제타로 실행됩니다. 각 슬라이스는 자기 헤더와 로드 커맨드를 가진 완전한 Mach-O 이미지이며, 그래서 유니버설 바이너리 크기는 대략 부분의 합이고 lipo는 슬라이스 하나만 꺼내 파일을 줄일 수 있습니다.
- 코드 서명이 있으면 믿을 수 있는 바이너리인가요?
- 아닙니다. LC_CODE_SIGNATURE는 __LINKEDIT 안 어느 오프셋에 서명 블록이 있다는 사실만 말합니다. 해시가 지금 페이지와 일치하는지, 인증서가 애플까지 이어지는지, 폐기되지 않았는지, 공증을 받았는지는 말하지 않습니다. 그것을 확인하려면 인증서 체인이 필요하고 공증은 애플 서버까지 필요합니다. 즉 Mac에서 codesign --verify --deep --strict와 spctl --assess를 돌려야 합니다. 하드닝된 런타임도 헤더가 아니라 서명의 엔타이틀먼트와 플래그에 있으므로 헤더만으로는 켜져 있는지 알 수 없습니다.
- 코드보다 파일이 훨씬 큰 이유는 무엇인가요?
- 세그먼트 표에서 두 크기 열을 나란히 보세요. __TEXT는 코드와 읽기 전용 데이터로 보통 읽기·실행으로 매핑되고, __DATA와 __DATA_CONST는 쓰기 가능한 데이터이며, __LINKEDIT에는 심볼 테이블, 함수 시작 목록, dyld 픽스업, 코드 서명이 들어가 배포 바이너리에서 가장 큰 부분인 경우가 많습니다. 메모리 크기와 파일 크기가 다른 것은 의도된 것으로, __bss 같은 제로필 섹션은 주소 공간만 차지하고 디스크는 쓰지 않으며 __PAGEZERO는 파일에서 매핑되지 않는 4GB를 예약할 뿐입니다. 유니버설 파일이라면 모든 아키텍처를 한꺼번에 담고 있다는 점도 잊지 마세요.
- 설치 이름은 무엇이고 @rpath는 왜 계속 보이나요?
- 동적 라이브러리는 자신이 놓일 것으로 기대하는 경로를 LC_ID_DYLIB에 적고, 그 라이브러리를 링크하는 프로그램은 그 문자열을 자기 LC_LOAD_DYLIB에 그대로 복사합니다. 절대 경로라면 모든 기기에서 정확히 그 자리에 있어야 합니다. 요즘 프레임워크는 대신 @rpath/Something.framework/Something을 쓰고 해석을 프로그램에 맡깁니다. 여기 나열된 LC_RPATH 항목이 dyld가 @rpath 자리에 순서대로 넣어 볼 디렉터리이며, 앱 번들을 어디로 옮겨도 되도록 보통 @executable_path나 @loader_path 기준으로 적습니다. 실행 시 “image not found”가 나는 흔한 원인이 바로 빠진 rpath입니다.
- iOS 바이너리에서 암호화 칩은 무엇을 뜻하나요?
- App Store용 iOS 바이너리에는 cryptid가 1인 LC_ENCRYPTION_INFO(또는 64비트 형태)가 들어 있고, 이는 보통 __TEXT 대부분에 해당하는 구간이 스토어가 적용한 키로 암호화돼 있다는 표시입니다. 커널이 로드하면서 그 구간을 해독하므로 디스크의 파일은 그 영역을 읽을 수 없습니다. strings로도 쓸 만한 것이 나오지 않고 디스어셈블러에는 잡음으로 보입니다. 직접 빌드한 바이너리나 스토어 재서명 전 IPA에서 꺼낸 바이너리는 cryptid가 0이고 평문입니다. 커맨드는 있지만 값이 0인 파일도 많기 때문에 이 도구는 커맨드 유무가 아니라 cryptid를 봅니다.
- 바이너리가 어딘가로 업로드되나요?
- 아닙니다. 파일은 브라우저의 File API로 읽고 페이지 안 자바스크립트가 해석합니다. 서버로 보내지 않으며 보낼 서버 자체가 없습니다. 미출시 빌드나 고객사 앱을 웹사이트에 넘기고 싶지는 않을 테니, 이 도구에서는 특히 중요한 점입니다. 페이지를 연 뒤 네트워크를 끊어도 모든 기능이 그대로 동작합니다.
관련 도구
ELF 바이너리 검사기
리눅스 실행 파일·.so·.o를 브라우저에서 엽니다. 아키텍처, 필요한 공유 라이브러리, 빌드 ID, PIE·NX·RELRO 적용 여부까지.
PE 인스펙터: Windows EXE·DLL 뷰어
Windows의 .exe, .dll, .sys, .efi를 브라우저에서 엽니다. 아키텍처, 서브시스템, 임포트, 익스포트, 섹션, ASLR·DEP·CFG 적용 여부까지.
자바 클래스 파일 분석기
컴파일된 .class 파일을 브라우저에서 바로 분석합니다. 클래스 파일 버전과 자바 릴리스, 접근 플래그, 상수 풀, 필드, 메서드, 참조 클래스를 보여줍니다.
폰트 파일 검사기
.ttf, .otf, .woff 파일을 열어 실제 패밀리 이름, 글리프·문자 수, 버전, 라이선스 문구와 임베딩 허용 범위를 확인합니다.
gzip(.gz) 구조 분석기
.gz 헤더를 필드 단위로 읽고 모든 멤버를 훑은 뒤, 브라우저에서 CRC32와 실제 크기를 다시 계산해 트레일러를 검증합니다.
xz(.xz) 스트림 분석기
xz 컨테이너를 읽습니다. 스트림 헤더, 블록 헤더와 필터 체인, 인덱스와 푸터, 그리고 압축을 풀지 않고 알아낸 실제 원본 크기까지.