rubrapack 매뉴얼←↑→

22 파일 속의 수, 그리고 비트 플래그

파일 형식은 크기, 개수, 위치, 판 번호 같은 수를 정해진 수의 바이트로 저장한다. 이 장은 그 방법과, 수 하나가 예/아니요 설정을 한꺼번에 여럿 담는 방법을 보인다.

22.1 1, 2, 4, 8 바이트#

파일에 저장된 수는 형식이 정한 만큼의 바이트를 차지한다:

크기비트부호 없는 범위흔한 이름
1 바이트80 ~ 255byte, u8, uint8
2 바이트160 ~ 65,535word, u16, uint16, short
4 바이트320 ~ 4,294,967,295double word(DWORD), u32, uint32, int
8 바이트640 ~ 약 1.8 x 10^19quad 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장의 복합 파일 머리에서:

오프셋바이트뒤집으면값뜻
0x183e 0000 3e62부 판 번호
0x1A04 0000 044주 판 번호: 4 KiB 섹터
0x1Cfe ffff fe0xFFFE바이트 순서 표시: 0xFEFF 를 얻은 읽기 프로그램은 거꾸로 읽었음을 안다
0x1E0c 0000 0c12섹터 크기는 2^12 = 4096
0x3800 10 00 0000 00 10 004096이보다 작은 스트림은 미니 스트림에 들어간다

22.3 음수: 2의 보수#

음수가 필요한 형식은 2의 보수를 쓴다: 같은 비트를 쓰되 범위의 위쪽 절반을 음수로 읽는다. 1바이트에서 0~127 은 그대로이고, 128~255 가 -128~-1 을 나타낸다:

바이트부호 없이부호 있게
0000
7f127127
80128-128
fe254-2
ff255-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진수켜진 비트뜻(튜토리얼 장)
2581 0000 0010256 + 2Upgrade 행: 최소 판 포함, 찾기만 함(3)
2721 0001 0000256 + 16Component: 64비트, 영구(4)
51210 0000 0000512File: 필수(2, 18)
1631010 0011128 + 32 + 2 + 1ServiceControl: 제거 때 삭제, 제거 때 멈춤, 설치 때 멈춤, 설치 때 시작(12)
30901100 0001 00102048 + 1024 + 16 + 2사용자 지정 동작: 가장(impersonation) 없음, 지연, 원본은 File, .exe(13)
34101101 0101 00102048 + 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 이 모두 계산한다 - 수가 왜 그렇게 생겼는지 설명해 준다.

22.6 쓰이는 곳#