본문으로 건너뛰기

공통

1장 비트·바이트·16진수

비트 여덟 개가 바이트 하나입니다. 2진수와 16진수, 음수와 체크섬까지 숫자를 읽는 법을 익힙니다.

비트와 바이트 쉽게

컴퓨터 안의 모든 것은 "켜짐"과 "꺼짐" 두 가지로 적힙니다. 전구 하나를 떠올려 보세요. 켜지면 1, 꺼지면 0입니다. 이 0 또는 1 한 칸을 비트(bit)라고 합니다.

전구 하나로는 두 가지밖에 나타낼 수 없지만 여덟 개를 나란히 두면 켜고 끄는 조합이 2 × 2 × … × 2 = 256가지가 됩니다. 이 여덟 칸 묶음이 바이트(byte)이고, 0부터 255까지의 숫자 하나를 담습니다. 롬 파일은 이런 바이트가 수백만 개 늘어선 것입니다.

10진수와 2진수의 자릿값10진수 365는 3×100 + 6×10 + 5×1이다. 2진수 0011 0000은 자릿값 128, 64, 32, 16, 8, 4, 2, 1 가운데 32와 16 칸이 켜져 있어 48이다.10진수 365365×100×10×1= 300 + 60 + 52진수 0011 0000012806413211608040201= 32 + 16 = 48 (켜진 칸의 자릿값만 더함)
자릿값. 10진수는 칸마다 10배씩, 2진수는 칸마다 2배씩 커집니다.

10진수에서 오른쪽부터 1, 10, 100의 자리가 있듯 2진수에는 1, 2, 4, 8, … 128의 자리가 있습니다. 켜진 칸의 자릿값을 더하면 10진수가 됩니다. 아래 「비트 켜고 끄기」에서 직접 켜 보세요. 처음 값 $30은 돌격편 롬 헤더에 실제로 있는 바이트입니다(실제 패치에서).

비트 켜고 끄기

전구를 눌러 켜고 끄거나, 아래 칸에 16진수 두 자리를 적어 보세요.

전구 여덟 개가 바이트 하나입니다. 지금은 비트 5와 4가 켜져 있습니다(스크립트가 꺼져 있어 누를 수 없습니다).

2진수
0011 0000
16진수
$30
10진수
48
부호 있는 수로 읽으면
48

더 깊이음수, 비트 연산, 10진 모드

음수: 2의 보수

바이트에는 부호 칸이 따로 없습니다. 대신 "맨 왼쪽 비트(128의 자리)가 켜져 있으면 256을 빼서 읽는다"고 약속합니다. 이것을 2의 보수라고 합니다. $FF는 255이면서 −1이고, $80은 128이면서 −128입니다. 같은 바이트를 어느 쪽으로 읽을지는 그것을 쓰는 명령이 정합니다.

2의 보수 수직선$00부터 $7F까지는 0부터 127, $80부터 $FF까지는 −128부터 −1이다.$00$7F$80$FF0127−128−1양수(맨 왼쪽 비트 0)음수(맨 왼쪽 비트 1)
한 바이트를 부호 있는 수로 읽으면 $80부터 음수입니다.

2의 보수에서 −x는 "모든 비트를 뒤집고 1을 더한 것"입니다. 「비트 켜고 끄기」에서 $06을 넣고 「모두 뒤집기」, 「+1」을 차례로 누르면 $FA, 곧 −6이 됩니다. 5장의 분기 명령 D0 FA가 "6바이트 되돌아가라"인 이유가 이것입니다. 이때 거리는 분기 명령 자신이 아니라 그다음 명령의 위치부터 셉니다. D0 FA가 $800A에 있으면 다음 명령은 $800C이고, 거기서 6바이트 되돌아간 $8006으로 갑니다.

비트 연산

롬 해킹 공식에는 자릿수 계산보다 비트 연산이 자주 나옵니다.

  • AND(&): 둘 다 1인 칸만 1. 필요한 칸만 남기는 "마스크"로 씁니다. $A8 & $7F = $28은 맨 왼쪽 비트만 지운 것입니다(4장의 LoROM 공식).
  • OR(|): 하나라도 1이면 1. 특정 칸을 켤 때 씁니다.
  • XOR(^): 서로 다르면 1. ^ $FF는 모든 칸 뒤집기입니다(「비트 켜고 끄기」의 「모두 뒤집기」).
  • 시프트(<<, >>): 칸을 왼쪽·오른쪽으로 미는 것으로, 한 칸에 ×2, ÷2입니다. n << 15는 n × $8000입니다.

10진 모드(BCD)

점수처럼 화면에 10진수로 보여 줄 값은 한 바이트의 두 16진수 자리에 10진수 두 자리를 따로 넣기도 합니다. $25를 "25"로 읽는 방식으로, BCD라고 합니다. 65c816은 P 레지스터의 D 플래그를 켜면 덧셈(ADC)·뺄셈(SBC)을 이 방식으로 계산합니다. 롬에서 $99 다음이 $00이 되는 값을 보면 BCD를 의심해 보세요.

16진수: 네 칸씩 묶어 읽기 쉽게

0011 0000처럼 0과 1을 여덟 개씩 적으면 읽기 힘듭니다. 그래서 네 칸씩 끊어 한 글자로 적습니다. 네 칸은 0부터 15까지 16가지라서, 0–9 다음에 A–F를 이어 붙여 16가지 글자를 씁니다. 이것이 16진수입니다.

2진수 여덟 칸을 16진수 두 자리로0011 0000의 앞 네 칸 0011은 3, 뒤 네 칸 0000은 0이라 16진수로 30, $30이라고 적는다.0 0 1 10 0 0 02 + 1030→ $30
바이트 하나 = 네 칸 두 묶음 = 16진수 두 자리
16진수 한 자리와 2진수 네 칸
10진수2진수16진수10진수2진수16진수
000000810008
100011910019
200102101010A
300113111011B
401004121100C
501015131101D
601106141110E
701117151111F

16진수라는 것을 표시하는 방법이 여러 가지라 처음엔 헷갈립니다. $30, 0x30, 30h는 모두 같은 값(10진수 48)입니다. 게임기 쪽 문서와 어셈블리는 $를, 파일 오프셋과 프로그래밍 언어는 0x를 주로 씁니다. 이 가이드도 CPU 주소는 $, 파일 위치는 0x로 적습니다.

KB와 MB, 2의 거듭제곱 쉽게

롬 크기를 말할 때 1KB는 1000이 아니라 210 = 1,024바이트이고, 1MB는 1,024KB = 1,048,576바이트입니다. 1,000단위와 헷갈리지 않게 KiB·MiB라고 적기도 하지만, 롬 해킹 자료는 대부분 KB·MB를 1,024단위로 씁니다. 16진수로 적으면 깔끔합니다. 1KB = $400, 32KB = $8000, 64KB = $10000, 1MB = $100000입니다. 앞으로 자주 나오는 숫자이니 눈에 익혀 두세요.

롬 크기 바이트헤더의 롬 크기 바이트가 0A면 2의 10제곱 KB, 곧 1MB이고 16진수로 $100000바이트다. 0B는 2MB($200000), 0C는 4MB($400000)다.헤더 바이트뜻바이트 수0A2¹⁰KB = 1MB$100000제절초 원본0B2¹¹KB = 2MB$200000돌격편·각성편0C2¹²KB = 4MB$400000슈퍼 마리오 RPG
두 배씩 커지는 크기와 16진수. 롬 헤더는 크기를 '2의 몇 제곱 KB'로 적습니다.

실제 패치에서 실제 패치에서

헤더 바이트 하나에 정보 두 개

돌격편 롬의 파일 0x7FD5에는 $30이 있습니다. 2진수로 0011 0000입니다. 이 바이트는 칸마다 뜻이 따로 있어서, 오른쪽 네 칸 0000은 "LoROM 방식" (4장), 비트 4(16의 자리)는 "빠른 롬(FastROM)"을 뜻합니다. 같은 LoROM이지만 빠른 롬을 쓰지 않는 제절초는 $20입니다. 「비트 켜고 끄기」에서 비트 4만 꺼 보면 $30이 $20이 됩니다.

체크섬: 모든 바이트의 합

롬 헤더에는 롬의 모든 바이트를 더한 값의 아래 16비트(체크섬)와, 그 값의 모든 비트를 뒤집은 값(보수)이 들어 있습니다. 둘을 더하면 언제나 $FFFF입니다. 패치로 바이트를 바꾸면 합도 바뀌므로 다시 계산해 적어야 합니다.

롬체크섬보수합
돌격편 일본판$10F0$EF0F$FFFF
돌격편 v1.1$2B67$D498$FFFF
슈퍼 마리오 RPG 일본판$4575$BA8A$FFFF

실제 게임기는 이 값을 확인하지 않아서 틀려도 게임은 돌아갑니다. 하지만 에뮬레이터나 롬 관리 도구가 "손상된 롬"으로 표시하므로 한글패치들은 마지막 단계에서 다시 계산합니다.

더 깊이체크섬을 정확히 계산하는 법
체크섬과 보수롬의 모든 바이트를 더한 값에서 아래 16비트만 남기면 체크섬 $10F0이다. 모든 비트를 뒤집으면 보수 $EF0F이고, 둘을 더하면 $FFFF다.롬의 모든 바이트를 더함 → …10F0아래 16비트만 남김(& $FFFF) → 체크섬 $10F0모든 비트 뒤집기(^ $FFFF) → 보수 $EF0F확인: $10F0 + $EF0F = $FFFF
모든 바이트의 합에서 체크섬과 보수를 얻는 순서(돌격편 일본판)
  • 더할 때 헤더의 보수·체크섬 네 바이트는 FF FF 00 00으로 놓고 셉니다. 보수와 체크섬은 서로 비트를 뒤집은 값이라 네 바이트의 합이 언제나 $1FE로 같아서, 어떤 값이 들어 있든 미리 정해 둔 값으로 더해도 결과가 같습니다.
  • 롬 크기가 2의 거듭제곱이 아니면(예: 3MB) 카트리지가 보여 주는 모양대로 더합니다. 가장 큰 2의 거듭제곱 부분(2MB)을 한 번, 나머지(1MB)는 그 크기(2MB)가 될 때까지 되풀이해 더합니다. kr-patch-toolkit의 krpatch snes checksum이 이렇게 계산합니다.
  • 체크섬 자리는 헤더 위치에 따라 다릅니다. LoROM·SA-1은 파일 0x7FDC–0x7FDF, HiROM은 0xFFDC–0xFFDF, ExHiROM은 0x40FFDC–0x40FFDF입니다(보수 2바이트, 체크섬 2바이트, 각각 작은 자리 먼저). 카피어 헤더가 붙은 파일은 그 512바이트를 빼고 셉니다.

글자 코드의 맨 위 비트

돌격편 대사는 글자 하나를 16비트 코드로 적는데, 맨 위 비트($8000)가 켜진 코드는 작은 글자로 그립니다. 비트 하나를 "글자 크기 표시"로 빌려 쓴 것입니다. 이렇게 한 칸 한 칸에 뜻을 싣는 일이 롬 안에서는 아주 흔합니다.

직접 해 보기 직접 해 보기

헤더 바이트 세 개(맵 모드, 카트리지 종류, 롬 크기)를 헥스 뷰어로 봅니다. 돌격편 일본판이라면 30 02 0B가 나옵니다.

xxd -s 0x7FD5 -l 3 game.sfc

체크섬은 툴킷이 계산해 줍니다. 틀리면 1로 끝나고, --fix를 주면 제자리에 다시 씁니다.

krpatch snes checksum game.sfc
krpatch snes checksum game.sfc --fix