Contra (J) NSF Dump
进入声音模式
使用FCEUX模拟器打开ROM, 标题画面下按 A + B + Start进入声音模式
在此界面, B键播放, A键停止.

声音入口查找步骤
- 先添加声音寄存器 4000 写断点
- 单击 Run 等待断点触发(如果此时程序处于暂停状态)
- 发生断点暂停后, 如果执行地址不在 C 000 − C000- C000−FFFF, 则单击 Step Out
- 否则添加断点记录上一条JSR指令的跳转地址的执行断点, 并禁用添加的新断点
- 重复执行步骤1
开始查找
先确保声音没有播放(如果在播放则按A键停止)
打开调试器

添加声音寄存器 4000 的写入断点


单击激活模拟器主窗口, 然后按B键播放声音
调试器停在
>01:8736: 8D 00 40 STA $4000 = #$30

这里地址是8736, 按 Step Out (跳出), 直到执行地址在C000-FFFF为止, 调试器停在
>07:F9DE: 20 AF F9 JSR $F9AF

这里地址是F9DE, 上一行是
07:F9DB: 20 19 87 JSR $8719
添加断点记录8719的执行断点



双击刚刚添加的新断点进行禁用(禁用后断点前面没有字母E)

单击运行
断点停留在
>01:88B5: 9D 00 40 STA $4000,X @ $4000 = #$30

这里地址是88B5, 按 Step Out (跳出), 直到执行地址在C000-FFFF为止, 调试器停在
>07:F9CA: 20 AF F9 JSR $F9AF

这里地址是F9CA, 上一行是
07:F9C7: 20 A2 87 JSR $87A2
添加断点记录87A2的执行断点



双击刚刚添加的新断点进行禁用(禁用后断点前面没有字母E)

单击运行
断点停留在
>01:8189: 9D 00 40 STA $4000,X

这里地址是8189, 按 Step Out (跳出), 直到执行地址在C000-FFFF为止, 调试器停在
>07:F8C6: 20 BD C1 JSR $C1BD

这里地址是F8C6, 上一行是
07:F8C3: 20 D5 80 JSR $80D5
添加断点记录80D5的执行断点



双击刚刚添加的新断点进行禁用(禁用后断点前面没有字母E)

后续再重复执行声音入口查找步骤, 得到的依旧是80D5, 重复的就不再进行断点记录添加
提取数据
单击执行, 此时模拟器暂停

此时执行的程序位于第01个16KB数据块中
使用 Hxd 打开 Contra (J) [!].nes, 按 Ctrl + G, 跳转到十六进制地址 01 * 4000 + 10 = 4010 地址


按 Ctrl + E, 选择 4010 起始的十六进制的4000字节数据

单击文件 -> 选取另存为…

文件名暂且叫做
Contra (J) [!] 8000_BFFF.bin

提取DMC数据
添加声音寄存器 4012 的写入断点

只启用4012 的写入断点

单击运行, 调试器暂停

可以看到, 向声音寄存器 4012 写入的数据是在8915,Y
打开模拟器的 Debug -> Hex Editor…

按 Ctrl + G 输入 8915, 单击 OK, 跳转到指定位置


这里的数据, 分析调试器中的程序
01:88F3: B9 13 89 LDA $8913,Y @ $8917 = #$0F
01:88F6: 8D 10 40 STA $4010 = #$0F
01:88F9: B9 14 89 LDA $8914,Y @ $8918 = #$75
01:88FC: 8D 11 40 STA $4011 = #$75
01:88FF: B9 15 89 LDA $8915,Y @ $8919 = #$F3
>01:8902: 8D 12 40 STA $4012 = #$F0
01:8905: B9 16 89 LDA $8916,Y @ $891A = #$25
01:8908: 8D 13 40 STA $4013 = #$05
可以知道, 从 8913开始, 每4字节一组, 分别写入声音寄存器的4010-4013
观察了一下, 从8913开始的16个字节应该是有规律的4组数据
0F 2F F0 05
0F 75 F3 25
0F 7F F3 03
0F 00 F3 25
关于DMC数据地址与长度可以参考 APU DMC - NESdev Wiki
于是可以得到4段DMC数据地址与长度
F0 05 -> F0 * 40 + C000 = FC00, 长度 0x50 字节
F3 25 -> F3 * 40 + C000 = FCC0, 长度 0x0250 字节
F3 03 -> F3 * 40 + C000 = FCC0, 长度 0x30 字节
F3 25 -> F3 * 40 + C000 = FCC0, 长度 0x0250 字节
也就是FC00开始的数据基本都是DMC数据
按 Ctrl + G 输入 FC00, 单击 OK, 跳转到指定位置

选中 FC00-FFFF 范围内数据

然后 单击 Edit -> Copy 拷贝一下

然后在Hxd中按 Ctrl + N 新建空白文件

然后 按 Ctrl + V 粘贴数据

然后Ctrl + S 保存, 文件名暂且叫做
Contra (J) [!] .dmc


区分播放入口与初始化入口
从上面操作中已经得到3个相关声音入口: 8719, 87A2, 80D5
那么哪个是播放入口, 哪个初始化入口呢?
思路如下
- 如果声音没有播放时, 都会执行的, 那么一般就是播放入口
- 如果从静音到播放时才会被执行一次的入口, 一般就是初始化入口, 而且播放不同曲目时, 执行入口时寄存器A的值是不同的
找出播放入口
按A键停止音乐播放, 然后在调试器中仅启用3个入口的断点,
结果80D5断点被执行了, 那么可以确定这个入口就是播放入口了

修改下断点描述



找出初始化入口
还剩下2个相关声音入口: 8719, 87A2
先禁用全部断点

然后单击 Run 继续运行

仅启用8719, 87A2的执行断点

然后按B键播放曲目, 执行8719时, 调试器暂停, 此时寄存器A = 80

单击 Run 继续运行, 执行87A2时, 调试器暂停, 此时寄存器A = 46

然后禁用全部断点

单击 Run 继续运行

然后单击模拟器主窗口, 先按 A键 停止播放, 再按下键切换到不同曲目

在调试器中仅启用8719, 87A2的执行断点

然后模拟器主窗口中按 B键 播放曲目, 执行8719时, 调试器暂停, 此时寄存器A = 80

单击 Run 继续运行, 执行87A2时, 调试器暂停, 此时寄存器A = 5A

这时候可以确定初始化入口是87A2
修改下断点描述



提取曲目号
禁用所有断点, 单击 Run 继续执行

在模拟器窗口中按 A键 停止播放
单击 Debug -> Trace Logger 打开执行日志记录窗口


单击 Start 开始记录

在调试器窗口仅启用87A2初始化断点

在模拟器窗口按 B键 播放曲目, 调试器暂停, 此时A = 5A

在日志记录器中查找到 5A 这个曲目号来自于这里
$04:A7F6: B9 2A AA LDA $AA2A,Y @ $AA2B = #$5A

于是知道了这个 5A 来源后, 可以在调试器中添加一个读取断点

断点地址: A7F6, 条件: $AA2B == #5A


然后仅启用这个断点, 再单击运行继续执行

然后在模拟器中按B键再次播放曲目

打开模拟器 Debug -> Hex Editor
按 Ctrl + G, 输入 AA2A, 回车跳转到指定地址


这里的46就是最开始的曲目号
调试器上单击 Run 继续执行

然后再模拟器中将光标移动到最后一个曲目后按 B键 播放
调试器暂停, 得到最后一个曲目号为 72

回到 Hex Editor, 选中 起始曲目号 46 到终止曲目号 72 在内的数据

按 Ctrl + C 拷贝
然后在Hxd中, 按Ctrl + N 新建空文件, 再按 Ctrl + V 粘贴

然后Ctrl + S 保存, 文件名暂且叫做
Contra (J) [!] track.bin


生成NSF文件
新建文本文件 Contra (J) [!].asm

内容写上, 然后保存
;Contra (J) NSF
;Dump By FlameCyclone
;2025.8.4
.INSFVERSION 1 ;版本
.INSFTOTALSONGS 43 ;总曲目 Contra (J) [!] track.bin 43字节
.INSFSTARTSONG 1 ;起始曲目
.INSFLOAD $8000 ;数据载入地址
.INSFINIT Init_Addr ;初始化地址
.INSFPLAY $80D5 ;播放地址
.INSFNAME "Contra" ;专辑名
.INSFARTIST "H. Maezawa, K. Sada" ;作者
.INSFCOPYRIGHT "1988 Konami" ;版权
.INSFSPEEDNTSC 16666 ;NTSC播放速度
.INSFBANKSWITCH 0,1,2,3,4,5,6,7 ;4KB数据块布局
.INSFSPEEDPAL 16666 ;PAL播放速度
.INSFSTANDARD 0 ;制式标准
.INSFEXTRASOUND 0 ;扩展音源
;音乐数据
.BANK16 0
.ORG $8000
.INCBIN "Contra (J) [!] 8000_BFFF.bin"
;DMC数据
.BANK16 1
.ORG $C000
.INCBIN "Contra (J) [!] .dmc"
;初始化处理
Init_Addr
TAX
LDA Track_Data,X
JMP $87A2;跳转到初始化入口
;曲目号数据
Track_Data
.INCBIN "Contra (J) [!] track.bin"
打开 nesasm_Gui_3_2, 将刚刚保存的文件 “Contra (J) [!].asm” 拖到输入到nesasm_Gui_3_2窗口

单击编译生成nsf文件


模拟器打开试听, 发现DMC怪怪的

这是因为原本DMC数据地址是在FC00, 我这里是放在C000
那么需要将此前写入4012的F0和F3修改才行
F0和F3对应FC00和FCC0,
那么改成00和03对于C000和C0C0即可
结合此前写入声音寄存器4012的数据

可以得到
8915 F0
8919 F3
891D F3
8921 F3
于是在 “Contra (J) [!].asm” 中进行简单修改即可, 最后源码改成如下
;Contra (J) NSF
;Dump By FlameCyclone
;2025.8.4
.INSFVERSION 1 ;版本
.INSFTOTALSONGS 43 ;总曲目 Contra (J) [!] track.bin 43字节
.INSFSTARTSONG 1 ;起始曲目
.INSFLOAD $8000 ;数据载入地址
.INSFINIT Init_Addr ;初始化地址
.INSFPLAY $80D5 ;播放地址
.INSFNAME "Contra" ;专辑名
.INSFARTIST "H. Maezawa, K. Sada" ;作者
.INSFCOPYRIGHT "1988 Konami" ;版权
.INSFSPEEDNTSC 16666 ;NTSC播放速度
.INSFBANKSWITCH 0,1,2,3,4,5,6,7 ;4KB数据块布局
.INSFSPEEDPAL 16666 ;PAL播放速度
.INSFSTANDARD 0 ;制式标准
.INSFEXTRASOUND 0 ;扩展音源
;音乐数据
.BANK16 0
.ORG $8000
.INCBIN "Contra (J) [!] 8000_BFFF.bin"
;修正DMC起始地址
.BANK16 0
.ORG $8915
.DB $F0 - (($FC00 - $C000) / $40)
.ORG $8919
.DB $F3 - (($FC00 - $C000) / $40)
.ORG $891D
.DB $F3 - (($FC00 - $C000) / $40)
.ORG $8921
.DB $F3 - (($FC00 - $C000) / $40)
;DMC数据
.BANK16 1
.ORG $C000
.INCBIN "Contra (J) [!] .dmc"
;初始化处理
Init_Addr
TAX
LDA Track_Data,X
JMP $87A2;跳转到初始化入口
;曲目号数据
Track_Data
.INCBIN "Contra (J) [!] track.bin"

保存源码后再次编译

再次使用模拟器打开, DMC数据也正常了, NSF提取就算成功了.

1858

被折叠的 条评论
为什么被折叠?



