Let's take a look at the code! A few seconds of Googling led me to a high-quality annotated disassembly by Kent Hansen and Nick Mikstas: https://www.metroid-database.com/source-code/
Whenever the user is on the password screen, the following routine runs every frame:
EnterPassword:
L9147: JSR EraseAllSprites ;($C1A3)Remove sprites from screen.
L914A: LDA Joy1Change ;
L914C: AND #$10 ;Check to see if START has been pressed.
L914E: BEQ + ;If not, branch.
L9150: JMP CheckPassword ;($8C5E)Check if password is correct.
... (draw the password menu and handle user input)
If the user pressses the start button, we jump to the CheckPassword
routine:
CheckPassword:
L8C5E: JSR ConsolidatePassword ;($8F60)Convert password characters to password bytes.
L8C61: JSR ValidatePassword ;($8DDE)Verify password is correct.
L8C64: BCS + ;Branch if incorrect password.
L8C66: JMP InitializeGame ;($92D4)Preliminary housekeeping before game starts.
... (handle incorrect password)
ConsolidatePassword
converts the password from characters into bytes, and stores it in memory at address $6988
. The password in question is decoded into the byte sequence 39 74 0A 40 E6 D2 35 53 A2 59 87 51 39 B3 DE 31 43 9B
. The last two bytes are special: the 43
indicates that the password should be deobfuscated by rotating it to the right by 0x43 bits, and the 9B
is a checksum. ValidatePassword
performs the rotation, and computes the checksum. The password then becomes the sequence 12 CC 3A 89 CD 9E F1 89 CB A0 52 07 36 91 AA 9D 43
, which (by pure coincidence) happens to add up to 0x9B, meaning the checksum matches and the game treats it as a valid password rather than displaying the error screen.
We then jump to the InitializeGame
routine:
InitializeGame:
L92D4: JSR ClearRAM_33_DF ;($C1D4)Clear RAM.
L92D7: JSR ClearSamusStats ;($C578)Reset Samus stats for a new game.
L92DA: JSR LoadPasswordData ;($8D12)Load data from password.
...
L931A: LDA InArea ;Load area Samus is to start in.
L931C: AND #$0F ;
L931E: TAY ;
L931F: LDA BankTable,Y ;Change to proper memory page.
L9322: STA SwitchPending ;
L9324: RTS
LoadPasswordData
initializes game variables using the password bytes:
LoadPasswordData:
L8D12: LDA NARPASSWORD ;If invincible Samus active, skip-->
L8D15: BNE +++ ;further password processing.
L8D17: JSR LoadUniqueItems ;($8BD4)Load unique items from password.
L8D1A: JSR LoadTanksAndMissiles ;($8D3D)Calculate number of missiles from password.
L8D1D: LDY #$00 ;
L8D1F: LDA PasswordByte08 ;If MSB in PasswordByte08 is set,-->
L8D22: AND #$80 ;Samus is not wearing her suit.
L8D24: BEQ + ;
L8D26: INY ;
L8D27:* STY JustInBailey ;
L8D2A: LDA PasswordByte08 ;Extract first 5 bits from PasswordByte08-->
L8D2D: AND #$3F ;and use it to determine starting area.
L8D2F: STA InArea ;
L8D31: LDY #$03 ;
L8D33:* LDA PasswordByte0B,Y ;Load Samus' age.
L8D36: STA SamusAge,Y ;
L8D39: DEY ;
L8D3A: BPL - ;Loop to load all 3 age bytes.
L8D3C:* RTS ;
Since this isn't actually a "real", meaningful password, this routine just ends up loading a bunch of nonsensical, meaningless values. The one that actually causes the game to crash is the InArea
, which comes from the low 5 bits of byte 8 of the password, or 0xB.
We return to InitializeGame
, which eventually looks up the area number in a table to determine which memory bank to load. Here's the table:
LCA30: .byte $02 ;Brinstar.
LCA31: .byte $03 ;Norfair.
LCA32: .byte $05 ;Kraid hideout.
LCA33: .byte $04 ;Tourian.
LCA34: .byte $06 ;Ridley hideout.
But note that this table has only 5 entries! An area of 0xB (decimal 11) causes us to read well past the end of the table and into the next routine. The byte we read happens to be 0x84, which is not a valid bank number.
A while later, we perform the actual bankswitch:
SwitchOK:
LC4E8: lda #$00 ;Reset(so that the bank switch won't be performed-->
LC4EA: sta SwitchPending ;every succeeding frame too).
LC4EC: dey ;Y now contains the bank to switch to.
LC4ED: sty CurrentBank ;
ROMSwitch:
LC4EF: tya ;
LC4F0: sta $00 ;Bank to switch to is stored at location $00.
LC4F2: lda SwitchUpperBits ;Load upper two bits for Reg 3 (they should always be 0).
LC4F4: and #$18 ;Extract bits 3 and 4 and add them to the current-->
LC4F6: ora $00 ;bank to switch to.
LC4F8: sta SwitchUpperBits ;Store any new bits set in 3 or 4(there should be none).
;Loads the lower memory page with the bank specified in A.
MMCWriteReg3:
LC4FA: sta MMC1Reg3 ;Write bit 0 of ROM bank #.
LC4FD: lsr ;
LC4FE: sta MMC1Reg3 ;Write bit 1 of ROM bank #.
LC501: lsr ;
LC502: sta MMC1Reg3 ;Write bit 2 of ROM bank #.
LC505: lsr ;
LC506: sta MMC1Reg3 ;Write bit 3 of ROM bank #.
LC509: lsr ;
LC50A: sta MMC1Reg3 ;Write bit 4 of ROM bank #.
LC50D: lda $00 ;Restore A with current bank number before exiting.
LC50F:* rts ;
The MMC1 was a slightly unusual mapper, in that it is accessed via a serial interface which accepts one bit at a time. See the NESDev Wiki's documentation:
Unlike almost all other mappers, the MMC1 is configured through a serial port in order to reduce pin count. CPU $8000-$FFFF is connected to a common shift register. Writing a value with bit 7 set ($80 through $FF) to any address in $8000-$FFFF clears the shift register to its initial state. To change a register's value, the CPU writes five times with bit 7 clear and a bit of the desired value in bit 0. On the first four writes, the MMC1 shifts bit 0 into a shift register. On the fifth write, the MMC1 copies bit 0 and the shift register contents into an internal register selected by bits 14 and 13 of the address, and then it clears the shift register.
In other words: each time we write to an MMC register, bit 7 indicates whether we want to clear the shift register or write to it, and if we want to write then bit 0 indicates the bit we want to write. Bits 1-6 are ignored
However, when we try to switch banks to a value that's way out of range (0x83, because of the dey
instruction), the first write has bit 7 set and actually resets the shift register rather than writing. The remaining 4 writes write to the shift register but do not trigger a bankswitch, since the MMC1 is waiting for a 5th write.
Later on, in the NMI handler, the game attempts to configure the PPU's nametable layout:
SetPPUMirror:
LC4B6: lsr ;
LC4B7: lsr ;Move bit 3 to bit 0 position.
LC4B8: lsr ;
LC4B9: and #$01 ;Remove all other bits.
LC4BB: sta $00 ;Store at address $00.
LC4BD: lda MMCReg0Cntrl ;
LC4BF: and #$FE ;Load MMCReg0Cntrl and remove bit 0.
LC4C1: ora $00 ;Replace bit 0 with stored bit at $00.
LC4C3: sta MMCReg0Cntrl ;
LC4C5: sta MMC1Reg0 ;
LC4C8: lsr ;
LC4C9: sta MMC1Reg0 ;
LC4Cc: lsr ;
LC4CD: sta MMC1Reg0 ;
LC4D0: lsr ;Load new configuration data serially-->
LC4D1: sta MMC1Reg0 ;into MMC1Reg0.
LC4D4: lsr ;
LC4D5: sta MMC1Reg0 ;
LC4D8: rts ;
This routine attempts to update the MMC1 control register, which is documented on the NESDev wiki:
4bit0
-----
CPPMM
|||||
|||++- Mirroring (0: one-screen, lower bank; 1: one-screen, upper bank;
||| 2: vertical; 3: horizontal)
|++--- PRG ROM bank mode (0, 1: switch 32 KB at $8000, ignoring low bit of bank number;
| 2: fix first bank at $8000 and switch 16 KB bank at $C000;
| 3: fix last bank at $C000 and switch 16 KB bank at $8000)
+----- CHR ROM bank mode (0: switch 8 KB at a time; 1: switch two separate 4 KB banks)
However, the shift register still contains 4 leftover bits from the failed bankswitch; it's contents (in binary) are x0001
. The first write to the MMC1 register fills in the 5th bit (the x
) and causes all 5 bits to immediately be written to the control register. This causes the MMC1 to suddenly switch into 32-KiB bank mode.
Normally, the game runs with the MMC1 in 16-KiB mode, mapping area-specific routines and data (in this case, for the title screen) to addresses $8000-BFFF
and the game engine to $C000-$FFFF
. However, when the MMC1 is switched into 32-KiB mode, the game engine is suddenly replaced with the area data for Brinstar. The address we are currently executing, $C4C8
, is suddenly replaced with what appears to be unrelated enemy AI routines:
L84C2: LDY EnXRoomPos,X
L84C5: BNE $84DA
L84C7: LDA $49
L84C9: CMP #$02
L84CB: BCC $84DA
L84CD: LDA $FD
L84CF: BEQ $84D4
We've ended up misaligned in the instruction stream: note that one instruction starts at address C4C7
, and the next starts at C4C9
; but we're trying to execute the instruction starting at C4C8
. The processor ends up interpreting the LDA operand (memory address $49
) as an opcode, which happens to correspond to the instruction EOR #i
. This doesn't do anything interesting, but it means that we're still misaligned and now attempting to execute address C4CA
(the #$02
operand to the CMP
). Opcode 02 is invalid; if executed, it just causes the CPU to freeze until the console is reset.
There's no way this crash could possibly cause any permanent damage to the cartridge or console. The 02
opcode itself could not cause any harm; it just locks up the CPU until the user presses the reset button or cycles the power. Besides, the Metroid NES cartridge included no persistent storage so there's no way to "brick" it. There were (to my knowledge) no other "versions" of Metroid released on the NES, so it's not possible that I investigated the wrong version.
As far as emulators: an accurate emulator should behave identically to the hardware, so the password will cause the game to crash on an emulator just as it would on hardware. But it's virtually impossible for the crash to actually have any effect on the system running; it would take an extraordinarily badly-written emulator to actually cause harm to the system it's running on if it encounters an invalid opcode.
rm -rf /
is included in that.) Basically, any change to persistent storage has the potential to brick things if the boot ROM doesn't provide a means to recover.