Hacker News

Resident Evil 4 (GameCube) – complete byte-identical decompilation to C/C++

Resident Evil 4(GameCube) — C/C++로 완성한 바이트 동일 디컴파일

Resident Evil 4 GameCube 디버그 빌드 전체를 C/C++로 재구성한 프로젝트입니다. 원본 컴파일러와 도구를 재현해 main.dol과 114개 REL을 바이트 단위로 일치시키며, 1,083개 오브젝트와 15,641개 함수를 약 55만 줄의 소스로 제공합니다.

AI 요약

Resident Evil 4(GameCube)의 G4BE08 디버그 빌드, 즉 2004년 11월 25일 프로토타입의 두 디스크를 대상으로 완전한 디컴파일을 진행한 프로젝트입니다. 빌드 결과가 원본과 바이트 단위로 같아야 한다는 기준을 적용해 main.dol과 114개 REL 오버레이를 모두 재현합니다. 저장소는 게임 데이터를 포함하지 않으며, 빌드 시 사용자가 보유한 디버그 디스크 이미지에서 원본 파일을 읽습니다.

■ 규모와 컴파일러 구성

전체 대상은 1,083개 오브젝트입니다. main.dol에 675개, 114개 REL에 408개가 들어갑니다. 함수 수는 15,641개이며, C/C++ 소스는 약 55만 줄, 헤더는 약 3만 3천 줄입니다. 모든 오브젝트는 원본 바이트와 일치하도록 빌드합니다.

게임 코드는 SN Systems ProDG 3.9.3과 GCC 2.95.3의 SN BUILD v1.79를 사용합니다. 프로젝트는 SN의 GPL 소스 공개본으로 네이티브 컴파일러를 빌드합니다. CRI Middleware 라이브러리는 CRI가 배포할 때 사용한 Metrowerks CodeWarrior 2.4.7을 적용합니다. Nintendo SDK는 dolsdk2004에서 가져온 소스와 Metrowerks CodeWarrior GC/1.2.5n으로 빌드합니다. 서로 다른 컴파일러와 버전을 라이브러리별로 맞춰야 하므로 단순히 최신 GCC로 전체 코드를 다시 컴파일하는 방식이 아닙니다.

■ 빌드와 검증 절차

환경은 Linux, Python 3, Ninja입니다. 첫 configure 실행에서 decomp-toolkit, objdiff, wibo, CodeWarrior 빌드 등 필요한 도구를 내려받습니다. 단, SN의 네이티브 GCC는 별도로 SN GPL 소스 공개본을 준비한 뒤 tools/sn-gcc/build.sh로 빌드해야 합니다.

사용자는 디버그 디스크 이미지도 준비해야 합니다. 디스크 1에는 main.dol과 110개 REL이 있고, 디스크 2에는 섬 지역 스테이지용 st3_0부터 st3_3까지 네 개 REL이 있습니다. 이미지를 orig/G4BE08/에 둔 다음 python3 configure.py && ninja를 실행하면 됩니다. 빌드가 끝나면 DOL과 REL 모듈의 매칭 및 링크 진행률이 100%가 되고, build/tools/dtk shasum -c config/G4BE08/build.sha1 명령으로 115개 검증 항목이 모두 OK인지 확인합니다. 특정 단위는 bytecmp.py로 워드 단위 비교를 하고, fdiff.py로 함수 하나의 차이를 확인합니다.

■ 원본 바이트를 재현하는 방법

저장소에는 config/G4BE08/ 아래에 오브젝트와 모듈 목록, 심볼, 분할 정보, 링크 스크립트, REL별 데이터가 정리되어 있습니다. docs/matching.mddocs/research/에는 컴파일러 동작을 분석한 과정과 각 소스 형태가 특정 레지스터 선택이나 명령어 스케줄을 재현하는 이유를 기록합니다.

자연스러운 C/C++ 표현만으로 원본 바이트가 나오지 않는 경우에는 // COMPILER-DIFF: 주석을 붙입니다. 해당 항목은 데드 테스트, 빈 asm(""), 레지스터 고정, 패딩 문장 등 644개입니다. 이 표식 자체는 명령어를 생성하지 않습니다. asmcheck.py --all로 검증하면 이런 템플릿에서 실제 명령어가 나온 경우를 따로 확인할 수 있으며, 하드웨어 커널에서 나온 명령어는 총 231개입니다.

이전 버전에는 게임 코드에 li, lis/addi, mr 같은 명령어를 직접 배치한 코드가 약 100개 있었고 CRI 라이브러리에도 레지스터 고정용 블록이 약 100개 있었습니다. 프로젝트는 이를 2026년 9월 17일 C 코드로 대체했다고 설명합니다. 다만 원래 개발자가 어셈블리로 작성한 부분은 남겨 두었습니다. GCC 게임 코드의 paired-single 및 행렬 커널, GQR 설정, 예외 처리기, MWCC CRI 라이브러리의 paired-single·캐시·SPR 커널과 SDK의 mtx, vec, quat, GX intrinsic 등이 여기에 포함됩니다.

■ 소스와 이름 복원

게임 코드는 C++이며 일부 newlib 코드는 C입니다. SDK, CRI, 런타임 코드는 C 단위로 구성됩니다. src/game/ 아래에는 적, 무기, 플레이어, 방, 인게임 디버그 에디터, 서브스크린 코드가 있고 src/lib/에는 SDK와 CRI, 런타임이 들어갑니다. 애니메이션을 glTF와 BVH로 내보내는 motion_export.py도 제공하며, 게임 자체 코드로 평가한 뒤 Dolphin 실행 결과와 비교합니다.

함수 이름은 디버그 빌드의 Bio4.sym 파일에서 가져옵니다. 파일명과 단위 경계는 바이너리에 남은 D:/Bio4/Prog/<file>.cpp 문자열을 사용합니다. 구조체와 필드 이름은 벤더가 남긴 이름, PS2 디버그 빌드의 타입 정보에서 얻은 이름, 사용 패턴을 보고 프로젝트가 붙인 이름으로 나뉩니다. 아직 의미를 모르는 필드는 오프셋을 뜻하는 xNN 형태로 표시합니다. PS2 타입 정보는 tools/ps2sym.py로 GameCube 레이아웃과 대조합니다.

저장소는 Capcom, Nintendo, CRI Middleware의 지식재산을 포함하며 연구와 보존을 목적으로 공개됐습니다. 빌드 스크립트와 도구, 문서는 CC0으로 배포합니다.

■ Hacker News 반응

  • @davikr — 몇 개의 토큰을 사용했고, 어떤 모델을 사용했나요?
  • @wk_end — 보존 관점에서 보면, 이 프로젝트는 지금까지 나온 디컴파일 중 가장 쓸모없는 축에 듭니다. Capcom이 RE4를 정말 모든 곳에 포팅하려고 애쓰고 있으니까요. 농담입니다. 유출된 디버그 빌드와 심볼을 사용해 만들었다는 점은, 이런 자료를 확보하고 배포하는 게임 보존 커뮤니티의 작업이 얼마나 의미 있는지 보여줍니다. 반면 다음 부분은 꽤 불편합니다. “컴파일러가 레지스터 선택이나 스케줄을 맞추려면 특정 소스 형태가 필요하고 자연스러운 표현을 찾지 못한 경우 // COMPILER-DIFF: 주석을 붙인다. 데드 테스트, 빈 asm("") 세척과 앵커, register T x asm("rN") 고정, 패딩 문장 등 644개가 있다.” 디컴파일의 가치는 원본 바이트를 다시 만드는 데 있지 않다고 봅니다. 원본 바이트는 이미 있으니까요. 사람이 읽을 수 있는 소스 코드로 원래 게임을 이해하는 내용을 복원하는 데 있습니다. 바이트 단위 일치는 제대로 복원했다는 지표일 뿐입니다. 원본 출력을 강제하려고 많은 불필요한 코드를 넣어야 한다면 실제로는 잘못 복원했다는 뜻일 수 있습니다. 특히 AI에 Goodhart의 법칙이 적용될 때 생기는 위험을 보여주는 사례입니다.
  • @Pannoniae — 맞습니다. 정확히 일치시키지 못한 부분을 위한 해킹일 뿐입니다. 그렇지 않다면 빌드 환경이 같지 않은 경우입니다. 정확한 파일 배치나 변수 선언 순서, 컴파일 순서 같은 요소는 바이트 단위로 재현하기가 정말 어렵습니다. 그 차이가 작지만 동등한 결과를 만들 수 있고, 인라이닝이나 최적화 선택도 달라질 수 있습니다. 그래서 바이트 단위 일치는 매우 까다롭습니다.
  • @mitxela — 그런 요소를 왜 재현할 수 없나요?
  • @Pannoniae — 포인터에 올바른 타입을 모두 지정해야 하고, 잘못 추측하면 COMDAT folding이 달라집니다. 함수 안의 지역 변수 순서도 맞춰야 레지스터 할당이 같아지는데, 어떤 함수에는 지역 변수가 150개나 있습니다. PDB가 없다면 번역 단위 경계를 정확히 찾는 일부터 어렵고, 그 뒤에는 컴파일 순서도 알아내야 합니다. 프로젝트 전체의 컴파일 옵션을 강제로 모두 대입해 봐야 하고, CRT나 미들웨어처럼 보통 미리 빌드된 구성 요소도 같은 작업을 해야 합니다. 계속 이어갈까요? ;)

원문: GitHub / 번역·요약: Trawling