22 파일 속의 수, 그리고 비트 플래그
파일 형식은 크기, 개수, 위치, 판 번호 같은 수를 정해진 수의 바이트로 저장한다. 이 장은 그 방법과, 수 하나가 예/아니요 설정을 한꺼번에 여럿 담는 방법을 보인다.
22.1 1, 2, 4, 8 바이트#
파일에 저장된 수는 형식이 정한 만큼의 바이트를 차지한다:
| 크기 | 비트 | 부호 없는 범위 | 흔한 이름 |
|---|---|---|---|
| 1 바이트 | 8 | 0 ~ 255 | byte, u8, uint8 |
| 2 바이트 | 16 | 0 ~ 65,535 | word, u16, uint16, short |
| 4 바이트 | 32 | 0 ~ 4,294,967,295 | double word(DWORD), u32, uint32, int |
| 8 바이트 | 64 | 0 ~ 약 1.8 x 10^19 | quad word(QWORD), u64, uint64 |
부호 없는(unsigned)은 음수가 없다는 뜻이다. 크기에 비해 너무 큰 수는 저장할 수 없다 - 그래서 i2(2 바이트)로 선언된 MSI 표의 열은 100,000 을 담지 못하고, 위치를 4 KiB 섹터의 4바이트 번호로 적는 복합 파일에는 크기 상한이 있다.
22.2 바이트 순서: little-endian#
여러 바이트로 된 수는 바이트로 쪼개진다. 튜토리얼의 hello.exe 크기인 17,920 바이트를 보자. 16진수로는 0x00004600 이고, 큰 자리부터 네 바이트로 쓰면 00 00 46 00 이다. 그런데 Windows 용 파일은 거의 언제나 바이트를 반대로, 작은 자리부터 저장한다:
17920 = 0x00004600
most significant first (big-endian): 00 00 46 00
least significant first (little-endian): 00 46 00 00
| | | |
| | | +-- x 16,777,216 (256^3) = 0
| | +----- x 65,536 (256^2) = 0
| +-------- x 256 = 17,920 (0x46 = 70)
+----------- x 1 = 0
이 순서를 little-endian("작은 끝 먼저")이라 하고, 반대는 big-endian 이라 한다. Windows 컴퓨터의 Intel 과 Arm 처리기는 little-endian 으로 일하고, MSI, CAB, ZIP, 프로그램 파일 형식도 그렇다. 네트워크 프로토콜과 인증서(8장)는 big-endian 을 쓴다. 제4부의 표가 "u32 LE" 라고 하면 "부호 없는 4바이트 수, little-endian" 이라는 뜻이다.
덤프에서 little-endian 수를 읽는 법: 그 바이트들을 뒤집어 16진수로 읽는다. 1장의 복합 파일 머리에서:
| 오프셋 | 바이트 | 뒤집으면 | 값 | 뜻 |
|---|---|---|---|---|
0x18 | 3e 00 | 00 3e | 62 | 부 판 번호 |
0x1A | 04 00 | 00 04 | 4 | 주 판 번호: 4 KiB 섹터 |
0x1C | fe ff | ff fe | 0xFFFE | 바이트 순서 표시: 0xFEFF 를 얻은 읽기 프로그램은 거꾸로 읽었음을 안다 |
0x1E | 0c 00 | 00 0c | 12 | 섹터 크기는 2^12 = 4096 |
0x38 | 00 10 00 00 | 00 00 10 00 | 4096 | 이보다 작은 스트림은 미니 스트림에 들어간다 |
22.3 음수: 2의 보수#
음수가 필요한 형식은 2의 보수를 쓴다: 같은 비트를 쓰되 범위의 위쪽 절반을 음수로 읽는다. 1바이트에서 0~127 은 그대로이고, 128~255 가 -128~-1 을 나타낸다:
| 바이트 | 부호 없이 | 부호 있게 |
|---|---|---|
00 | 0 | 0 |
7f | 127 | 127 |
80 | 128 | -128 |
fe | 254 | -2 |
ff | 255 | -1 |
규칙: 맨 위 비트가 1 이면 음수이고, 값은 부호 없는 값에서 2^비트수 를 뺀 것이다. 4바이트에서 ff ff ff ff 는 -1, fe ff ff ff 는 -2 다. 형식들은 이런 "모두 1" 값을 표시로 즐겨 쓴다: 복합 파일에서 섹터 번호 0xFFFFFFFE(-2)는 "사슬의 끝", 0xFFFFFFFF(-1)는 "비어 있음"이다.
22.4 비트 플래그#
형식이 한 가지에 대해 예/아니요 설정을 여럿 두어야 할 때가 많다. 설정마다 바이트를 하나씩 주는 대신, 설정마다 수의 비트 하나를 주고, 켜진 비트의 값을 더한 것이 그 수가 된다. 비트마다 값이 2의 거듭제곱 - 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, ... - 이므로 어떤 합이든 만드는 방법은 하나뿐이고, 그 수에서 설정을 거꾸로 읽어 낼 수 있다.
튜토리얼에서 이런 수를 여럿 만났다. 여기서 분해해 본다 - 수를 2진수로 쓰면 1 하나하나가 설정 하나다:
| 수 | 2진수 | 켜진 비트 | 뜻(튜토리얼 장) |
|---|---|---|---|
| 258 | 1 0000 0010 | 256 + 2 | Upgrade 행: 최소 판 포함, 찾기만 함(3) |
| 272 | 1 0001 0000 | 256 + 16 | Component: 64비트, 영구(4) |
| 512 | 10 0000 0000 | 512 | File: 필수(2, 18) |
| 163 | 1010 0011 | 128 + 32 + 2 + 1 | ServiceControl: 제거 때 삭제, 제거 때 멈춤, 설치 때 멈춤, 설치 때 시작(12) |
| 3090 | 1100 0001 0010 | 2048 + 1024 + 16 + 2 | 사용자 지정 동작: 가장(impersonation) 없음, 지연, 원본은 File, .exe(13) |
| 3410 | 1101 0101 0010 | 2048 + 1024 + 256 + 64 + 16 + 2 | 같은 것에 되돌리기와 종료 코드 무시를 더함(13) |
손으로 분해하려면 들어가는 가장 큰 2의 거듭제곱을 빼고 되풀이한다: 3090 - 2048 = 1042, 1042 - 1024 = 18, 18 - 16 = 2, 2 - 2 = 0. 그래서 3090 = 2048 + 1024 + 16 + 2 다. (Microsoft 문서는 사용자 지정 동작의 2 + 16 을 합쳐 "형식 18" 이라 부른다: 패키지가 설치하는 .exe 를 실행한다.)
16진수로 보면 플래그가 잘 보인다. 16진수 한 자리가 4비트이기 때문이다: 3090 = 0xC12 이고, C = 1100 은 2048 과 1024, 1 = 0001 은 16, 2 = 0010 은 2 다.
22.5 비트 확인하기와 켜기: AND, OR#
프로그램은 AND 로 플래그를 확인한다: 두 수에서 모두 1 인 비트만 남긴다.
3090 = 1100 0001 0010
AND 1024 = 0100 0000 0000
= 0100 0000 0000 not zero: "deferred" is on
OR(어느 한쪽이라도 1 이면 1)로 비트를 켜고, 반대 수와의 AND 로 끈다. Windows Installer 조건(튜토리얼 9장)에도 같은 확인이 연산자 >< 로 있다: MYFLAGS >< 2 는 속성 MYFLAGS 에서 값이 2 인 비트가 켜져 있으면 참이다. rubrapack 을 쓰는 데 이것들이 필요하지는 않지만 - 이 수들은 rubrapack 이 모두 계산한다 - 수가 왜 그렇게 생겼는지 설명해 준다.