본문으로 건너뛰기

슈퍼 패미컴

3장 CPU와 65c816

CPU는 명령을 한 줄씩 읽어 실행하는 일꾼입니다. 슈퍼 패미컴의 65c816이 가진 레지스터와 8/16비트 전환을 봅니다.

CPU: 명령을 한 줄씩 실행하는 장치 쉽게

롬 안의 프로그램은 "A에 5를 넣어라", "그 값을 어디에 적어라" 같은 아주 작은 명령이 줄지어 있는 것입니다. CPU는 이 명령을 한 줄 읽고, 무슨 뜻인지 알아내고, 실행한 다음, 다음 줄로 넘어가는 일을 1초에 수십만에서 백만 번 가까이 되풀이하는 일꾼입니다.

지금 몇 번째 줄을 읽는지는 PC(프로그램 카운터)라는 책갈피가 기억합니다. 보통은 한 줄을 실행하면 책갈피가 다음 줄로 넘어가지만, "어디로 가라"는 명령은 책갈피를 다른 곳에 꽂습니다. 반복과 갈림길은 모두 이렇게 만들어집니다.

읽기, 해석, 실행의 반복읽기, 해석, 실행 세 단계가 고리처럼 이어지고, 실행이 끝나면 PC가 다음 줄을 가리킨 채 다시 읽기로 돌아간다.1 읽기PC 자리의 바이트2 해석무슨 명령인가3 실행계산·읽기·적기실행이 끝나면 PC는 다음 줄로다음 줄
CPU가 쉬지 않고 도는 고리. PC가 다음에 읽을 위치를 가리킵니다.

레지스터: CPU 안의 작은 저장 칸 쉽게

메모리는 크지만 CPU에서 멀리 있습니다. 그래서 CPU 안에는 지금 다루는 값을 잠깐 쥐고 있는 아주 작은 칸이 몇 개 있습니다. 이것이 레지스터입니다. 일꾼의 손이나 작업대 위 메모지라고 생각하면 됩니다. 슈퍼 패미컴의 CPU인 65c816은 손이 이렇게 생겼습니다.

65c816 레지스터16비트 레지스터 A, X, Y, S, PC, D와 8비트 레지스터 P, DB, PB. A는 위쪽 바이트 B와 아래쪽 바이트 A로 나뉜다. PB와 PC를 합치면 24비트 코드 주소가 된다.AB (위 8비트)A (아래 8비트)X16비트Y16비트S16비트PC16비트P8비트DB8비트PB8비트D16비트계산세기세기스택 꼭대기다음에 읽을 줄상태 표시데이터 뱅크코드 뱅크짧은 주소 기준
65c816의 레지스터. 칸 길이가 크기(8비트·16비트)를 나타냅니다.
  • A (16비트): 계산하는 손. 읽고, 더하고, 비교하고, 적는 일 대부분이 A를 거칩니다.
  • X (16비트): 세는 손. "표의 몇 번째"처럼 위치를 셀 때 씁니다(인덱스).
  • Y (16비트): X와 같은 일을 하는 두 번째 세는 손.
  • PC (16비트): 프로그램 카운터. 지금 읽을 줄을 가리키는 책갈피.
  • P (8비트): 상태 플래그 8개. "방금 결과가 0이었나" 같은 표시와 8/16비트 설정.

그림의 나머지 S·DB·PB·D는 이 장의 「더 깊이」와 5장에서 다룹니다.

레지스터 이름은 외울 필요가 없습니다. A로 계산하고, X·Y로 세고, PC가 다음 줄을 가리키고, P에 "방금 결과" 표시가 남는다는 것만 기억하면 5장의 어셈블리를 읽을 수 있습니다.

8비트와 16비트를 오가는 CPU 쉽게

65c816은 한 번에 1바이트(8비트)씩 다룰 수도, 2바이트(16비트)씩 다룰 수도 있습니다. 어느 쪽인지는 P 레지스터의 두 칸이 정합니다.

  • M 칸이 1이면 A가 8비트, 0이면 16비트입니다.
  • X 칸이 1이면 X·Y가 8비트, 0이면 16비트입니다.

이 칸을 끄는 명령이 REP, 켜는 명령이 SEP입니다. REP #$20은 "M 칸(값 $20)을 꺼라", 곧 "A를 16비트로"라는 뜻입니다. 중요한 점은 같은 명령 바이트라도 이 설정에 따라 뒤에 붙는 값의 길이가 달라진다는 것입니다. 아래 프로그램을 한 줄씩 실행해 보세요.

명령 한 줄씩 실행하기: 8비트와 16비트

같은 LDA라도 M 플래그에 따라 한 번에 1바이트를 다룰지 2바이트를 다룰지가 바뀝니다. 「한 줄 실행」을 누를 때마다 ▶ 표시가 붙은 명령 하나를 실행합니다.

시작 상태: 이미 네이티브 모드로 바꾼 뒤이고, A·X·Y는 8비트(M=1, X=1), DB와 PB는 $80, S는 $1FFF입니다. 전원을 켠 직후(에뮬레이션 모드, DB·PB·D가 모두 0)와는 다릅니다.

  1. 8000C2 20REP #$20P의 M 비트를 끕니다. 이제 A는 16비트입니다.
  2. 8002A9 34 12LDA #$123416비트 값 $1234를 A에 넣습니다. 피연산자가 2바이트(34 12)입니다.
  3. 80058D 00 0ASTA $0A00A를 $0A00에 적습니다. 16비트라 두 칸에 34, 12 순서로 들어갑니다.
  4. 8008E2 20SEP #$20M 비트를 다시 켭니다. A는 8비트로 돌아갑니다.
  5. 800AA9 56LDA #$56이번 LDA는 피연산자가 1바이트(56)뿐입니다. 위 바이트 $12는 B에 숨어 남습니다.
  6. 800C8D 02 0ASTA $0A028비트라 한 칸에만 56을 적습니다.
  7. 800FDBSTPSTP. 여기서 멈춥니다.

아직 아무 명령도 실행하지 않았습니다.

A
B $00 · A $00
X
$00
Y
$00
S
$1FFF
PC
$80:8000
DB
$80

P(상태 플래그) — 대문자·1이 켜진 비트

  1. n0
  2. v0
  3. M1
  4. X1
  5. d0
  6. I1
  7. z0
  8. c0

M=1: A 8비트 · X=1: X·Y 8비트

메모리(WRAM)

  • $7E:0A0000방금 바뀜
  • $7E:0A0100방금 바뀜
  • $7E:0A0200방금 바뀜

끝까지 실행한 뒤(명령 7개)

A
B $12 · A $56
X
$00
Y
$00
S
$1FFF
PC
$80:8010
DB
$80

M=1: A 8비트 · X=1: X·Y 8비트

  • $7E:0A00 34
  • $7E:0A01 12
  • $7E:0A02 56

LDA(A에 값 넣기)의 명령 바이트는 두 번 다 A9이지만, 처음에는 뒤에 2바이트(34 12), 두 번째에는 1바이트(56)가 붙습니다. 그래서 롬을 읽을 때는 "지금 M·X가 어떤 상태인지" 알아야 바이트를 제대로 끊을 수 있습니다. 엉뚱한 상태로 읽으면 한 바이트씩 어긋나 뒤의 명령이 모두 엉터리로 보입니다. 마지막에 A가 $1256인 것도 눈여겨보세요. 8비트 모드에서도 위 바이트(B)는 지워지지 않고 숨어 있습니다.

더 깊이65c816의 계보, 두 가지 모드, P 레지스터 전부

6502에서 65c816까지

65c816은 1970년대의 8비트 CPU 6502를 16비트로 넓힌 것입니다. Western Design Center(WDC)의 빌 멘시(Bill Mensch)가 설계했고 1985년에 나왔습니다. Apple IIGS와 슈퍼 패미컴이 이 CPU를 썼는데, 슈퍼 패미컴의 CPU 칩(Ricoh 5A22)은 65c816에 DMA 같은 장치를 더한 것입니다. 주소는 24비트라 16MB까지 가리킬 수 있습니다.

슈퍼 패미컴에서 CPU 속도는 어느 주소를 읽느냐에 따라 3.58MHz, 2.68MHz, 1.78MHz로 달라집니다. 롬은 보통 2.68MHz로 읽지만, 게임이 레지스터 $420D의 비트 0을 켜면 $80–$BF의 $8000–$FFFF와 $C0–$FF 뱅크를 3.58MHz로 읽습니다(FastROM). 롬 헤더의 "빠른 롬" 비트는 "이 카트리지는 빠르게 읽어도 된다"는 표시일 뿐, 속도를 바꾸지는 않습니다(4장).

에뮬레이션 모드와 네이티브 모드

65c816은 전원을 켜면 6502처럼 동작하는 에뮬레이션 모드로 시작합니다. 이 모드에서는 A·X·Y가 모두 8비트로 묶이고 스택은 $0100–$01FF에만 쌓입니다. 옛 6502 프로그램이 그대로 돌게 하려는 장치입니다. 16비트 기능을 쓰려면 CLC 다음 XCE를 실행해 네이티브 모드로 바꿉니다. 슈퍼 패미컴 게임은 거의 모두 시작하자마자 이렇게 합니다. 이 장과 5장의 「명령 한 줄씩 실행하기」는 네이티브 모드만 다룹니다.

P 레지스터의 여덟 칸

P 레지스터 비트비트 7부터 0까지 N 음수, V 넘침, M 누산기 8비트, X 인덱스 8비트, D 10진, I 인터럽트 막기, Z 0, C 올림이다.N7음수V6넘침M5A 8비트X4X·Y 8비트D310진I2IRQ 막기Z10C0올림
네이티브 모드의 P 레지스터. 왼쪽이 비트 7입니다.
  • N: 결과의 맨 위 비트(음수 표시). Z: 결과가 0이면 1.
  • C: 덧셈에서 올림이 났거나, 비교에서 "크거나 같음"이면 1. V: 부호 있는 덧셈이 넘쳤으면 1.
  • M·X: 위에서 본 8/16비트 설정. X를 1로 바꾸는 순간 X·Y의 위 바이트는 0으로 지워집니다. A의 위 바이트(B)는 M을 바꿔도 남아 있고, XBA로 A와 맞바꿀 수 있습니다.
  • D: 10진(BCD) 계산 모드. I: 켜면 IRQ 인터럽트를 받지 않습니다.
  • 에뮬레이션 모드에서는 비트 5가 쓰이지 않고 비트 4가 B(BRK 표시)입니다.

뱅크 레지스터와 다이렉트 페이지

명령에 적힌 주소는 대부분 16비트라서, 나머지 8비트(뱅크)는 레지스터가 채웁니다. 코드는 PB, 데이터는 DB를 씁니다. LDA $8012는 실제로 DB:$8012를 읽습니다. 짧은 주소 LDA $10은 다이렉트 페이지라고 하며 D + $10, 늘 뱅크 $00을 읽습니다. 1바이트 짧아 자주 쓰는 변수에 씁니다. 리셋 때 DB·PB·D는 모두 0입니다.

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

전원을 켜면 어디부터 실행하나

돌격편 롬의 파일 0x7FFC에는 00 80, 곧 $8000이 있습니다. 이 자리를 리셋 벡터라고 하며, CPU는 전원을 켜면 에뮬레이션 모드로 $00:8000부터 읽기 시작합니다. 같은 헤더의 0x7FEA에 있는 $818D는 NMI 루틴의 주소입니다. NMI는 화면을 한 번 다 그린 직후마다 불리는데, 게임이 레지스터 $4200의 비트 7을 켜 두었을 때만 옵니다.

CPU가 둘인 카트리지

슈퍼 마리오 RPG 카트리지에는 SA-1이라는 CPU가 하나 더 있습니다. 본체 CPU보다 세 배쯤 빠른(10.74MHz) 65c816이라 같은 어셈블리로 짭니다. 두 CPU는 동시에 돌고, 이 게임은 필드 메뉴를 본체 CPU에, 전투 메뉴를 SA-1에 맡기도록 만들어졌습니다. 그래서 아이템 이름 하나를 한글로 바꾸려 해도 두 길을 따로 고쳐야 했습니다.

직접 해 보기 직접 해 보기

툴킷의 역어셈블러는 롬 바이트를 위 「명령 한 줄씩 실행하기」의 목록처럼 명령으로 풀어 줍니다. REP·SEP를 만나면 폭을 바꿔 가며 읽고, 중간부터 읽을 때는 시작 폭을 직접 알려 줍니다.

krpatch snes dis game.sfc 808000 40            # $80:8000부터 명령 40개
krpatch snes dis game.sfc 808000 --m16 --x16   # A·X/Y를 16비트로 놓고 시작

같은 자리를 --m16 없이 한 번, 붙여서 한 번 읽어 보세요. 어느 쪽이 말이 되는지 비교하는 것이 실제 분석에서 폭을 알아내는 방법입니다.