
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