早在 80 年代,一家名为 Binary Systems 的不知名公司发布了游戏 Starflight。游戏中玩家将扮演一名被派往银河系探索的星舰船长。没有设定的路径,让玩家可以在采矿、舰对舰战斗和外星人外交之间自由切换。游戏的更广阔的情节慢慢浮现,玩家发现一个古老的生物种族正在导致星星闪耀并摧毁所有生物。该游戏受到当代和现代评论家的广泛好评,是最早的沙盒游戏之一。该游戏发布后数十年影响了许多其他游戏的设计。
要了解有关游戏的更多信息,请查看以下链接:
游戏可以在GoG购买
当我第一次听说这个游戏时,我就想玩它。然而,我太小了,不会说英语。 20年后我再次尝试,这是一次非常愉快的经历。探索很有趣,故事情节史诗般,结尾充满惊喜,这是我经历过的最好的体验之一。当然,这款游戏还没有成熟,但你可以感受到开发者对游戏的投入。这款游戏既有艺术的一面,也有工匠对细节的关注。
尽管玩这款真正令人惊叹的游戏很有趣,但对这款游戏进行逆向工程也很有趣。您追随开发者的脚步,体验他们的思维过程,仿佛回到了 1985 年。对于这场比赛,期待意想不到的事情。通常,当你对这样的老游戏进行逆向工程时,你必须收到数万行纯汇编代码,你可以使用 IDA Pro 等常用工具进行分析。但这次不是。实际上,在这个游戏中你可以扔掉常用的工具。它们毫无用处。你只能靠你自己了。原因是《星际飞行》是用 Forth 编写的,而我对这种语言几乎一无所知。
Forth 是一种在语法方面具有终极极简主义的语言。没有比“单词”之间的空格更多的语法了。您基本上可以用几行代码编写一个 Forth 阅读器和解释器。
用现代语言你可以写类似的东西
print ( 2 + 3 )
打印 2+3 的结果。然而在 Forth 中它看起来像这样。
2 3 + .
Forth 是一个堆栈机,具有逆波兰表示法。解释如下
语法简单,解释器也简单。 “2”、“3”、“+”和“.”简称为“词”。数据和代码之间没有语法区别。当然,这是一种能够满足早期家用计算机局限性的语言。
当您剖析可执行文件 STARFLT.COM 时,它会揭示一些奇妙的内部结构
如上所述,Forth 是一台堆栈机。作为编码机制,它使用间接线程,这是一种非常节省空间的方法来存储编译后的代码。线程代码的形式基本上完全由对子例程的调用组成。间接线程使用指向位置的指针,这些位置又指向机器代码。
假设您的指令指针指向地址 0x1000 并包含 16 位值 Read16(0x1000)=0x0f72。
0x1000 : dw 0x0f72
值 0x0f72 是第四个字“+”的等效编码。记住上面的描述。单词“+”弹出最后两个堆栈条目,将它们相加并将结果推回堆栈顶部。根据间接线程,这个 16 位值 0x0f72 是一个指向某个位置的指针,该位置又指向机器代码。当您读取内存内容 Read16(0x0f72) 时,您将获得指向 0x0f74 的指针。事实上,当您查看这个内存位置并反汇编它时,您会收到以下内容
0x0f72 : dw 0x0f74
0x0f74 : pop ax
0x0f75 : pop bx
0x0f76 : add ax , bx
0x0f78 : push ax
0x0f79 : lodsw
0x0f7a : mov bx , ax
0x0f7c : jmp word ptr [ bx ]
前四个指令完全执行单词“+”应执行的操作。从“lodsw”开始的最后三个汇编指令增加指令指针并跳转到下一个代码。
让我们继续吧。现在指令指针指向0x1002
0x1002 : dw 0x53a3
读取地址 0x53a3 显示
0x53a3 : dw 0x1d29
0x53a5 : dw 0x0001
以及对应的代码
0x1d29 : inc bx
0x1d2a : inc bx
0x1d2b : push bx
0x1d2c : lodsw
0x1d2d : mov bx , ax
0x1d2f : jmp word ptr [ bx ]
此时寄存器bx包含字地址0x53a3。所以这段代码只是将地址 0x53a5 压入堆栈顶部。我们所做的就是为程序提供一个指向变量的指针。该变量的内容为 0x0001。第四个词“@”将从堆栈中弹出地址,读取其内容并将其推回到堆栈上。
到目前为止,我可以识别 6256 个包含代码或数据的单词。
这实际上就是您需要了解的有关代码结构的全部内容。正如您所看到的,这可能是一种节省空间的编码,但从速度上来说,这是一场灾难。每隔几个机器代码指令,您就必须跳转到不同的代码块。
C 中间接线程的等效项如下所示。
uint16_t instruction_pointer = start_of_program_pointer ;
void Call ( uint16_t word_adress )
{
// the first two byte of the word's address contain
// the address of the corresponding code, which must be executed for this word
uint16_t code_address = Read16 ( word_address );
switch ( code_address )
{
.
.
.
case 0x0f74 : // word '+'
Push16 ( Pop16 () + Pop16 ());
break ;
.
.
.
}
}
void Run ()
{
while ( 1 )
{
uint16_t word_address = Read16 ( instruction_pointer );
instruction_pointer += 2 ;
Call ( word_address );
}
}
针对特定字执行的代码可以访问 5 个主要变量(16 位)
反汇编器将 FORTH 代码转换为 C 风格代码。大多数转换后的代码都可以编译。要了解该程序的功能,请查看下表。它将“字节码”(主要是 16 位指针)作为输入并将其转换为 C 语言。
第四代码:
: .C ( -- )
Display context stack contents.
CR CDEPTH IF CXSP @ 3 + END-CX
DO I 1.5@ .DRJ -3 +LOOP
ELSE ." MT STK"
THEN CR ;
EXIT
改造:
16 位指针 | 福斯 | C |
---|---|---|
: .C (--) | void DrawC() { | |
unsigned short int i, imax; | ||
0x0642 | CR | Exec("CR"); |
0x75d5 | CDEPTH | CDEPTH(); |
0x15fa 0x0020 | 如果 | if (Pop() != 0) { |
0x54ae | CXSP | Push(Read16(pp_CXSP) + 3); |
0xbae | @ | |
0x3b73 | 3 | |
0x0f72 | + | |
0x4ffd | END-CX | Push(Read16(cc_END_dash_CX)); |
0x15b8 | 做 | i = Pop(); |
imax = Pop(); | ||
do { | ||
0x50e0 | 我 | Push(i); |
0x4995 | 1.5@ | _1_dot_5_at_(); |
0x81d5 | .DRJ | DrawDRJ(); |
0x175d 0xfffd | -3 | Push(-3); |
0x155c 0xffff | +循环 | int step = Pop(); |
i += step; | ||
if (((step>=0) && (i>=imax)) || ((step<0) && (i<=imax))) break; | ||
} while(1); | ||
0x1660 0x000b | 别的 | } else { |
0x1bdc | “MT STK” | PRINT("MT STK", 6); |
0x06 | ||
0x4d | 'M' | |
0x54 | 'T' | |
0x20 | '' | |
0x53 | 'S' | |
0x54 | 'T' | |
0x4b | 'K' | |
然后 | } | |
0x0642 | CR | Exec("CR"); |
0x1690 | 出口 | } |
游戏有 3 个文件
STARA.com 的内容
入口 | 尺寸 | 描述 |
---|---|---|
目录 | 4096 | 包含 STARA 和 STARB 目录 |
ELO-CPIC | 4816 | |
燃气太保 | 3120 | |
MEC-太保 | 2848 | |
MYS-CPIC | 6064 | |
NOM-CPIC | 1136 | |
特殊PE-太保 | 1888年 | |
太热太保 | 2480 | |
维尔-CPIC | 4672 | |
VPR-CPIC | 1248 | |
闽太保 | 2096 | |
溅 | 16384 | 图片 |
医学-PIC | 2048 | 图片 |
相位 | 6144 | |
HUM-PIC | 第480章 | 图片 |
威利PIC | 第432章 | 图片 |
THR-PIC | 第272章 | 图片 |
埃洛-PIC | 608 | 图片 |
和-PIC | 640 | 图片 |
节省 | 124000 | |
音乐 | 4960 | 代码叠加 |
地球 | 第1152章 | 地球地图 |
星系 | 6304 | |
学分 | 16384 | 图片 |
太保集团 | 2928 | |
字体 | 第768章 | |
CGA | 3600 | CGA 显卡的机器代码例程 |
尾气分析 | 3600 | EGA 显卡的机器代码例程 |
STARB.COM 内容
入口 | 尺寸 | 描述 |
---|---|---|
目录 | 4096 | 包含 STARA 和 STARB 目录 |
实例 | 150528 | 包含游戏大部分内容的树形结构 |
盒子 | 1024 | 桌子 |
银行运输 | 144 | 桌子 |
船员 | 128 | 桌子 |
血管 | 1936年 | 桌子 |
元素 | 第544章 | 桌子 |
人工制品 | 第1584章 | 桌子 |
行星 | 1360 | 桌子 |
标本 | 第448章 | 桌子 |
生物数据 | 第448章 | 桌子 |
TPORT-PIC | 2416 | 图片 |
BPORT-PIC | 3984 | 图片 |
分析文本 | 3200 | 桌子 |
按钮 | 第944章 | 桌子 |
图标1:1 | 912 | |
图标1:2 | 912 | |
图标1:4 | 912 | |
图标名称 | 第736章 | |
DPART-OV | 第1552章 | 代码叠加 |
地区 | 176 | 桌子 |
生物 | 17024 | 桌子 |
CHK航班-OV | 960 | 代码叠加 |
分形OV | 4640 | 代码叠加 |
ICONP-OV | 第832章 | 代码叠加 |
站点-OV | 1888年 | 代码叠加 |
超MSG-OV | 4112 | 代码叠加 |
吉宝利 | 第368章 | |
面 | 288 | |
顶点 | 第416章 | |
BLT-OV | 第864章 | 代码叠加 |
杂项OV | 1440 | 代码叠加 |
OV银行 | 1520 | 代码叠加 |
螺丝-OV | 2800 | 代码叠加 |
人员-OV | 4192 | 代码叠加 |
船舶图形OV | 2112 | 代码叠加 |
配置-OV | 3072 | 代码叠加 |
TDEPOT-OV | 4800 | 代码叠加 |
端口菜单-OV | 3120 | 代码叠加 |
维他-OV | 3552 | 代码叠加 |
高压OV | 4832 | 代码叠加 |
LP-OV | 5280 | 代码叠加 |
发送OV | 4784 | 代码叠加 |
电视OV | 3472 | 代码叠加 |
通讯OV | 7232 | 代码叠加 |
通信规范-OV | 2864 | 代码叠加 |
种子OV | 2400 | 代码叠加 |
列表图标 | 720 | 代码叠加 |
移动OV | 3808 | 代码叠加 |
工程师 | 2320 | 代码叠加 |
医生 | 1280 | 代码叠加 |
轨道OV | 6640 | 代码叠加 |
队长 | 5952 | 代码叠加 |
科学 | 3952 | 代码叠加 |
导航仪 | 880 | 代码叠加 |
船按钮 | 1984年 | |
MAP-OV | 4160 | 代码叠加 |
超OV | 7168 | 代码叠加 |
分析OV | 2560 | 代码叠加 |
发射-OV | 1360 | 代码叠加 |
通量效应 | 第464章 | |
OP-OV | 4400 | 代码叠加 |
项目-OV | 6016 | 代码叠加 |
系统图标 | 第752章 | |
系统图标 | 第448章 | |
系统图标 | 176 | |
行为OV | 5360 | |
CMAP | 1008 | |
安装 | 800 | |
治愈OV | 第1232章 | 代码叠加 |
修复OV | 1696 | 代码叠加 |
游戏OV | 5920 | 代码叠加 |
PLSET-OV | 2400 | 代码叠加 |
地图OV | 2240 | 代码叠加 |
VES-BLT | 4528 | |
风暴OV | 第1232章 | 代码叠加 |
化合物 | 176 | 桌子 |
IT-OV | 1936年 | 代码叠加 |
战斗OV | 6192 | 代码叠加 |
伤害OV | 2752 | 代码叠加 |
陆地-OV | 1088 | 代码叠加 |
统计统计 | 64 | 桌子 |
STP-OV | 1440 | 代码叠加 |
将原始 Starflight 游戏的文件放入文件夹starflt1-in
和starflt2-in
并运行make
。您应该获得两个可执行文件( disasOV1
和disasOV2
),它们会在文件夹starflt1-out
和starflt2-out
中生成内容。生成的输出是该存储库的一部分。