본문으로 건너뛰기

슈퍼 패미컴

4장 주소와 뱅크

$80:8000은 "80번 책의 8000쪽"입니다. LoROM·HiROM·SA-1이 롬 파일을 CPU 주소에 어떻게 펼치는지 계산해 봅니다.

뱅크: 주소를 64KB씩 나눈 묶음 쉽게

65c816은 주소가 24비트라서 16MB까지 가리킬 수 있습니다. 이 24비트를 앞 8비트와 뒤 16비트로 나눠 적는 것이 관례입니다. 앞 8비트가 뱅크, 뒤 16비트가 뱅크 안의 주소입니다.

책장에 책이 256권(뱅크 $00–$FF) 꽂혀 있고, 책 한 권은 65,536쪽(64KB, $0000–$FFFF)이라고 생각해 보세요. $80:8000은 "$80번 책의 $8000쪽"입니다. 콜론은 읽기 쉽게 넣은 것이라 $808000과 같은 주소입니다.

뱅크와 주소$A8:800C는 뱅크 A8과 뱅크 안 주소 800C로 나뉜다. 책장의 A8번 책을 꺼내 800C쪽을 펼치는 것과 같다.$A8:800C뱅크(책 번호)주소(쪽)$00 … 책 256권 … $FF800C쪽한 권 = 64KB
$A8:800C를 뱅크와 뱅크 안 주소로 나누어 읽기

CPU 주소와 파일 위치는 다르다 쉽게

롬 파일은 처음부터 끝까지 이어진 바이트 줄이지만, 게임기는 롬을 책장에 그대로 꽂지 않습니다. 카트리지 기판의 배선에 따라 롬 조각이 책장의 정해진 자리에 놓입니다. 이 배치 규칙을 매핑이라고 하고, 대표적인 것이 LoROM과 HiROM입니다. 그래서 "CPU 주소 $A8:800C는 파일의 몇 번째 바이트인가"를 계산하려면 그 게임의 매핑을 알아야 합니다.

LoROM 매핑롬 파일을 32KB 조각 0, 1, 2로 자르면 조각 0은 뱅크 $80의 $8000-$FFFF, 조각 1은 뱅크 $81의 $8000-$FFFF, 조각 2는 뱅크 $82의 $8000-$FFFF에 놓인다. 각 뱅크의 아래쪽 반 $0000-$7FFF는 롬이 아니다.롬 파일0x000000-0x008000-0x010000-…$80$0000-$7FFF$8000-$FFFF$81$0000-$7FFF$8000-$FFFF$82$0000-$7FFF$8000-$FFFF회색 칸은 롬이 아닌 시스템 영역(아래 절)
LoROM: 롬을 32KB씩 잘라 뱅크마다 위쪽 반에만 꽂습니다.
HiROM 매핑롬 파일을 64KB 조각으로 자르면 조각 0은 뱅크 $C0 전체, 조각 1은 뱅크 $C1 전체에 놓인다. 파일 오프셋은 (뱅크 & $3F) × $10000 + 주소다.롬 파일0x000000-0x00FFFF0x010000-0x01FFFF$C0$C1$0000-$FFFF 전체$0000-$FFFF 전체
HiROM: 롬을 64KB씩 잘라 뱅크 $C0부터 통째로 꽂습니다.

LoROM은 뱅크마다 32KB만 롬입니다. 뱅크 번호에서 맨 위 비트를 떼고($A8 → $28) $8000을 곱한 다음, 주소에서 $8000을 뺀 값을 더합니다. $A8:800C라면 $28 × $8000 + ($800C − $8000) = $140000 + $000C, 곧 파일 0x14000C입니다.

HiROM은 뱅크 $C0–$FF에 64KB씩 통째로 놓입니다. 뱅크 번호에서 $C0을 빼고 $10000을 곱한 다음, 주소를 그대로 더합니다. $C1:2345라면 ($C1 − $C0) × $10000 + $2345 = 파일 0x012345입니다.

아래 계산기는 kr-patch-toolkit의 krpatch snes dis와 같은 공식을 쓰고, 툴킷의 테스트 사례로 결과가 같은지 확인했습니다. 다른 뱅크 번호와 주소도 넣어 보세요.

주소 → 롬 위치 계산기

CPU 주소 $뱅크:주소를 롬 파일 오프셋으로 바꿉니다. 계산은 kr-patch-toolkit의 krpatch snes dis와 같은 공식입니다. 매핑을 고르고 주소나 오프셋 중 하나를 고쳐 보세요.

매핑

LoROM 롬에서 $A8:800C는 롬이고, 공식으로 계산한 파일 위치는 0x14000C입니다.

  1. 뱅크 $A8에서 맨 위 비트를 뗍니다: $A8 & $7F = $28
  2. 뱅크 하나는 롬 32KB($8000바이트)입니다: $28 × $8000 = $140000
  3. 주소 $800C에서 위쪽 반 표시($8000)를 뗍니다: $800C & $7FFF = $000C
  4. 둘을 더하면 파일 오프셋 0x14000C

뱅크 256개 한눈에 보기 (한 칸 = 뱅크 하나, 왼쪽 반 $0000-$7FFF · 오른쪽 반 $8000-$FFFF)

  • 뱅크 전체가 롬
  • 오른쪽 반(위쪽 32KB)만 롬
  • WRAM($7E·$7F)
  • SA-1 BW-RAM
  • 롬이 아님
  • 지금 고른 뱅크

뱅크 아래쪽 반: WRAM과 레지스터 쉽게

LoROM 그림에서 회색으로 남은 아래쪽 반($0000–$7FFF)에는 롬 대신 게임기 본체의 것들이 있습니다. 뱅크 $00–$3F와 $80–$BF는 모두 이 모양이라, 어느 뱅크에서 코드를 실행하든 같은 주소로 작업 메모리와 레지스터에 닿을 수 있습니다.

시스템 뱅크의 구성$0000-$1FFF는 WRAM 앞 8KB가 비쳐 보이는 곳, $2000-$20FF는 쓰지 않음, $2100-$21FF는 PPU·APU 레지스터, $2200-$3FFF는 쓰지 않음, $4000-$41FF는 옛 방식 컨트롤러 포트, $4200-$5FFF는 CPU 레지스터, $6000-$7FFF는 확장 영역, $8000-$FFFF는 롬이다.$0000-$1FFFWRAM 앞 8KB가 비쳐 보임$2000-$20FF쓰지 않음$2100-$21FFPPU·APU 레지스터화면과 소리$2200-$3FFF쓰지 않음SA-1 같은 카트리지 칩이 쓰기도 함$4000-$41FF옛 방식 컨트롤러 포트이 구간만 1.78MHz로 느림$4200-$5FFFCPU 레지스터DMA·인터럽트·곱셈 등$6000-$7FFF확장 영역HiROM 세이브 RAM($20-$3F·$A0-$BF만)$8000-$FFFF롬
뱅크 $00-$3F·$80-$BF 한 권의 쪽 배치

게임의 작업 메모리 WRAM(128KB)은 뱅크 $7E와 $7F에 있습니다. 그런데 앞 8KB는 다른 뱅크의 $0000–$1FFF에서도 보입니다. 그래서 $80:0A00에 적으면 실제로는 $7E:0A00에 적힙니다. 이처럼 같은 것이 두 주소에서 보이는 것을 미러(거울)라고 합니다.

더 깊이매핑 공식 전부: 미러, FastROM, ExHiROM, SA-1

롬도 두 번 보인다

LoROM 롬은 뱅크 $00–$3F에 한 번, $80–$BF에 한 번 더 보입니다. $00:8000과 $80:8000은 같은 바이트입니다. 차이는 속도입니다. 게임이 레지스터 $420D의 비트 0을 켜면 $80–$BF쪽 롬(과 $C0–$FF)은 3.58MHz로 읽히고, $00–$3F 쪽은 늘 2.68MHz입니다. 헤더의 FastROM 비트는 "빠르게 읽어도 되는 롬"이라는 표시일 뿐 속도를 바꾸지 않습니다. 그래서 FastROM 게임 주소는 대개 $80:8000처럼 적고, 툴킷도 파일 오프셋을 CPU 주소로 바꿀 때 $80 이상 뱅크로 적습니다. 공식에서 & $7F는 이 두 거울을 같은 자리로 모으는 것입니다.

롬 미러뱅크 $00-$3F의 $8000-$FFFF와 뱅크 $80-$BF의 $8000-$FFFF가 같은 롬 칩을 가리킨다. 앞쪽은 늘 2.68MHz, 뒤쪽은 FastROM을 켜면 3.58MHz다.$00-$3F:$8000-$FFFF · 2.68MHz$80-$BF:$8000-$FFFF · 최대 3.58MHz롬 칩같은 바이트
LoROM 롬 칩 하나가 두 뱅크 범위에 함께 보입니다.
  • LoROM: 파일 = (뱅크 & $7F) × $8000 + (주소 & $7FFF). 보통 2MB까지 $00–$3F에 놓고, 더 큰 롬은 $40–$7D(와 $C0–$FF)로 이어 4MB까지 씁니다.
  • HiROM: 파일 = (뱅크 & $3F) × $10000 + 주소. 롬은 $C0–$FF(와 $40–$7D)에 통째로 보이고, $00–$3F·$80–$BF의 위쪽 반에도 각 64KB의 뒤쪽 32KB가 보입니다. 최대 4MB입니다.
  • ExHiROM: 4MB를 넘는 HiROM. 앞 4MB는 $C0–$FF에, 뒤쪽은 $40–$7D에 놓습니다. 파일 = (뱅크가 $80 이상이면 0, 아니면 $400000) + (뱅크 & $3F) × $10000 + 주소.

SA-1의 창 바꾸기(Super MMC)

SA-1 카트리지는 롬을 1MB 블록으로 나누고, 레지스터 $2220–$2223으로 블록을 골라 네 개의 창에 보여 줍니다. 창은 HiROM식 $C0–$CF·$D0–$DF·$E0–$EF·$F0–$FF와, LoROM식 $00–$1F·$20–$3F·$80–$9F·$A0–$BF의 위쪽 반입니다. 전원을 켜면 네 레지스터가 0, 1, 2, 3이라 처음 4MB가 차례로 보입니다. 각 레지스터의 비트 7이 0이면 LoROM식 창은 블록 선택과 상관없이 앞 2MB($00–$3F)와 다음 2MB($80–$BF)를 보여 줍니다. 이 장의 계산기와 툴킷은 이 전원 상태로 계산합니다.

SA-1 쪽 RAM도 있습니다. 칩 안의 I-RAM 2KB는 $00:3000–$37FF에, 카트리지의 BW-RAM은 뱅크 $40–$43에 있고, BW-RAM 중 8KB가 레지스터 $2224가 고른 대로 $00:6000–$7FFF에 보입니다. 뱅크 $60–$6F의 BW-RAM 비트맵 보기는 SA-1에서만 보이고 본체 CPU에서는 보이지 않습니다.

롬이 어느 매핑인지 알아내기

롬 안에는 게임 이름과 크기를 적은 내부 헤더가 CPU 주소 $00:FFC0에 있습니다. 파일로는 LoROM·SA-1이 0x7FC0, HiROM이 0xFFC0, ExHiROM이 0x40FFC0입니다. 세 자리를 읽어 보고 게임 이름이 제대로 보이는 쪽이 답입니다. 확인은 헤더의 맵 모드 바이트(+0x15)로 합니다. 아래 네 비트가 0이면 LoROM, 1이면 HiROM, 3이면 SA-1, 5면 ExHiROM이고, 비트 4는 FastROM 표시, 비트 5는 늘 1입니다.

뱅크 경계

PC는 16비트라서 코드가 뱅크 끝 $FFFF를 지나면 다음 뱅크가 아니라 같은 뱅크의 처음으로 돌아갑니다. 2바이트 포인터도 같은 뱅크 안만 가리킵니다. 다른 뱅크로 가려면 3바이트 주소를 쓰는 JSL· JML 같은 명령을 써야 합니다.

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

돌격편: 대사는 뱅크 밖으로 못 나간다

돌격편(LoROM)은 대사 5,370줄을 아홉 개 뱅크에 나눠 두고, 뱅크마다 2바이트 포인터 표를 둡니다. 대사 0x006의 포인터는 $A8:8000 + 0x006 × 2 = $A8:800C, 파일로는 0x28 × 0x8000 + 0x000C = 0x14000C입니다. 「주소 → 롬 위치 계산기」의 처음 값이 이 주소입니다.

포인터가 2바이트라 대사는 자기 뱅크(32KB) 안에서만 옮길 수 있습니다. 한국어 번역이 일본어보다 길어져도 다른 뱅크로 넘길 수 없어서, v1.1은 뱅크마다 문자열 영역을 처음부터 다시 채우고 같은 문자열은 하나만 두어 공간을 아꼈습니다.

슈퍼 마리오 RPG: 4MB를 넘기지 않은 이유

슈퍼 마리오 RPG는 SA-1 카트리지입니다. 새 한글 이름 표는 $E2:B400(파일 0x22B400)에 있습니다. 롬을 4MB보다 키우면 실행 중에 창의 블록을 바꿔야 하는데, 두 CPU가 동시에 돌아서 바꾸는 사이 다른 쪽이 엉뚱한 블록을 읽을 수 있습니다. 그래서 롬을 늘리지 않고 원래 비어 있던 공간만 썼습니다.

제절초: 뱅크를 넘는 대본과 세이브 RAM

제절초(LoROM) 한글판 대본은 $20:8000부터 이어지고, 뱅크 끝 $XX:FFFF를 넘으면 읽는 코드가 다음 뱅크의 $8000으로 건너갑니다. LoROM은 뱅크마다 위쪽 반만 롬이라 파일에서는 끊김 없이 이어진 바이트입니다. 세이브 데이터는 LoROM의 세이브 RAM 자리인 $70:0000– $70:1FFF(8KB)에 있습니다.

직접 해 보기 직접 해 보기

역어셈블러에는 CPU 주소를 줍니다. 매핑은 헤더를 읽어 고르고(auto), 직접 정할 수도 있습니다.

krpatch snes dis game.sfc 81AC4C 20 --mapper lorom --m16   # 돌격편: 16비트 대사 ID로 뱅크를 고르는 루틴
krpatch snes dis smrpg.sfc C08000 --mapper sa1

Python에서는 매핑 객체로 두 방향 변환을 할 수 있습니다.

>>> from kr_patch.snes import mapper
>>> lo = mapper.LoROM()
>>> hex(lo.to_offset(0xA8800C))
'0x14000c'
>>> hex(lo.to_cpu(0x14000C))
'0xa8800c'