DeepQuantum/dconstruct
GitHub: DeepQuantum/dconstruct
针对《最后生还者 第二部》DC-Script 二进制文件的专用反汇编与实验性反编译工具,支持代码分析与二进制编辑修改。
Stars: 10 | Forks: 2
[](https://github.com/deepquantum/dconstruct/releases)
[](https://creativecommons.org/licenses/by-nc-nd/4.0/)
# dconstruct

[打开源 MP4 文件](images/Disassembly.mp4)
`dconstruct` 是一款针对《最后生还者 第二部》中使用的 DC-Script 文件的逆向工程工具。它具备反汇编器和反编译器功能。
它会输出包含反汇编结构和字节码的 `.asm` 文件,以及包含类似 C 语言伪代码的 `.dcpl`(DC 伪语言)文件。
你还可以通过命令行对文件进行编辑,包括毫不费力地替换整个结构体。这使得创建仅修改 .bin 文件中几个值的 Mod 变得极其简单。
# 主要特性
- 速度优化。反汇编和反编译文件的速度极快。
- 准确重建原始源代码,尤其是控制流部分。
- 准确自动解释反汇编中的所有结构体。
- 支持同时反汇编和反编译多个文件。反编译游戏中的每一个 .bin 文件仅需几秒钟。
- 通过 `-e` 标志进行编辑,创建可用于 Mod 的新文件。
- 加载用于反汇编的自定义 sidbase。
# 使用说明
首先,建议将解压后的 dconstruct 目录移动到一个安全的位置,例如 `C:\Program Files`。
为了让 dconstruct 尽可能易于使用,建议将 dconstruct 文件夹内的 .\bin 目录添加到你的 `PATH` 环境变量中。你可以在[这里](https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/)了解更多信息,或者按照以下快速步骤操作:
- 进入 Windows 搜索栏并输入“环境变量”,你应该会看到一个“编辑系统环境变量”的选项。
- 选择该选项后,会弹出一个标题为“系统属性”的窗口。在“确定”、“取消”和“应用”按钮的上方,你会看到一个名为“环境变量...”的按钮。
- 点击该按钮后,会出现另一个窗口。在这里,进入第二个表格(标题为“系统变量”),找到变量名为“Path”的条目并双击它。
- 在此对话框中,选择右侧的“新建”选项。现在粘贴 .\bin 目录的路径。它看起来应该像这样:

- 确保你的路径以“\bin”结尾,而不是“\dconstruct”。
- 在所有打开的对话框中点击“确定”。
- 要验证是否成功,请打开一个新的命令提示符并输入 `dconstruct --about`。你应该会看到程序的一些输出信息且没有错误提示。
在命令行中运行类似这样的命令来生成你的第一个反汇编文件:
```
dconstruct my_bin_file.bin
```
这将在输入文件所在的目录下输出一个名为 `my_bin_file.bin.asm` 的文件。然后你可以使用文本/代码编辑器打开该文件。我建议使用类似 VSCode 这样提供高级搜索功能且善于处理大文件的工具。不建议使用标准的 Windows 记事本。
要反编译文件,请在运行命令时添加 `--decompile` 标志。
# 命令行参数
- `-i` - 输入文件或文件夹。如果将输入路径作为第一个参数传入,则可以省略。
- `-o` - 输出路径。如果输入路径是一个文件夹,则此项不能是文件。如果未指定输出,.txt 文件将被放置在输入文件旁边。如果输入是文件夹且未指定输出,程序将在当前工作目录中创建一个“output”目录并将所有文件放入其中。
- `-s` - 指定 sidbase 的路径。默认情况下,程序将在该路径中查找目录。
- `--no_decompile` - 不要将反编译的伪代码输出到 .dcpl 文件中。该文件将被放置在 .asm 文件旁边。默认为 false。
- `--no_optimize` - 不优化和清理 dcpl 代码。涉及内联函数调用、移除未使用的变量、将兼容的 for 循环转换为 foreach 循环,以及将部分 if-else 链转换为 match 表达式。
- `--pascal_case` - 在 dcpl 输出中将游戏的函数名转换为 pascal case,例如 get-boolean -> GetBoolean。
- `--graphs` - 输出包含所有已反编译函数的控制流图的 .svg 文件。每个 .bin 文件都会有自己的文件夹来存放其所有图形。这会**显著**降低反编译速度,因此在同时反编译大量文件时不建议使用。
- `--emit_once` - 禁止在反汇编中多次输出相同的结构体。如果某个结构体出现多次,只有第一个实例会被完整输出,所有其他出现的地方将被替换为 `ALREADY_EMITTED` 标签。这可以显著减小文件大小。
- `-e` - 进行编辑。更多信息请参见下文。
- `--edit_file` - 提供一个编辑文件。编辑文件每行包含一次编辑。它使用与 -e 标志相同的语法。
# 什么是反汇编器?
[反汇编器](https://en.wikipedia.org/wiki/Disassembler)是一种读取二进制指令(也称[字节码](https://en.wikipedia.org/wiki/Bytecode)或[机器码](https://en.wikipedia.org/wiki/Machine_code))并将每条指令翻译成人类可读版本(称为[助记符](https://en.wikipedia.org/wiki/Assembly_language#Mnemonics))的工具。反汇编器通常不会试图过多解释这些指令的含义,而只是将它们 1:1 地转换为可读版本。例如,以下指令:
```
15 00 00 00
4A 01 01 00
43 31 01 00
1C 00 00 01
```
会被反汇编成以下人类可读的版本:
```
LookupPointer r0, 0
LoadStaticU64Imm r1, 1
Move r49, r1
CallFf r0, r0, 1
```
字节码中的所有数字都是以[十六进制](https://en.wikipedia.org/wiki/hexadecimal)编写的。每行的第一列代表 `opcode`(操作码),或要执行的操作类型。下一列是目标寄存器,用于存储操作的结果。最后两列是操作数 1 和操作数 2,它们可以是寄存器或要执行操作的直接数字。并非所有指令都会使用全部 4 个字节,例如,第一个 `LookupPointer` 指令只需要一个操作数。
dconstruct 反汇编器还添加了一些附加信息,旨在使阅读指令变得更容易。它还插入了标签(如 `L_0`),使代码中的分支更容易追踪。
```
15 00 00 00 LookupPointer r0, 0 r0 = ST[0] ->
4A 01 01 00 LoadStaticU64Imm r1, 1 r1 = ST[1] ->
43 31 01 00 Move r49, r1 r49 = player
1C 00 00 01 CallFf r0, r0, 1 r0 = is-player-abby?(player)
2F 0D 00 00 BranchIfNot r0, 0xD IF NOT r0 => L_0
```
当你想查看文件的原始内容而不希望程序做太多猜测时,这非常有用。但对于大段代码来说可能很难阅读,因为完全没有结构可言。这就是反编译器发挥作用的地方。
# 什么是反编译器?
反编译器是编译器的逆过程。[编译器](https://en.wikipedia.org/wiki/Compiler)是一个接收人类编写的代码(如 C、Java、C++ 等)并生成机器指令的程序。就 TLOU2 以及许多其他 ND 游戏而言,使用的脚本语言称为 'DC',它基本上就是编程语言 [Racket](
0002 0x09A938 43 02 00 00 Move r2, r0 r2 = arg_0
0003 0x09A940 43 31 02 00 Move r49, r2 r49 = arg_0
0004 0x09A948 1B 01 01 01 Call r1, r1, 1 r1 = absf(arg_0)
0005 0x09A950 43 02 00 00 Move r2, r0 r2 = arg_0
0006 0x09A958 40 03 01 00 LoadStaticFloatImm r3, 1 r3 = ST[1] -> <0.000000>
0007 0x09A960 24 02 02 03 FGreaterThanEqual r2, r2, r3 r2 = r2 >= r3
0008 0x09A968 2F 0B 02 00 BranchIfNot r2, 0xB IF NOT r2 => L_0
0009 0x09A970 40 02 02 00 LoadStaticFloatImm r2, 2 r2 = ST[2] -> <1.000000>
000A 0x09A978 2D 0C 00 00 Branch 0xC GOTO => L_1
L_0:
000B 0x09A980 40 02 03 00 LoadStaticFloatImm r2, 3 r2 = ST[3] -> <-1.000000>
L_1:
000C 0x09A988 43 03 02 00 Move r3, r2 r3 = -1.000000
000D 0x09A990 15 04 04 00 LookupPointer r4, 4 r4 = ST[4] ->
000E 0x09A998 43 05 01 00 Move r5, r1 r5 = RET_absf
000F 0x09A9A0 43 31 05 00 Move r49, r5 r49 = RET_absf
0010 0x09A9A8 1C 04 04 01 CallFf r4, r4, 1 r4 = sqrt(RET_absf)
0011 0x09A9B0 07 03 03 04 FMul r3, r3, r4 -1.000000 = -1.000000 * RET_sqrt
0012 0x09A9B8 43 01 03 00 Move r1, r3 r1 = -1.000000
0013 0x09A9C0 00 01 01 00 Return r1 Return
SYMBOL TABLE:
0000 0x09A9C8 function: absf
0001 0x09A9D0 float: 0.000000
0002 0x09A9D8 float: 1.000000
0003 0x09A9E0 float: -1.000000
0004 0x09A9E8 function: sqrt
}
```
代码现在变得易读多了,但即使只有一个分支,如果你不习惯阅读汇编的话,也会令人头疼。
## 控制流图
在此不多赘述,[控制流图 (CFG)](https://en.wikipedia.org/wiki/control_flow_graph) 会沿着各种分支指令将汇编代码分割成“节点”。当我们分析代码以找出程序“流”在何处可能分叉成不同路径时,这至关重要,这可能需要我们输出变量、if 语句、for 循环等。这些图形需要在后台生成,但你可以使用 `--graphs` 程序标志将它们打印成图像。
## 最终伪代码
```
u64? sqrt-sign(f32 arg_0) {
f32 var_1;
if (arg_0 >= 0.00) {
var_1 = 1.00;
} else {
var_1 = -1.00;
}
return var_1 * sqrt(absf(arg_0));
}
```
这个函数的目的现在非常清晰了,我们取参数的绝对值,计算该值的平方根,然后再乘以参数的原始符号。例如,`sqrt-sign(-9) = -3`。
## 优化阶段
dconstruct 会自动对伪代码应用优化阶段。以下是一些示例:
### 函数调用内联
#### 优化前
```
u64? set-arrow-explosive-handle-rootvars() {
u64? var_0 = get-uint64(fx-handle, self);
u64? var_1 = get-float(kill, self);
set-effect-float(var_0, killradius, var_1);
u64? var_2 = get-uint64(fx-handle, self);
u64? var_3 = get-float(strong, self);
set-effect-float(var_2, strongradius, var_3);
u64? var_4 = get-uint64(fx-handle, self);
u64? var_5 = get-float(weak, self);
u64? var_6 = set-effect-float(var_4, weakradius, var_5);
return var_6;
}
```
### 优化后
```
u64? set-arrow-explosive-handle-rootvars() {
set-effect-float(get-uint64(fx-handle, self), killradius, get-float(kill, self));
set-effect-float(get-uint64(fx-handle, self), strongradius, get-float(strong, self));
return set-effect-float(get-uint64(fx-handle, self), weakradius, get-float(weak, self));
}
```
### Foreach 循环
### 优化前
```
u64? bmm-deactivate-all(u64? arg_0) {
u64? var_0 = darray-count(arg_0);
begin-foreach();
for (u64 i = 0; i < var_0; i++) {
u64? var_1 = darray-at(arg_0, i);
u16 var_2;
if (var_1 && *(u16*)(var_1 + 12) == 7) {
var_2 = *(u64*)var_1;
} else if (var_1 && *(u16*)(var_1 + 12) == 5) {
var_2 = *(u64*)var_1;
} else if (var_1 && *(u16*)(var_1 + 12) == 4) {
var_2 = *(u64*)var_1;
} else {
var_2 = 0;
}
net-send-event-all(deactivate, var_2);
}
u64? var_3 = end-foreach();
return var_3;
}
```
### 优化后
```
u64? bmm-deactivate-all(u64? arg_0) {
foreach (u64? var_1 : arg_0) {
u16 var_2;
if (var_1 && *(u16*)(var_1 + 12) == 7) {
var_2 = *(u64*)var_1;
} else if (var_1 && *(u16*)(var_1 + 12) == 5) {
var_2 = *(u64*)var_1;
} else if (var_1 && *(u16*)(var_1 + 12) == 4) {
var_2 = *(u64*)var_1;
} else {
var_2 = 0;
}
net-send-event-all(deactivate, var_2);
}
}
```
### Match 表达式
### 优化前
```
string #C57EE0A64537AE8F(u16 arg_0) {
string var_0;
if (arg_0 == 0) {
var_0 = "Militia";
} else if (arg_0 == 1) {
var_0 = "Scars";
} else if (arg_0 == 2) {
var_0 = "Rattlers";
} else if (arg_0 == 3) {
var_0 = "Infected";
} else if (arg_0 == 4) {
var_0 = "Max Num Factions";
} else {
var_0 = "Invalid";
}
return var_0;
}
```
### 优化后
```
string #C57EE0A64537AE8F(u16 arg_0) {
return match (arg_0) {
0 -> "Militia"
1 -> "Scars"
2 -> "Rattlers"
3 -> "Infected"
4 -> "Max Num Factions"
else -> "Invalid"
};
}
```
## 反汇编结构体示例
```
*ellie-weapons* = symbol-array [0x00190] {
[0] int: 6
[1] int: 0
[2] array [0x198] {size: 6} {
[0] anonymous struct [0x780] {
[0] sid: pistol-beretta
}
[1] anonymous struct [0x788] {
[0] sid: pistol-revolver-taurus
}
[2] anonymous struct [0x790] {
[0] sid: rifle-remington-bolt
}
[3] anonymous struct [0x798] {
[0] sid: bow-ellie
}
[4] anonymous struct [0x7a0] {
[0] sid: shotgun-remington-pump
}
[5] anonymous struct [0x7a8] {
[0] sid: rifle-mpx5
}
}
}
```
# 编辑
使用 -e 标志编辑 DC 文件
你可以使用 -e 标志对 DC 文件应用修改。这些修改会保存到原始文件的新副本中,保持原始文件不变。可以同时指定多个 -e 标志以一次性进行多项修改。
或者,你可以为程序提供一个编辑文件的路径。编辑文件每行包含一次编辑。它使用与 -e 标志相同的语法,但如果你想一次性应用多项修改,使用起来会更容易一些。
编辑发生在反汇编和反编译_之前_,因此修改内容将显示在生成的文件中。
## 修改语法
每次修改遵循以下语法:
```
[]=
```
- ``:你要修改的结构体的内存地址(采用十六进制,以 `0x` 开头。最简单的方法是直接从你要修改的文件的反汇编版本中复制粘贴)。
- ``:结构体内部成员变量的索引。等同于你在成员左侧看到的数字。
- ``:分配给该成员的新值。大小必须相同(整数和浮点数的大小为 4,sid/结构体的大小为 8)。程序不会检查结构体是否属于同一类型。
## 示例
假设你有一个像这样的结构体:
```
[4] firearm-gameplay-def [0x11C28] {
[0] float 0.7 // might represent the rate of fire, so i want to lower it for my mod
...
}
```
要将第一个成员变量(索引为 0)替换为浮点值 0.5,修改命令应为:
`-e 0x11C28[0]=0.5`
我们要修改的结构体位于 `0x11C28`,而我们想要修改第一个成员变量(即浮点数左侧的 0)。然后我们将新值放在 `=` 后面,本例中为 0.5。如果修改成功,程序将显示一条消息,指出该值已从 `0.7->0.5` 更改。
对于编辑文件,只需去掉 `-e` 并每行放置一次修改:
### edit_file.txt
0x11C28[0]=0.5
0x11C28[1]=0.2
...
## 成员变量的类型
结构体可以包含不同类型的成员变量:
- `float` - 指定带小数点的十进制值(例如,0.5)。
- `int` - 指定不带小数点的整数值(例如,42)。
- `sid`(字符串标识符)-(更多信息见下文)
- `string` - 目前不支持替换
- `structure` - 通过替换指针(更多信息见下文)
### 通过名称查找替换 sid:
`-e 0xABC[5]=ellie`
这将在当前的 sidbase 中查找值“ellie”。如果不存在,将发出警告并且不应用任何修改。如果找到该值,实际的哈希值(一个大数字)将替换成员变量处的当前值。
### 通过直接手动哈希覆盖替换 sid:
`-e 0xABC[5]=#XXXXXXXXXXXXXXXX`
# 表示原始哈希值,无需查找即可直接应用。
### 替换成员结构体
如果一个结构体包含另一个结构体作为成员,你可以通过为其分配另一个结构体的地址来替换整个成员结构体。
例如,假设你有以下内容:
```
[4] weapon-gameplay-def [0x0C523] {
...
[7] firearm-gameplay-def [0x11C28] {
...
}
}
```
所以 `weapon-gameplay-def` 包含一个 `firearm-gameplay-def`。
要将 `weapon-gameplay-def` 中的 `firearm-gameplay-def` 替换为位于地址 `0x0ABC` 的另一个不同的 `firearm-gameplay-def`,修改命令应为:
`-e 0x11C28[7]=0x0ABC`
# DCPL 语言 VS Code 扩展
VS Code 支持创建自定义扩展,为自定义语言添加语法高亮。dconstruct 附带一个 .vsix 文件,可为 .dcpl 文件扩展名添加此支持。因为它是一种单一用途的语言,并不真正用于日常编程,所以我不会将该扩展上传到应用商店,而是作为原始的 .vsix 文件发布。要在你的 VS Code 中安装此扩展,请运行以下命令:
```
code --install-extension
```
或者使用 CTRL+SHIFT+P 打开命令面板,输入“Install extension via VSIX”并选择该 .vsix 文。
完成此操作后,你的 .dcpl 代码看起来应该像这样:

# 已知问题
反编译器目前尚未 100% 完成,因此处于“实验性”状态。如果你在反编译过程中收到警告,请不要担心,因为这些功能目前尚不支持,但有望在未来得到支持。除此之外,目前还有更多已知问题:
- 具有重度短路特性的单表达式函数(尤其是结构体内的函数)尚未实现。我已经开始研究这些算法,但不知道需要多长时间才能完成,尽管这属于高优先级事项
- 某些类型不正确,特别是参数类型
- if/else 语句中空的 if 块可能会导致奇怪的缩进。这同样不完全是我的问题,因为存在某些实际上不做任何实际工作的分支,很难被检测到
除了这些之外,还有一些可能不会修复的问题:
- 当不知道函数是否为 void 时返回垃圾值
- 存在大量冗余代码
# 计划中的功能
- 完整的 Racket 和 Python 输出格式
# 特别感谢
- **icemesh** – 提供了底层的 [DC 文件结构](https://github.com/icemesh/dc/tree/main/t2)以及[他的反汇编器](https://github.com/icemesh/t2-dc-disasm),这提供了极大的启发。
- **Specilizer** – 提供了他的 DC-Tool,同样也是本程序的灵感来源。
- **uxh** – 提供了脚本知识。
- **bigdragon** 和 **Wedge** – 进行了 Beta 测试。
- 整个 Mod 制作 Discord 社区 – 感谢你们的友好与帮助。
## 支持
我所有的工具和 Mod 都将永远保持 100% 免费,但像这样的程序需要投入大量的辛勤工作。
如果你想支持我,可以访问我的 Ko-fi:
[](https://ko-fi.com/deepquantum)
## 许可协议
你使用此 Mod 创建的文件完全属于你,你可以自由地对它们进行任何操作。如果能注明出处将不胜感激,但这并不是严格要求的。
本程序本身基于
[知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/)。
进行授权。
这意味着如果你注明出处,则允许与他人分享本程序,但目前不允许对其进行修改或将其商业化。
标签:Amass, DC-Script, Findomain, The Last of Us Part II, 二进制文件分析, 云资产清单, 伪代码, 反汇编器, 反编译器, 游戏修改, 游戏模组, 自定义脚本, 逆向工程