Resident Evil 4를 바이트 하나까지 되살리다: 15,641개 함수 완전 복원 프로젝트

한눈에 보는 요약
게임큐브판 Resident Evil 4의 디버그 빌드(G4BE08)를 C/C++ 소스로 되돌린 디컴파일 프로젝트가 공개됐습니다. 놀라운 점은 "비슷하게" 재현한 수준이 아니라는 것입니다. 이 프로젝트를 빌드하면 원본 main.dol과 114개의 REL 오버레이가 바이트 단위까지 정확히 일치합니다.
숫자로 보면 규모가 더 와닿습니다.
- 복원한 오브젝트: 1,083개 (DOL에 675개, REL 114개에 408개)
- 복원한 함수: 15,641개
- C/C++ 소스: 약 55만 5,000줄, 헤더: 약 3만 3,000줄
- 별도 어셈블리 파일: 없음 (인라인 어셈블리와 예외 유닛 8개만 존재)
대상은 게임큐브용 "Nov 25 2004" 프로토타입 디스크 2장입니다. 디스크 1에서 main.dol과 REL 110개를, 디스크 2에서 섬 스테이지용 REL 4개(st3_0~st3_3)를 읽어옵니다.
왜 "바이트 단위 일치"가 어려운가
디컴파일에는 여러 수준이 있습니다. 읽을 수 있는 코드로 되돌리는 것과, 같은 컴파일러에 넣었을 때 원본과 똑같은 기계어가 나오는 코드를 찾아내는 것은 난이도가 완전히 다릅니다. 후자는 레지스터 배정, 명령어 스케줄링, 리터럴 풀링 같은 컴파일러의 미세한 습관까지 역으로 추론해야 하기 때문입니다.
이 프로젝트는 코드의 출처에 따라 세 종류의 컴파일러를 각각 사용합니다.
| 코드 영역 | 사용 컴파일러 |
|---|---|
| 게임 코드 | SN Systems ProDG 3.9.3의 GCC 2.95.3 "SN BUILD v1.79" |
| CRI 미들웨어 (adx_*, sfd_*, mpv_*) | Metrowerks CodeWarrior 2.4.7 (GC/2.7) |
| Nintendo SDK (OS*, GX*) | Metrowerks CodeWarrior GC/1.2.5n |
게임 코드용 네이티브 컴파일러는 SN이 GPL로 공개한 소스에서 직접 빌드해야 하고, SDK 소스는 dolsdk2004에서 가져옵니다. 그리고 매 빌드마다 config/G4BE08/build.sha1로 해시를 검증합니다. 빌드가 끝나면 DOL과 REL 일치율이 100%로 표시되고, 해시 검사는 OK 115개를 출력합니다.
자연스러운 C로 표현되지 않을 때
원본과 같은 바이트를 만들되 어셈블리는 쓰지 않는다 — 이 원칙을 지키다 보면 "이 명령어 배열을 어떻게 C로 유도하지?"라는 문제에 부딪힙니다. 프로젝트는 이런 지점에 // COMPILER-DIFF: 주석을 달아 644곳을 기록해 두었습니다.
여기에 쓰이는 기법들입니다.
- 실행되지 않는 조건 검사
- 빈
asm("")를 이용한 값 처리와 고정 지점 만들기 register T x asm("rN")형태의 레지스터 고정- 의도적인 패딩 문장
중요한 건 이 구문들이 어떤 명령어도 생성하지 않는다는 점입니다. 순수하게 컴파일러의 판단을 유도하기 위한 장치이고, 각각이 왜 동작하는지는 docs/matching.md와 docs/research/에 정리돼 있습니다.
심지어 어셈블리를 걷어내는 작업도 계속 진행됐습니다. 예전 소스에는 게임 코드에 직접 박아 넣은 명령어 약 100개와 CRI 라이브러리의 레지스터 고정 블록 약 100개가 있었지만, 2026년 9월 17일 자로 모두 C로 대체됐습니다. asm("li %0,0"), asm("lis/addi"), asm("mr") 같은 것들이 사라진 셈입니다.
그래도 남은 어셈블리
남아 있는 어셈블리는 원본에서도 어셈블리로 작성됐던 코드입니다. 당시 컴파일러로는 달리 표현할 방법이 없었던 부분이죠.
paired-single과 하드웨어 커널 (GCC 게임 코드)
math_sub의SINF,COSF,RSQRT,LIMIT_ANGLEtrans,shape,dbmodule의 행렬 연산Espgen42,espgen45의 양자화된psq_lmain과scheduler의 GQR 설정, libsnsndvd의 예외 처리기
CRI/SDK 쪽 (CodeWarrior)
mpv_umc,mpv_mc,dct_fsri,cftyp422_ppc,mpv_lib- SDK의
mtx,vec,quat,GX내장 연산
흥미로운 예외도 있습니다. dct_ac의 dctac_Init에는 레지스터 유도 블록이 하나 남았는데, 공급업체의 컴파일러 빌드는 .bss만 풀링하고 함수 내 8바이트 리터럴은 풀링하지 않은 반면 이 프로젝트의 빌드는 둘 다 풀링하기 때문입니다. 또 기계어를 만들지 않는 asm { mr r11, x; mr x, r11 } 고정 구문과 asm { mr v, v } 자기 복사가 27곳에 남아 있습니다. 전자는 할당 과정에서 두 이동이 제거되지만 쓸 수 있는 레지스터를 하나 줄이는 효과를, 후자는 컴파일러가 내부를 알 수 없는 두 번째 정의 역할을 합니다.
함수 전체가 어셈블리인 유닛은 8개입니다. crt0의 __start, eabi, SN의 tealeaf, fileserver, ppcdown, proview, 그리고 Capcom의 memset_2, yz2asm이죠. 이들은 C/C++ 파일 안에 GAS 문법의 최상위 asm()으로 작성돼 다른 코드와 동일한 ProDG 드라이버로 컴파일됩니다. include/asm_regs.h가 r3, f1, GQR0 같은 이름을 .set 상수로 제공합니다.
검증 도구인 tools/asmcheck.py는 모든 GCC 유닛의 어셈블리 템플릿에 표시를 붙여 컴파일한 뒤 실제로 생성된 명령어를 추적합니다. 그 결과 검출되는 명령어는 하드웨어 커널의 231개뿐이며, 어셈블리 본문 유닛 8개는 asm-bodied로 따로 표시돼 이 수치에서 제외됩니다.
이름은 어디서 왔을까
복원 프로젝트에서 가장 고된 작업 중 하나가 "이 함수가 뭘 하는지 알아내고 이름 붙이기"입니다. 다행히 디버그 빌드에는 힌트가 넉넉히 남아 있었습니다.
- 함수 이름: 디버그 빌드의
Bio4.sym파일에 전부 들어 있습니다. C++ 이름 맹글링이 적용된 덕분에 게임 코드는 C++로, SDK·CRI·newlib 유닛은 C로 구성해야 한다는 것도 역으로 알 수 있었습니다. - 파일 이름과 유닛 경계: 바이너리의 assert에 남은
D:/Bio4/Prog/<file>.cpp문자열에서 가져왔습니다. - 구조체·필드 이름: PS2 디버그 빌드의 타입 정보를
tools/ps2sym.py로 게임큐브 레이아웃에 대응시켰습니다. 추정해서 붙인 이름은 그 사실을 표시하고, 의미를 모르는 필드는xNN형태의 16진수 오프셋 자리표시자를 씁니다.
공급업체가 쓰던 철자를 그대로 유지하기 때문에 명명 규칙이 일부러 섞여 있고, #line 지시문으로 assert 문자열의 원본 줄 번호까지 재현합니다. 자세한 내용은 docs/naming.md에 있습니다.
소스 구조 살펴보기
src/game/ C++ 게임 코드 + 일부 newlib C 유닛
src/em*/ 적 캐릭터
src/wep*/ 무기
src/pl*/ 플레이어 캐릭터
src/st*/ 방 (방마다 REL 하나)
src/t_*/, src/Tools/, src/tools/ 게임 내 디버그 편집기
src/Sscrn/ 보조 화면
src/lib/ SDK, CRI, 런타임
include/ 복원한 구조체 레이아웃 포함 헤더
config/G4BE08/ objects.py, modules.py, symbols.txt, splits.txt, 링커 스크립트, build.sha1
tools/ 빌드 생성·REL 재구성·컴파일러 분석 도구
도구 쪽도 볼 만합니다. project.py가 빌드 생성기, ngccc.py가 ProDG 드라이버, make_rel.py·link_rel.py가 REL 재구성을 맡습니다. 개별 유닛을 손볼 때는 tools/bytecmp.py로 오브젝트를 워드 단위 비교하고, tools/fdiff.py로 특정 함수의 차이를 확인할 수 있습니다.
재미있는 건 motion_export.py와 motion/입니다. 게임 자체 코드로 애니메이션을 계산해 glTF/BVH로 내보내고, Dolphin에서 실제로 돌린 게임과 대조해 검증합니다. 복원한 코드가 진짜로 동작한다는 걸 보여주는 사례죠.
문서도 충실합니다. docs/overview.md는 하위 시스템별 소스 탐색 안내서, docs/matching.md는 컴파일러 출처와 바이트 일치 재현 요령, docs/unit-notes.md는 유닛별 기록, docs/research/는 단계별 연구 기록을 담고 있습니다.
직접 빌드해 보려면
필요한 것은 Linux, Python 3, ninja, 그리고 직접 준비한 디버그 디스크 2장의 이미지입니다. 최초 구성 시 decomp-toolkit, objdiff, wibo, CodeWarrior 빌드 등은 자동으로 내려받지만, 네이티브 SN GCC만큼은 별도로 빌드해야 합니다. 원본 파일은 구성 단계에서 디스크 이미지로부터 읽어옵니다.
기여를 생각한다면 CONTRIBUTING.md를 먼저 보세요. 빌드 방법, 세 가지 검증 절차, 바이트 불변 규칙, 명령어를 생성하는 어셈블리 금지 규칙, 명명 규칙이 정리돼 있습니다. 이름 변경을 제안할 때는 근거를 함께 제시해야 합니다.
법적 범위는 분명히
저장소에는 게임 에셋도, 디스크에서 복사한 코드나 데이터도 없습니다. 빌드하려면 본인이 디스크 이미지를 준비해야 하는 이유입니다.
복원된 게임 및 SDK 소스의 지식재산권은 Capcom, Nintendo, CRI Middleware 등 각 권리자에게 있으며, 이 프로젝트는 연구와 보존 목적으로만 공개됐습니다. 반면 프로젝트가 자체 작성한 빌드 스크립트, 도구, 문서는 CC0로 배포되며 관련 조건은 LICENSE에 명시돼 있습니다.
마무리하며
2005년에 출시된 게임의 코드를 20여 년이 지나 바이트 하나 틀리지 않게 되살린다는 건, 단순한 향수 프로젝트가 아닙니다. 당시 컴파일러가 어떤 판단을 내렸는지, 개발자들이 하드웨어 제약 아래서 어떤 선택을 했는지를 실행 가능한 형태로 남기는 작업입니다.
// COMPILER-DIFF: 주석 644개와 231개의 하드웨어 커널 명령어는 그 자체로 한 시대의 개발 환경에 대한 기록인 셈이죠. 게임 보존에 관심이 있다면, 혹은 컴파일러 내부 동작이 궁금하다면 docs/matching.md부터 읽어 보시길 권합니다.