DeepQuantum/dconstruct

GitHub: DeepQuantum/dconstruct

针对《最后生还者 第二部》DC-Script 二进制文件的专用反汇编与实验性反编译工具,支持代码分析与二进制编辑修改。

Stars: 10 | Forks: 2

[![版本](https://img.shields.io/badge/version-%s-blue.svg)](https://github.com/deepquantum/dconstruct/releases) [![许可协议: CC BY-NC-ND 4.0](https://img.shields.io/badge/License-CC--BY--NC--ND%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-nd/4.0/) # dconstruct ![完整的反汇编与反编译动画](https://raw.githubusercontent.com/DeepQuantum/dconstruct/main/images/disassembly_readme.webp) [打开源 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 目录的路径。它看起来应该像这样: ![a](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/8166f5afb5205035.png) - 确保你的路径以“\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 代码看起来应该像这样: ![dcpl_code](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/ccfc5a93ac205038.png) # 已知问题 反编译器目前尚未 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: [![Ko-fi](https://img.shields.io/badge/Donate-Ko--fi-ff5f5f.svg)](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, 二进制文件分析, 云资产清单, 伪代码, 反汇编器, 反编译器, 游戏修改, 游戏模组, 自定义脚本, 逆向工程