
TLOU2 DC 스크립트용 디스어셈블러 & 실험적인 디컴파일러.
dconstruct는 The Last of Us Part II에 사용된 DC-Script 파일을 위한 리버스 엔지니어링 도구입니다. 디스어셈블러와 디컴파일러를 갖추고 있습니다.
디스어셈블된 구조와 바이트코드가 포함된 .asm 파일과, C 유사 의사 코드가 포함된 .dcpl (DC Pseudo Language) 파일을 출력합니다.
명령줄을 통해 파일을 편집할 수도 있으며, 전체 구조를 최소한의 노력으로 교체할 수 있습니다. 이는 .bin 파일 내부의 몇 가지 값을 간단히 변경하는 모드를 매우 쉽게 만듭니다.
-e 플래그를 통해 편집 수행, 모드에 사용할 새 파일 생성먼저, 압축을 푼 dconstruct 디렉토리를 C:\Program Files와 같은 안전한 위치로 이동하는 것이 좋습니다.
dconstruct를 최대한 쉽게 사용하려면 dconstruct 폴더 안의 .\bin 디렉토리를 PATH에 추가하는 것이 좋습니다. 자세한 내용은 여기에서 확인하거나, 다음 빠른 단계를 따르세요:

경로가 "\bin"으로 끝나고 "\dconstruct"로 끝나지 않도록 하십시오.
열려 있는 모든 대화 상자에서 '확인'을 클릭합니다.
작동하는지 확인하려면 새 명령 프롬프트를 열고 dconstruct --about을 입력하십시오. 프로그램의 출력이 표시되고 오류 메시지가 없어야 합니다.
명령줄에서 다음과 같은 명령을 실행하여 첫 번째 디스어셈블 파일을 생성하세요:```shell dconstruct my_bin_file.bin
그러면 입력 파일과 동일한 디렉토리에 `my_bin_file.bin.asm` 파일이 출력됩니다. 그런 다음 텍스트/코드 편집기를 사용하여 해당 파일을 열 수 있습니다. 고급 검색 기능을 제공하고 대용량 파일을 잘 처리할 수 있는 VSCode와 같은 프로그램을 추천합니다. 표준 Windows 메모장은 권장되지 않습니다.
파일을 디컴파일하려면 명령을 실행할 때 `--decompile` 플래그를 추가하세요.
# 명령줄 인수
- `-i` - 입력 파일 또는 폴더. 입력 경로를 첫 번째 인수로 전달하는 경우 생략할 수 있습니다.
- `-o` - 출력 경로. 입력 경로가 폴더인 경우 파일로 지정할 수 없습니다. 출력이 지정되지 않으면 .txt 파일이 입력 파일 옆에 배치됩니다. 입력이 폴더이고 출력이 지정되지 않은 경우 프로그램은 현재 작업 디렉토리에 "output" 디렉토리를 생성하고 모든 파일을 그곳에 배치합니다.
- sidbase는 실행 파일 옆에 있는 `sidbase.bin`에서 로드됩니다.
- `--no_decompile` - 디컴파일된 의사 코드를 .dcpl 파일로 내보내지 않습니다. 파일은 .asm 파일 옆에 배치됩니다. 기본값은 false입니다.
- `--no_optimize` - dcpl 코드를 최적화하고 정리하지 않습니다. 여기에는 함수 호출 인라인화, 사용되지 않는 변수 제거, 호환 가능한 for 루프를 foreach 루프로 변환, 일부 if-else 체인을 match 표현식으로 변환하는 작업이 포함됩니다.
- `--pascal_case` - 게임 함수 이름을 dcpl 출력에서 파스칼 표기법으로 변환합니다. 예: 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로 읽을 수 있는 버전으로 변환합니다. 예를 들어, 명령어:```arm
15 00 00 00
4A 01 01 00
43 31 01 00
1C 00 00 01
다음과 같은 사람이 읽을 수 있는 버전으로 분해됩니다:```arm LookupPointer r0, 0 LoadStaticU64Imm r1, 1 Move r49, r1 CallFf r0, r0, 1
바이트코드의 모든 숫자는 [16진수](https://en.wikipedia.org/wiki/hexadecimal)로 작성됩니다. 각 행의 첫 번째 열은 실행할 명령어 유형인 `opcode`를 나타냅니다. 다음 열은 연산 결과가 저장될 대상 레지스터입니다. 마지막 두 열은 연산을 수행할 피연산자 1과 2로, 레지스터 또는 리터럴 숫자입니다. 모든 명령어가 4바이트를 모두 사용하는 것은 아닙니다. 예를 들어, 첫 번째 `LookupPointer` 명령어는 피연산자가 하나만 필요합니다.
dconstruct 디스어셈블러는 명령어를 읽기 쉽게 하기 위해 몇 가지 추가 정보를 제공합니다. 또한 코드의 분기를 쉽게 추적할 수 있도록 레이블(예: `L_0`)을 삽입합니다.```arm
15 00 00 00 LookupPointer r0, 0 r0 = ST[0] -> <is-player-abby?>
4A 01 01 00 LoadStaticU64Imm r1, 1 r1 = ST[1] -> <player>
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
이는 프로그램이 과도한 추측을 하지 않고 파일의 원시 내용을 보고 싶을 때 유용합니다. 하지만 구조가 전혀 없기 때문에 큰 코드 블록의 경우 읽기가 어려울 수 있습니다. 이때 디컴파일러가 필요합니다.
디컴파일러는 컴파일러의 역입니다. 컴파일러는 사람이 작성한 코드(C, Java, C++ 등)를 받아 머신 명령어로 변환하는 프로그램입니다. TLOU2와 다른 많은 ND 게임의 경우, 사용되는 스크립팅 언어는 'DC'라고 불리며, 이는 기본적으로 프로그래밍 언어 Racket의 한 버전에 불과하며, "머신"은 게임 실행 중 명령어를 실행하는 게임 자체입니다. 본질적으로 프로그래머는 DC를 작성하고 컴파일러를 사용하여 해당 코드를 게임과 함께 제공되는 .bin 파일로 변환합니다.
디컴파일러는 위에서 디스어셈블된 코드를 가져와 의사 코드(pseudo code)라고 알려진 것을 생성합니다. 의사 코드는 원시 명령어를 생성하는 데 사용된 원본 소스 코드를 재구성하려는 _시도_입니다. 이는 코드 이해를 훨씬 쉽게 만들기 위한 것이지만, 의사 코드를 생성하는 과정은 최종 바이트 코드를 생성할 수 있는 다양한 소스 코드 버전과 컴파일 중 이루어지는 최적화로 인해 상당히 복잡합니다.
현재 dconstruct 디컴파일러의 출력은 원본 DC와 구문적으로 유사하지 않습니다. DC(즉, Racket)는 고유한 구문을 가진 함수형 프로그래밍 언어로, 이에 익숙하지 않은 프로그래머에게는 엄청나게 읽기 어렵습니다. 이러한 이유로 현재는 의사 코드가 대부분의 사람들이 더 읽기 쉬운 C처럼 보이도록 선택했습니다. 그러나 Racket 및 Python 버전을 포함한 더 많은 구문을 만드는 것이 계획되어 있습니다.
43 00 31 00 15 01 00 00 43 02 00 00 43 31 02 00 1B 01 01 01 43 02 00 00 40 03 01 00 24 02 02 03 2F 0B 02 00 40 02 02 00 2D 0C 00 00 40 02 03 00 43 03 02 00 15 04 04 00 43 05 01 00 43 31 05 00 1C 04 04 01 07 03 03 04 43 01 03 00 00 01 01 00
이것만으로는 코드가 무엇을 하는지 거의 알 수 없습니다.
## 역어셈블된 코드와 레이블 및 심볼 테이블```arm
sqrt-sign = script-lambda [0x9A8D8] {
[1 args]
0000 0x09A928 43 00 31 00 Move r0, r49 r0 = arg_0
0001 0x09A930 15 01 00 00 LookupPointer r1, 0 r1 = ST[0] -> <absf>
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] -> <sqrt>
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
}
The code is now a lot more readable, but even the single branch is annoying to read if you're not used to reading assembly.
여기서 너무 자세히 설명하지는 않겠지만, 제어 흐름 그래프 (CFG)는 다양한 분기 명령어를 따라 어셈블리 코드를 "노드"로 분할합니다. 이는 프로그램의 "흐름"이 다른 경로로 분기될 수 있는 위치를 파악하기 위해 코드를 분석할 때 중요합니다. 이러한 분기는 변수, 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는 의사 코드에 자동으로 최적화 패스를 적용합니다. 다음은 몇 가지 예시입니다:
### 함수 호출 인라이닝
#### 이전```c
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 loops
### 이전```c#
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); } }
### 매치 표현식
### 이전```scala
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" }; }
## 분해된 구조의 예```c
*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 플래그와 동일한 구문을 사용하지만, 여러 편집을 한 번에 적용하려는 경우 조금 더 사용하기 쉬울 수 있습니다.
편집은 디스어셈블리 및 디컴파일 전에 수행되므로, 편집 내용이 생성된 파일에 표시됩니다.
각 편집은 다음 구문을 따릅니다:```xml
[]= ``` - ``: 편집하려는 구조체의 메모리 주소 (16진수, `0x`로 시작함. 편집하려는 파일의 디스어셈블된 버전에서 복사하여 붙여넣는 것이 가장 쉽습니다). - ``: 구조체 내 멤버 변수의 인덱스입니다. 멤버 왼쪽에 표시된 숫자와 동일합니다. - ``: 해당 멤버에 할당할 새 값입니다. 동일한 크기여야 합니다. (ints와 floats는 크기가 4, sids/structs는 크기가 8입니다). 프로그램은 구조체가 동일한 타입인지 확인하지 않습니다.다음과 같은 구조체가 있다고 가정해 보겠습니다:```c++ [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)를 float 값 0.5로 바꾸려면 편집 명령은 다음과 같습니다:
`-e 0x11C28[0]=0.5`
편집하려는 구조체는 `0x11C28`에 있으며, 첫 번째 멤버 변수(float 왼쪽의 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`
#은 원시 해시 값을 나타내며, 조회 없이 직접 적용됩니다.
### 멤버 구조체 교체
구조체가 다른 구조체를 멤버로 포함하는 경우, 전체 멤버 구조체를 다른 구조체의 주소를 할당하여 교체할 수 있습니다.
예를 들어, 다음과 같은 경우를 가정해 보겠습니다:```c++
[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
VS Code는 사용자 정의 언어에 구문 강조를 추가하기 위한 사용자 정의 확장을 생성하는 것을 지원합니다. dconstruct는 .dcpl 파일 확장자에 대한 이 지원을 추가하는 .vsix 파일을 함께 제공합니다. 이는 일상적인 프로그래밍을 위한 것이 아닌 단일 용도 언어이기 때문에, 확장을 마켓플레이스에 업로드하지 않고 대신 원시 .vsix 파일로 제공합니다. VS Code에 이 확장을 설치하려면 다음 명령을 실행하십시오:```shell code --install-extension <path/to/extension/dcpl-lint-0.0.1.vsix>
또는 CTRL+SHIFT+P를 눌러 명령 팔레트를 열고 'Install extension via VSIX'를 입력한 후 .vsix 파일을 선택하세요.
그러면 .dcpl 코드가 다음과 같이 표시됩니다:

# 알려진 문제
디컴파일러는 현재 100% 완성되지 않았으므로 '실험적' 상태입니다. 디컴파일 중 경고가 발생해도 걱정하지 마세요. 해당 기능들은 현재 지원되지 않지만, 향후 지원될 예정입니다. 이 외에도 현재 다음과 같은 알려진 문제가 있습니다:
- 단일 표현식 함수 중 단락 평가(short-circuiting)가 많이 사용된 함수(특히 구조체 내 함수)는 아직 구현되지 않았습니다. 이에 대한 알고리즘 작업을 시작했지만 완료까지 얼마나 걸릴지 모르겠습니다. 하지만 이는 우선순위가 높은 작업입니다.
- 일부 타입(특히 인수 타입)이 올바르지 않습니다.
- 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** – 베타 테스트를 해 주셨습니다.
- 전체 모딩 Discord 커뮤니티 – 친절하고 도움을 주셨습니다.
## 지원
제 모든 도구와 모드는 항상 100% 무료입니다. 하지만 이런 프로그램에는 많은 노력이 필요합니다.
저를 지원하고 싶다면 제 Ko-fi를 방문해 주세요:
[](https://ko-fi.com/deepquantum)
## 라이선스
이 모드를 사용하여 생성한 파일은 전적으로 귀하의 소유이며, 원하는 대로 자유롭게 사용할 수 있습니다. 출처 표시는 감사하지만 필수는 아닙니다.
프로그램 자체는 [크리에이티브 커먼즈 저작자표시-비영리-변경금지 4.0 국제 라이선스](https://creativecommons.org/licenses/by-nc-nd/4.0/)에 따라 라이선스가 부여됩니다.
이는 출처를 표시하면 다른 사람과 프로그램을 공유할 수 있지만, 현재 수정하거나 상업적으로 이용할 수 없음을 의미합니다.