大家好

尽管我在Sega Mega Drive
倒车游戏方面拥有丰富的经验,但我从未决定要破解它,并且它们并没有在Internet上出现。 但是,前几天有一个有趣的饼干想解决。 我同意你的决定...
内容描述
任务说明和朗姆酒本身可以在此处下载 。
尽管资源清单上有Hydra的事实,但Sega Ida Tools是Sega上用于调试和反转游戏的工具中的标准事实。 它具有解决此奶油所需的一切:
- 朗达朗姆酒装载机
- 调试器
- 查看和更改RAM / VDP内存
- 在VDP上显示几乎完整的信息
我们将最新版本放到Ide的插件中,然后开始看看我们拥有什么。
解决方案
任何Shogi游戏的发布都始于执行Reset
向量。 从朗姆酒开头的第二个DWORD中可以找到指向它的指针。


我们看到了一些从地址0x27A
开始的身份不明的函数。 让我们看看那里有什么。
sub_2EA()

根据我自己的经验,我会说这通常看起来像等待VBLANK
中断完成的功能。 让我们看看在哪里还有对byte_FF0026
变量的调用:

我们看到零位只是在VBLANK
中断中设置的。 因此,我们将变量vblank_ready
,检查该变量的函数为wait_for_vblank
。
sub_60E()
接下来,通过代码调用sub_60E
函数。 让我们看看那里是什么:

第一条命令写入VDP_CTRL
就是VDP
控制命令。 要了解她在做什么,我们站在此命令上并按J
键:

我们看到CRAM
(存储调色板的位置)中的条目已初始化。 这意味着所有后续功能代码都仅设置了某种初始调色板。 因此,该函数可以称为init_cram
。
sub_71A()

我们看到一些命令再次传输到VDP_CTRL
,然后再次按J
并发现此命令初始化了视频内存中的记录:

此外,要了解在那里传输到视频存储器的内容没有任何意义。 因此,我们只需调用函数load_vdp_data
。
sub_C60()
这里几乎发生与上一个函数相同的事情,因此,在不赘述的情况下,我们仅调用load_vdp_data2
函数。
sub_8DA()
已经有更多的代码。 此外,该函数还调用了另一个函数。 让我们看一下-在sub_D08
。
sub_D08()

我们看到在寄存器D0
了VDP_CTRL
D0
命令,在D1
VDP_CTRL
将填充的值,在D2
和D3
-填充的宽度和高度(因为结果是两个周期:内部和外部)。 调用fill_vram_by_addr函数。
sub_8DA()
我们返回上一个功能。 将D0
寄存器中的值作为VDP_CTRL
的命令发送VDP_CTRL
,请按该值上的J
键。 我们得到:

再次,从将游戏转换为Sega的经验来看,我可以说此命令初始化了映射图块的记录。 在90%的情况下, $Dxxx
$Fxxx
, $Exxx
, $Dxxx
, $Dxxx
地址将是具有这些相同映射的区域的地址。 什么是映射:
这些是您可以指定在屏幕上的何处显示或该图块的值(图块是8x8
像素的正方形)。
因此该函数可以称为init_tile_mappings
。
sub_CDC()

第一条命令在地址$F000
处初始化记录。 一个注意事项:在“ 映射 ”的地址中,仍然存在一个存储精灵表的区域(这些区域是它们的位置,它们指向的图块等)。找出哪个区域负责可调试的内容。 但是现在,我们不需要此功能,因此我们只需调用函数init_other_mappings
。
此外,我们看到在此函数中初始化了两个变量: word_FF000A
和word_FF000C
。 根据我的经验(是的,他决定),我会说,如果在地址空间中有两个变量与映射相关联,那么在大多数情况下,它们将是某个对象(例如,精灵)的坐标。 因此,我建议称它们为sprite_pos_x
和sprite_pos_y
。 x
和y
的误差y
允许的,因为 进一步调试,将很容易修复。
VBLANK
由于循环在代码中进行得更远,因此我们可以假设我们已经完成了基本初始化。 现在您可以查看VBLANK
中断。

我们看到两个变量正在递增(这很奇怪,在每个变量的链接列表中,它绝对是空的)。 但是,由于它们每帧更新一次,因此可以将它们称为timer1
和timer2
。
接下来, sub_2FE
函数。 让我们看看那里是什么:
sub_2FE()

在那里-使用IO_CT1_DATA
端口(负责第一个操纵杆)。 端口地址被加载到寄存器A0
,并传递给sub_310
函数。 我们去那里:
sub_310()

我的经验再次帮助了我。 如果您看到适用于操纵杆的代码以及内存中的两个变量,则其中一个会存储pressed keys
,第二个会存储held keys
,即 只需按住键即可。 因此,我们将这些变量称为: pressed_keys
和held_keys
。 然后该函数可以称为update_joypad_state
。
sub_2FE()
将该函数称为read_joypad
。
处理程序循环
现在,一切看起来都更加清晰了:

因此,此循环响应所按下的键,并执行相应的操作。 让我们遍历循环中调用的每个函数。
sub_4D4()

有很多代码。 让我们从第一个函数sub_60C
。
sub_60C()
她什么也没做-乍看起来似乎是这样。 只是从当前函数返回的是rts
。 但是,因为 仅在其上发生跳转( bsr
),这意味着rts
将使我们返回到处理程序循环。 我将此函数称为retn_to_loop
。
sub_4D4()
接下来,我们看到对word_FF000E
变量的调用。 除了当前功能以外,没有在其他任何地方使用它,起初,我的目的并不明确。 但是,如果您仔细观察,我们可以假设仅在按键处理之间有很小的延迟才需要此变量。 ( 在本次朗姆酒中,它的实施效果很差,但是,我认为,如果没有此变量,情况将会更糟 )。

接下来,我们有大量的代码以某种方式处理sprite_pos_x
和sprite_pos_y
,这些sprite_pos_y
只能说一句话-这对于在字母中所选字符周围显示选择精灵是必须的。
因此,现在您可以安全地将函数命名为update_selection
。 让我们继续前进。

该代码检查某些按键的位是否已设置,并调用某些功能。 让我们看看它们。
sub_D28()

某种萨满魔术。 首先,从word_FF0018
变量中提取WORD
,然后执行一条有趣的指令:
bsr.w *+4
该命令只是跳转到其后的指令。
接下来是另一个魔术:
move.l d0,(sp) rts
寄存器D0
的值位于堆栈的顶部。 值得注意的是,对于Shogi以及某些x86
,函数调用时的返回地址都放在堆栈的顶部。 因此,第一个指令将某个地址放在顶部,第二个指令将其从堆栈中移出并沿其过渡。 好招 。
现在,您需要了解此值在变量中的含义,然后进行检查。 但是首先,我们将其称为jmp_addr
变量。
这些函数将称为:
sub_D38
: goto_to_d0
sub_D28
: jump_to_var_addr
jmp_addr
找出该变量的填充位置。 我们看一下参考文献清单:

只有一个地方可以写入此变量。 让我们看看他。
sub_3A4()

在此,根据子画面的坐标(请记住,这很可能是所选字符的地址),将输入该值或该值。 我们看到以下代码部分:

现有值向右移动4位,新值放入低字节,然后将结果再次输入到变量中。 从理论上讲,我们的jmp_addr
变量存储可以在键输入屏幕上输入的字符。 另请注意,变量的大小为WORD
。
实际上, sub_3A4
函数可以称为update_jmp_addr
。
sub_414()
现在,循环中只剩下一个函数,该函数无法识别。 它被称为sub_414
。

它的代码类似于update_jmp_addr
函数的代码,仅在最后我们有一个sub_45E
函数sub_45E
。 让我们看看那里。
sub_45E()

我们看到在D0
寄存器中输入了数字#$4B1E2003
,然后将其发送到VDP_CTRL
,这意味着我们正在处理另一个VDP
控制命令。 我们按J
,我们会收到一个映射$Cxxx
区域中的记录命令。
接下来,代码使用变量byte_FF0014
,除了当前函数外,该变量未在其他任何地方使用。 如果仔细看一下它的用法,您会注意到它可以安装的最大数量为4
。 我假设这是输入密钥的当前长度。 让我们来看看。
运行调试器
我将使用Smd Ida Tools
的调试器,但实际上,某些Gens KMod或Gens ReRecording就足够了。 最主要的是,有一个功能可以在内存中显示地址。

我的理论已经得到证实。 因此,变量byte_FF0014
现在可以key_length
。
还有另一个变量: dword_FF0010
,它也仅在当前函数中使用,并且其内容在添加到D0
的初始命令(回想起来,它是编号#$4B1E2003
)之后,被发送到VDP_CTRL
。 我add_to_vdp_cmd
,将其命名为变量add_to_vdp_cmd
。
那么这个功能做什么呢? 我假设她会绘制输入的字符。 检查这很简单-通过启动调试器并在调用sub_45E
函数之前和之后比较状态:
至:

之后:

我是对的-此函数绘制输入的字符。 我们称它为do_draw_input_char
,调用它的函数( sub_414
)为draw_input_char
。
现在呢
现在让我们检查一下我们称为jmp_addr
的变量jmp_addr
确实存储了输入的密钥。 我们将使用相同的Memory Watch
:

如您所见,这个猜想是正确的。 这给了我们什么? 我们可以跳转到任何地址。 但是哪一个呢? 在功能列表中,所有内容归根到底是:

然后,我开始滚动浏览代码,直到找到:

训练有素的眼睛看到了$4E, $75
的序列$4E, $75
在未分配字节的末尾。 这是rts
指令的操作码,即 从函数返回。 因此,这些未分配的字节可以是某些功能的代码。 让我们尝试将它们指定为代码,按C
:

显然,这是一个功能代码。 您也可以按其上的P
以使代码起作用。 记住这个名字: sub_D3C
。
然后想到了:如果您跳到sub_D3C
怎么sub_D3C
? 听起来不错,尽管在这里单跳显然是不够的,因为 没有更多到word_FF0020
变量的链接。
然后另一个想法浮现在我头:如果我们寻找另一个这样的未分配代码怎么办? 打开Binary search
对话框(Alt + B),在其中输入序列4E 75
,选中“ Find all occurrences
”框:

单击
开始搜索,我们得到以下结果。

朗姆酒中至少还有两个地方可能包含功能代码,您需要检查它们。 我们单击第一个选项,向上滚动一点,然后再次看到一系列未定义的字节。 将它们表示为功能吗? 是的 在字节开始处打P
:

好酷! 现在我们有了sub_34C
函数。 我们尝试对找到的最后一个选项重复同样的事情,并且...我们感到非常遗憾。 4E 75
之前有太多字节,因此不清楚函数从何处开始。 而且,显然,并非以上所有这些字节都是代码,因为 很多重复的字节。
确定函数的开始
如果我们找到数据的结束位置,那么最容易找到函数的开始。 怎么做? 实际上一点也不复杂:
- 我们在数据开始之前进行扭曲(代码中将存在指向它们的链接)
- 我们点击链接并寻找一个循环,在该循环中应显示此数据的大小
- 标记阵列
因此,我们执行第一段...:

...,我们立即看到,在一个循环中,从数组中一次将4个字节的数据(由于move.l
)复制到了VDP_DATA
。 接下来,我们看到数字2047
。 起初,似乎数组的最终大小为2047 * 4
,但是基于dbf
的循环执行的次数更多,而+1
迭代 最后比较的值不是0
,而是-1
。
总计:数组的大小为2048 * 4 = 8192
。 将字节表示为数组。 为此,请单击*
并指定大小:

我们扭曲到数组的末尾,然后看到字节,它们恰好是代码的字节:


现在我们有了sub_2D86
函数,并且我们拥有解决此sub_2D86
一切! 让我们看看新创建的函数的作用。
sub_2D86()
它将值#$4147
放入寄存器D1
并调用sub_34C
函数。 看看她。
sub_34C()

我们看到这里word_FF0020
变量的值被word_FF0020
。 如果查看它的链接,我们将看到在此变量中进行记录的另一个地方,而这正是我要跳过jmp_addr
变量的地方。 这确认了sub_D3C
肯定需要跳转到sub_D3C
。
但是接下来发生的事情让我无法理解,因此我把朗姆酒扔进了GHIDRA ,找到了这个功能,然后看了反编译的代码:
void FUN_0000034c(void) { ushort in_D1w; short sVar1; ushort *puVar2; if (((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(in_D1w ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,in_D1w ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
我们看到使用了具有奇怪名称in_D1w
的变量,还使用了变量DAT_00ff0020
,其地址类似于word_FF0020
提到的word_FF0020
。
in_D1w
告诉我们,该值是从寄存器D1
或in_D1w
的WORD一半中获取的,并将寄存器D1
设置为传递该值D1
函数。 还记得#$4147
吗? 因此,您需要将此寄存器指定为函数的输入参数。
为此,在带有反编译代码的窗口中,右键单击函数名称,然后选择“ Edit Function Signature
菜单项:

为了指示该函数通过特定的寄存器(而不是通过当前调用约定的标准方法)接受参数,您需要选中Use Custom Storage
,然后单击带有绿色加号的图标:

出现新输入参数的位置。 双击它,将出现一个对话框,指示参数的类型和媒介:

在反编译的代码中,我们看到in_D1w
是ushort
类型,这意味着我们将在type字段中指定它。 然后单击Add
按钮:

将出现一个位置来指示参数的D1w
Location
,我们需要在Location
指定D1w
寄存器,然后单击OK
:

反编译的代码将采用以下形式:
void FUN_0000034c(ushort param_1) { short sVar1; ushort *puVar2; if (((ushort)(param_1 ^ DAT_00ff0020 ^ 0x5e4e) == 0x5a5a) && ((ushort)(param_1 ^ DAT_00ff0020 ^ 0x4a44) == 0x4e50)) { write_volatile_4(0xc00004,0x4c060003); sVar1 = 0x22; puVar2 = &DAT_00002d94; do { write_volatile_2(VDP_DATA,param_1 ^ DAT_00ff0020 ^ *puVar2); sVar1 = sVar1 + -1; puVar2 = puVar2 + 1; } while (sVar1 != -1); } return; }
我们param_1
我们的param_1
值是常量,由调用函数传递,并且等于#$4147
。 那么DAT_00ff0020
的值应该是DAT_00ff0020
? 我们考虑:
0x4147 ^ DAT_00ff0020 ^ 0x5e4e = 0x5a5a 0x4147 ^ DAT_00ff0020 ^ 0x4a44 = 0x4e50
因为 xor
该操作是可逆的,所有常数可以相互争吵,并获得变量DAT_00ff0020
的所需值。
DAT_00ff0020 = 0x4147 ^ 0x5e4e ^ 0x5a5a = 0x4553 DAT_00ff0020 = 0x4147 ^ 0x4a44 ^ 0x4e50 = 0x4553
事实证明,变量的值应为0x4553
。 看来我已经看到了设置了这样一个值的地方...

结论与决定
我们得出以下结果:
- 首先,您需要跳转到地址
0x0D3C
,为此,您需要输入代码0D3C
- 跳转到函数
0x2D86
,该函数将D1
的值设置为寄存器#$4147
,为此,您需要输入代码2D86
通过实验,我们找出需要按下的键来检查输入的键: B
我们尝试:

谢谢你