바이너리 파일 포맷 속으로: PNG, ZIP, Mach-O 헤더 파헤치기
매일 쓰는 모든 파일 포맷(이미지, 아카이브, 실행 파일)에는 바이트 단위까지 정의된 구조가 있지만, 그 구조는 그런 파일을 여는 데 보통 쓰는 도구로는 거의 보이지 않습니다. 미리보기는 사진을 보여줍니다. 아카이브 유틸리티는 압축을 푼 파일을 보여줍니다. 어느 쪽도 그런 결과를 가능하게 한 바이트는 보여주지 않습니다. 흔한 세 가지 포맷이 속으로는 실제로 어떤 모습인지, 그리고 그것을 직접 들여다보는 방법을 소개합니다.
PNG: 라벨이 붙은 청크의 연속
모든 PNG 파일은 똑같은 여덟 바이트로 시작합니다: 89 50 4E 47 0D 0A 1A 0A. 그건 이미지 데이터가 아니라 시그니처입니다. 파일이 실수로 텍스트로 전송되면 못 쓰게 되는 바이트를 일부러 포함하도록 골라졌습니다(0x0A와 0x0D는 개행 문자라, 잘못된 텍스트 모드 전송은 이를 감지하기 쉬운 방식으로 손상시킵니다).
시그니처 다음에 오는 PNG의 모든 것은 청크이며, 모든 청크는 똑같은 네 부분 구조를 따릅니다:
- 길이(4바이트): 타입이나 CRC를 세지 않고, 뒤에 얼마나 많은 데이터 바이트가 오는지.
- 타입(4바이트, 항상 ASCII):
IHDR(이미지 헤더, 항상 첫 번째),IDAT(압축된 픽셀 데이터, 여러 개일 수 있음),IEND(파일의 끝을 표시) 같은 네 글자 코드. - 데이터: 그 청크 타입이 지정하는 무엇이든.
IHDR의 데이터는 너비, 높이, 비트 심도, 색 타입으로, 항상 똑같은 고정 배치입니다. - CRC(4바이트): 타입과 데이터의 체크섬으로, 디코더가 손상된 청크를 조용히 잘못 읽는 대신 감지할 수 있게 합니다.
이것이 포맷의 전부입니다. PNG 디코더는 그저 청크에서 청크로 걸어가며, 얼마나 건너뛸지 알기 위해 길이를 읽고, IEND에 닿을 때까지 반복합니다.
ZIP: 로컬 헤더, 그리고 끝에 있는 디렉터리
ZIP 아카이브는 각 파일을 자체 로컬 파일 헤더와 함께 저장하며, 이 헤더는 시그니처 50 4B 03 04로 표시됩니다(맨 앞의 PK는 포맷 창시자 Phil Katz를 가리킵니다). 그 헤더에는 파일 이름, 압축 방식, 그리고 압축된 크기와 압축되지 않은 크기가 모두 담기며, 바로 뒤에 그 파일의 압축된 데이터가 이어집니다.
사람들이 걸려 넘어지는 부분: 로컬 헤더에 있는 그 파일별 크기와 체크섬이 때때로 0이고, 진짜 값은 나중에 “데이터 디스크립터"에 저장됩니다. ZIP이 최종 크기를 데이터가 이미 기록되기 전에는 알 수 없는 스트리밍 쓰기를 지원하도록 설계되었기 때문입니다. 아카이브에 실제로 무엇이 들어 있는지에 대한 확정적인 목록은 파일의 맨 끝에 있는 중앙 디렉터리에 있습니다. 이것이 손상되거나 잘린 일부 ZIP이 처음부터 스캔하면 내용이 있는 것처럼 보이지만 여는 데 실패하는 이유이기도 합니다. 여러분이 쓰는 도구가 중앙 디렉터리를 먼저 읽는데 그것이 없거나 손상되었기 때문입니다.
Mach-O: 매직 넘버, 그다음 로드 커맨드
Mach-O는 macOS가 컴파일된 바이너리에 사용하는 실행 파일 포맷입니다. 로더에게 두 가지를 한 번에 알려주는 매직 넘버로 시작합니다. 32비트인지 64비트인지, 그리고 파일의 나머지가 어떤 바이트 순서를 쓰는지입니다. 0xFEEDFACE는 32비트, 0xFEEDFACF는 64비트이며, 바이트가 뒤집혀 보인다면(0xCEFAEDFE나 0xCFFAEDFE), 그 파일이 읽는 기계와 반대 바이트 순서로 기록되었다는 뜻입니다.
매직 넘버 다음에는 고정 헤더(CPU 타입, 파일 타입, 로드 커맨드 개수)가 오고, 이어서 그만큼의 로드 커맨드가 차례로 옵니다. 로드 커맨드는 Mach-O 파일이 로더에게 필요한 모든 것을 설명하는 방법입니다. 어떤 세그먼트를 메모리에 매핑할지, 어떤 동적 라이브러리를 링크할지, 진입점이 어디인지 말입니다. PNG의 청크와 비슷한 발상입니다. 고정 헤더, 그다음 스스로를 설명하는 레코드의 연속이며, 각 레코드가 다음으로 가려면 얼마나 건너뛰어야 하는지 읽는 쪽에 알려줍니다.
직접 들여다보기
이 중 어느 것도 파서를 작성할 필요가 없습니다. 어떤 PNG, ZIP, Mach-O 파일이든 Hexter에서 열면 분석 탭이 포맷을 자동으로 인식해, 실제 청크나 로드 커맨드 구조를 따라가며 원시 16진수 옆에 트리로 보여줍니다. 그래서 필드를 클릭하면 그것이 어떤 바이트에서 나왔는지 정확히 볼 수 있습니다. 기본값이 읽기 전용이라, 실제 파일(앱 바이너리, 스크린샷, 다운로드한 아카이브)에 대해서도 이를 바꿀 위험 없이 정말로 안전하게 할 수 있습니다.
위 내용을 대신 직접 손으로 확인하고 싶다면: PNG를 열어 처음 여덟 바이트가 시그니처와 일치하는지 확인한 다음, 8번부터 11번 바이트(첫 청크의 길이)와 12번부터 15번 바이트(그 타입, IHDR로 읽혀야 함)를 살펴보세요. 몇 분이면 할 수 있을 만큼 작은 연습이며, 평생 써 왔지만 실제로는 한 번도 들여다본 적 없는 파일 포맷을 이해하는 정말 유용한 방법입니다.