호스트 CPU에서 그대로 실행하는 C# 브리지SharpEmu는 "PS5용으로 빌드된 게임 실행 파일(eboot.bin)을 PC에서 로드하고, 그 안의 x86-64 기계어를 실행하며, PS5 운영체제 함수 호출을 PC 쪽 구현으로 가로채 대신 처리하는 에뮬레이터"다. 이름은 Sharp(C# = C-Sharp)와 Emulator의 합성어. 대부분의 콘솔 에뮬레이터가 C/C++로 쓰이는 것과 달리, 이 프로젝트는 거의 전부 C#(99.8%)이라는 점이 첫 번째 이례성이다.
보통 에뮬레이터는 통역사다 — 게스트 기계(예: 닌텐도 스위치의 ARM CPU)의 명령어를 호스트 PC(x86-64)의 명령어로 한 문장씩 통역해준다. 느리고 복잡하다. 그런데 PS5와 PC는 둘 다 x86-64라는 같은 언어를 쓴다. SharpEmu는 그래서 통역을 포기하고, PS5 게임 코드를 호스트 CPU가 원문 그대로 읽게 둔다.
대신 진짜 어려운 문제는 따로 있다: 손님(게임)이 "집주인(PS5 운영체제)"에게 "메모리 좀 줘", "화면에 그려줘" 하고 부탁하는 순간이다. 집주인이 없으니, SharpEmu가 집주인 흉내를 내며 그 부탁만 가로채 대신 응답한다. 이 "집주인 대역"이 바로 HLE이고, 이 레포 코드의 대부분을 차지한다.
sceKernelMapDirectMemory(메모리 매핑)를 호출하면, SharpEmu는 진짜 PS5 커널 대신 자기가 만든 C# 함수로 응답한다. 펌웨어 없이 돌릴 수 있고 빠르지만, 함수 하나하나를 직접 구현해야 해서 손이 많이 간다.eboot.bin을 열어 헤더를 읽고, 코드·데이터 조각(세그먼트)을 메모리에 배치하고, 시작 주소로 점프하는 것이다.콘솔 에뮬레이션에서 최신 세대(PS5)는 사실상 성배다. PS4용으로는 완성도 높은 ShadPS4가 있지만, PS5는 아직 제대로 게임이 돌아가는 에뮬레이터가 거의 없다. SharpEmu가 별 5개에 불과한 극초기 프로젝트인데도 TrendShift Daily 상위에 오른 건, "누군가 PS5 에뮬을 진지하게 C#으로 시도한다"는 사실 자체가 X·해커뉴스에서 화제가 됐기 때문이다. (TrendShift의 굵은 숫자는 별 수가 아니라 사회적 언급 모멘텀이다.)
대부분의 에뮬레이터는 인터프리터나 JIT로 게스트 명령어를 호스트 명령어로 번역한다. SharpEmu는 PS5도 PC도 x86-64라는 사실을 이용해 그 단계를 통째로 건너뛴다. 게스트 코드를 호스트 메모리에 실행 가능 상태로 얹고, 진짜 CPU가 직접 실행하게 한다. 코드에 실제로 CPU 엔진은 NativeOnly 하나뿐이고, 유명한 x86 디스어셈블러 Iced는 실행이 아니라 로그·진단용으로만 쓴다. 이 한 가지 결정이 이 레포의 모든 것을 설명한다.
DolphinㆍRPCS3ㆍShadPS4 등 주류 에뮬레이터는 성능을 위해 C++이 정석이다. SharpEmu는 관리형 언어(managed) C#으로 이 영역에 뛰어들었다. 관리형 언어의 가비지 컬렉터(GC)와 네이티브 게스트 코드가 충돌하는 까다로운 문제를, [UnmanagedCallersOnly] · 손수 emit한 기계어 · 원시 OS 스레드로 정면 돌파한다. "C#으로 이런 것까지 되나?"라는 호기심이 개발자들을 끌어들이는 지점.
고작 154 커밋짜리 v0.0.1이지만, 코드가 인터페이스로 잘 쪼개져(ISelfLoader·IVirtualMemory·IModuleManager…) 있고, HLE 함수를 [SysAbiExport] 애트리뷰트 하나로 등록하는 플러그인 구조, 8천 줄짜리 실행 백엔드를 partial class로 5조각 나눈 정리까지 — 교재로 삼기 좋은 설계 패턴이 곳곳에 있다. "작동하는 PS5 에뮬"로서가 아니라 "에뮬레이터 아키텍처 학습 자료"로서 값지다.
README가 분명히 밝힌다: 저장소에 펌웨어·게임 데이터·소니 자산이 전혀 없고, 테스트에 쓴 게임은 전부 개발자가 직접 소유한 콘솔에서 덤프한 것이며, 사용자도 합법적으로 취득한 사본만 쓰라고 못박는다. 순수 연구·교육 목적임을 반복 강조한다. (에뮬레이터 자체는 합법이며, 이 문서도 그 설계를 공부하기 위한 것이다.)
| 비교 축 | SharpEmu의 선택 | 흔한 에뮬레이터 |
|---|---|---|
| CPU 처리 | 호스트에서 직접 실행 (번역 없음) | 인터프리터 / JIT 재컴파일 |
| 구현 언어 | C# / .NET 10 (관리형) | C / C++ (네이티브) |
| OS 재현 | HLE (sceKernel* 함수 대체) | HLE 또는 LLE(펌웨어 실행) |
| 대상 세대 | PS5 전용 (PS4는 ShadPS4에 양보) | 대개 이전 세대 |
| 이식성 | 호스트가 x86-64여야만 함 (근본 한계) | ARM 호스트도 가능(번역하므로) |
직접 실행은 게스트와 호스트의 CPU가 같을 때만 가능하다. 스위치(ARM)를 x86 PC에서 돌리려면 반드시 번역해야 한다. PS5→x86 PC는 운 좋게 CPU가 같아서 이 지름길이 열린 것. 대신 대가가 있다: 이 방식은 ARM 맥이나 ARM PC에서는 원천적으로 불가능하고, 게스트 코드가 호스트를 직접 건드리니 보안 격리(샌드박싱)를 스스로 포기해야 한다(뒤에서 자세히).
SharpEmu는 순수 .NET 솔루션이다. global.json이 SDK를 10.0.103으로 못박고(rollForward: disable), 타깃 프레임워크는 net10.0, nullable·ImplicitUsings 켜짐, 그리고 네 개 프로젝트에서 AllowUnsafeBlocks가 켜져 있다(게스트 메모리를 포인터로 직접 만지기 때문). AOT는 쓰지 않고 JIT + SelfContained + PublishSingleFile로 배포한다.
SharpEmu.slnx에 6개 프로젝트가 있고, 의존 방향은 Logging ← HLE ← Libs ← Core ← CLI (GUI는 Logging에만 의존, CLI가 GUI를 호스팅).
| 프로젝트 | 역할 |
|---|---|
| SharpEmu.CLI | 진입점(Program.cs). 인자 파싱, 콘솔/UTF-8 설정, 실행 오케스트레이션 |
| SharpEmu.Core | 심장부 — CPU 디스패처 · 네이티브 실행 백엔드 · SELF 로더 · 메모리 관리 · 런타임 |
| SharpEmu.HLE | HLE 계약(인터페이스) + NID 엔진. CpuContext(게스트 레지스터), 애트리뷰트 기반 디스패치 테이블 |
| SharpEmu.Libs | PS5 시스템 라이브러리 구현 589개([SysAbiExport] 함수). Kernel·GPU(Agc)·VideoOut·Audio·Pad(듀얼센스)… |
| SharpEmu.GUI | Avalonia 프런트엔드 + ATRAC9 오디오 디코더(MIT) + 컨트롤러 리더 |
| SharpEmu.Logging | 로그 싱크(Console/File/Composite), LogLevel, 빌드 배너 |
| 영역 | 패키지 (버전) | 역할 / 왜 골랐나 |
|---|---|---|
| x86 디스어셈블러 | Iced 1.21.0 | 기계어 ↔ 사람이 읽는 어셈블리 변환. 실행이 아니라 진단·로그·패턴 스캔용 |
| 그래픽(Vulkan) | Silk.NET.Vulkan 2.23.0 (+KHR/EXT/Windowing) | PS5 GPU 명령을 번역해 Vulkan으로 화면 출력 |
| GUI | Avalonia 11.3.18 (+Desktop/Fluent/Inter) | 크로스플랫폼 데스크톱 UI(게임 목록·설정) |
| 리눅스 전송 | Tmds.DBus.Protocol 0.21.3 | 보안 픽스 위해 고정된 전이 의존성 |
48 8B 04 25 …)를 사람이 읽는 어셈블리(mov rax, ...)로 되돌리는 도구. Iced는 .NET에서 가장 널리 쓰이는 x86/x64 디스어셈블러다. 중요한 점: SharpEmu는 이걸로 코드를 실행하지 않는다. 게스트가 뭘 하려는지 사람이 로그로 보고 디버깅하기 위해, 그리고 특정 명령어 패턴(예: TLS 접근)을 찾아 패치하기 위해서만 쓴다.라이선스 구성도 학습거리다. 프로젝트 전체는 GPL-2.0-or-later이고, REUSE.toml + LICENSES/로 SPDX/REUSE 규격을 지킨다. PS5의 ATRAC9 오디오 코덱을 디코드하는 src/SharpEmu.GUI/Atrac9/만 별도로 MIT(Alex Barney의 LibAtrac9 포팅)다. NID 이름표 파일(aerolib.bin·ps5_names.txt)은 소니 코드가 아니라 함수 이름↔해시 대응표일 뿐이라 저작권 문제가 없다.
이 섹션의 첫 문장만 기억해도 이 레포의 90%를 이해한 것이다: SharpEmu는 CPU 에뮬레이터가 아니라 "로더 + HLE + 네이티브 실행 브리지"다. 게스트 PS5 코드는 호스트 x86-64 CPU 위에서 직접 돈다. 그래서 이 프로젝트의 어려움은 "명령어를 어떻게 흉내 내나"가 아니라 "내 코드(관리형 C#)와 게스트 코드(네이티브)를 어떻게 안전하게 오가나"에 집중돼 있다.
SharpEmu.CLI "eboot.bin"을 실행하면 SharpEmuRuntime.Run이 아래 파이프라인을 태운다.
Core/Loader/SelfLoader.cs(2,421줄)가 담당한다. PS5 실행 파일은 리눅스 ELF의 소니 변형(SELF)이다. 로더의 일:
0x4F153D1D이 있으면 SELF 래퍼를 파싱, 없으면 순수 ELF로 취급.PT_LOAD 세그먼트를 게스트 메모리에 Map(va, size, ...). PS5 이미지 베이스는 0x0000000800000000.ProgramHeaderType enum에 표준 Load/Dynamic/Tls 외에 소니 확장 SceRela(0x60000000)·SceDynLibData(0x61000000)·SceProcParam·SceRelro가 있다.DT_* + PS 확장 DT_SCE_* 태그를 읽어, R_X86_64_JUMP_SLOT(7)·GLOB_DAT(6)·RELATIVE(8)·DTPMOD64(16, TLS) 재배치를 적용.param.sfo가 아니라 PS5의 param.json을 읽어 제목·타이틀ID·버전을 취득.게임이 sceKernelMapDirectMemory 같은 시스템 함수를 부르면, 이걸 SharpEmu의 C# 구현으로 연결해야 한다. 그런데 PS5 실행 파일에는 함수 이름이 아니라 NID라는 해시가 들어있다. 그래서 두 가지가 필요하다: (1) NID↔이름 대응표, (2) 이름으로 등록된 내 C# 함수를 찾는 디스패치 테이블.
sceKernelMapDirectMemory 대신 L-Q3LEjIbgA 같은 NID만 들어있다. 에뮬레이터는 이 NID를 역으로 이름에 대응시켜야 어떤 함수인지 안다.SharpEmu의 scripts/generate_aerolib_binary.py에 실제 NID 계산식이 있다(소니 PS4/PS5 공용 소금값 518D64A6…):
# NID = 함수이름 + 고정소금 → SHA-1 → 앞 8바이트 → base64(+- 변형)
symbol = sha1( name.encode() + unhexlify('518D64A635DED8C1E6B039B1C3E55230') ).digest()
id_val = struct.unpack('<Q', symbol[:8])[0] # 앞 8바이트를 리틀엔디언 u64로
nid = base64encode(id_val, altchars=b'+-').rstrip(b'=') # 11글자 NID
이 스크립트가 ps5_names.txt의 148,678개 함수 이름을 전부 NID로 계산해 12.3MB aerolib.bin(NID↔이름 대응표)을 만든다. 런타임에는 Aerolib.cs가 이 표를 임베드해서 로드한다. 거대한 조회표를 코드가 아니라 오프라인 생성물로 분리한 깔끔한 설계다.
그 다음, 내 C# 구현을 등록하는 방식이 이 레포에서 가장 우아한 부분이다. ModuleManager.RegisterFromAssembly가 리플렉션으로 [SysAbiExport] 애트리뷰트가 붙은 모든 메서드를 훑어 NID → 함수 디스패치 테이블을 자동으로 만든다:
// PS5 함수 하나를 C#으로 구현 = 애트리뷰트 붙인 메서드 하나 (실제 발췌)
[SysAbiExport(Nid="L-Q3LEjIbgA", ExportName="sceKernelMapDirectMemory",
Target=Generation.Gen4|Generation.Gen5, LibraryName="libKernel")]
public static int KernelMapDirectMemory(CpuContext ctx) {
// System V AMD64 ABI: 인자는 RDI, RSI, RDX, RCX, R8, R9 순서
var inOutAddressPointer = ctx[CpuRegister.Rdi];
var length = ctx[CpuRegister.Rsi];
var protection = (int)ctx[CpuRegister.Rdx];
var flags = ctx[CpuRegister.Rcx];
// ... 게스트 VA 범위를 예약하고 결과를 in/out 포인터로 돌려줌 ...
// 반환값은 RAX에 기록됨
}
이렇게 등록된 함수가 589개. 새 시스템콜을 추가하려면 애트리뷰트 붙인 메서드 하나만 더 쓰면 끝이다. CpuContext는 게스트 레지스터 파일(GPR 16개 + XMM/YMM)을 흉내 낸 마샬링용 그릇으로, 실제 실행은 호스트 레지스터에서 일어나고 이 그릇은 HLE 경계에서만 채워진다.
Core/Cpu/Native/DirectExecutionBackend.cs(4,796줄, partial class로 5조각 분할)가 실제로 게스트 코드를 돌린다. 여기서 C#의 근본 문제와 마주친다: 가비지 컬렉터(GC)가 스택을 훑을 때, 관리형이 아닌 네이티브 게스트 프레임을 만나면 크래시한다는 것.
[UnmanagedCallersOnly]를 붙인다. SharpEmu는 게스트(네이티브)가 HLE(관리형 C#)를 호출해야 하므로 이 다리가 필수다.SharpEmu의 해법은 대담하다: 게스트 코드를 CLR 스레드가 아닌 원시 OS 스레드(CreateThread) 위에서 돌리고, 그 스레드의 실행 루프를 런타임에 손으로 emit한 x86-64 기계어로 만든다. 실제로 sub rsp,0x28 / WaitForSingleObject / call <진입 스텁> / SetEvent 같은 바이트를 코드가 직접 써넣는다. 이렇게 하면 게스트가 도는 동안 그 스레드에는 관리형 프레임이 하나도 없어 GC의 스택 워킹이 건드리지 않는다.
관리형 런타임 위에서 임의의 네이티브 코드를 돌리는 정공법은 극악하게 어렵다. SharpEmu는 정면 대결 대신 "게스트 실행 구간에는 관리형 프레임이 0이 되도록 스레드를 격리"하는 우회를 택했다. NativeWorker 파일 상단 주석이 이 GC/스택워크 제약을 훌륭히 설명한다 — 남의 코드에서 "왜 이렇게까지 했나"의 이유를 읽는 것 자체가 공부다.
게스트가 sceKernelMapDirectMemory를 호출하는 순간을 추적하면 전체 그림이 완성된다. 로더가 미리 심어둔 스텁이 트램펄린을 거쳐 관리형 게이트웨이로 점프한다:
여기 0xCC 0xC3(int3; ret) 스텁이 영리하다. 아직 패치 안 된 함수를 게스트가 부르면 int3가 즉시 폴트를 내 "미구현 함수 호출"이 바로 잡힌다. 구현된 함수는 jmp 트램펄린으로 덮여 관리형 HLE로 우회된다.
Core/Memory/PhysicalVirtualMemory.cs(1,091줄)가 게스트 메모리를 관리한다. 핵심 제약: 게스트 네이티브 코드가 게스트 포인터를 직접 역참조하므로, 게스트 가상주소(VA)에 실제 호스트 메모리를 바로 그 주소에 할당해야 한다. 이걸 아이덴티티 매핑이라 한다 — Win32 VirtualAlloc을 게스트 VA에 그대로 얹는다.
// 게스트 메모리 = 게스트 VA에 그대로 얹은 실제 호스트 메모리
// 정렬된 영역 목록 + 페이지 보호 맵을 RW 락으로 보호
List<MemoryRegion> _regions; // 정렬된 영역
Dictionary<ulong, ProgramHeaderFlags> _pageProtections; // 페이지별 권한
// kernel32: VirtualAlloc / VirtualProtect / VirtualQuery / FlushInstructionCache
// 큰 영역(≥1GiB)은 MEM_RESERVE만, 접근 시 VEH가 지연 커밋(lazy commit)
PS5는 메모리를 두 층으로 본다. SharpEmu도 이를 나눠 모델링한다:
| 층 | PS5 개념 | SharpEmu 구현 |
|---|---|---|
| 물리(직접) 메모리 | Direct Memory — 물리 메모리 풀을 직접 예약 | KernelMemoryCompatExports.cs의 _directAllocations · _nextPhysicalAddress (sceKernelAllocateDirectMemory) |
| 가상 메모리 | 물리 메모리를 가상주소에 매핑 | VirtualAlloc 백킹 VA (sceKernelMapDirectMemory · 로더) |
0x8_0000_0000을 쓰면 호스트도 바로 그 주소에 메모리를 둬야, 게스트 포인터가 그대로 유효해진다. VEH: 게스트 코드가 아직 커밋 안 된 메모리를 건드려 접근 위반(폴트)이 나면, Windows의 벡터드 예외 핸들러가 이를 가로채 그 자리에서 메모리를 지연 할당(lazy commit)하거나 실패를 처리한다. "폴트를 정상 흐름의 일부로 쓰는" 저수준 기법.구조를 읽는 요령: 세 개의 별표(★) 파일이 시스템의 뼈대다 — SelfLoader.cs(파일→메모리), DirectExecutionBackend.cs(코드 실행), PhysicalVirtualMemory.cs(메모리 모델). 그리고 Libs/의 거대한 파일들은 "PS5 OS 함수를 하나씩 채워 넣은" 노가다 영역이라, 처음엔 건너뛰고 뼈대부터 읽는 게 낫다.
| 정독 우선순위 | 파일 | 배우는 것 |
|---|---|---|
| ★★★ | Runtime/SharpEmuRuntime.cs | 전체 부팅 파이프라인 — 큰 그림을 여기서 잡는다 |
| ★★★ | Loader/SelfLoader.cs | ELF/SELF 파싱 · 세그먼트 매핑 · 동적 재배치 |
| ★★★ | HLE/ModuleManager.cs + SysAbiExportAttribute | 애트리뷰트+리플렉션 플러그인 디스패치 |
| ★★ | Cpu/Native/DirectExecutionBackend.NativeWorker.cs | GC 회피 · 원시 스레드 · 리버스 P/Invoke(주석이 명품) |
| ★★ | Memory/PhysicalVirtualMemory.cs | 아이덴티티 매핑 · 지연 커밋 · VEH |
| ★ | scripts/generate_aerolib_binary.py | NID 해싱 알고리즘(소금값+SHA-1+base64) |
에뮬레이션의 근본 선택이다. LLE(저수준)는 실제 펌웨어/하드웨어를 바이트 단위로 재현해 정확하지만 느리고 펌웨어가 필요하다. HLE(고수준)는 "이 함수가 하려던 일"을 새로 구현해 빠르지만, 함수 하나하나를 손으로 채워야 하고 미묘한 동작 차이가 버그가 된다. SharpEmu가 sceKernel* 589개를 HLE로 채우는 걸 보면, 이 트레이드오프가 코드량으로 실감난다. 에뮬·가상화·API 셰임(shim)을 다루는 모든 사람에게 보편적인 개념.
[SysAbiExport]를 붙인 메서드를 리플렉션으로 자동 수집해 디스패치 테이블을 만드는 패턴은, 에뮬레이터를 넘어 플러그인 시스템·명령 핸들러·라우터·직렬화기 어디에나 쓰인다. "선언(애트리뷰트)과 등록(리플렉션)을 분리"하면, 새 기능 추가가 "메서드 하나 더"로 끝난다. C#·Java·Python 데코레이터 모두 같은 발상.
[UnmanagedCallersOnly] · GCHandle · 손수 emit한 트램펄린 · GC 스택워크 제약 — 이건 C# 상급 주제의 집합이다. 대부분의 개발자는 평생 만질 일 없지만, "런타임이 내 발밑에서 무엇을 가정하는가"를 이해하는 데 이만한 사례가 없다. 상호운용(interop)·네이티브 AOT·게임 엔진 바인딩을 하는 사람에게 특히.
이름을 해시로 감추고(소금+SHA-1), 에뮬레이터가 거대한 이름 사전으로 역대응시키는 구조. 역공학·바이너리 분석·심볼 복원의 축소판이다. "왜 그냥 이름을 안 쓰고 해시를 쓰나", "그 해시를 어떻게 되돌리나"를 손으로 재현(실습 참고)하면 리버싱 감각이 크게 는다.
8천 줄 백엔드를 .cs / .Imports / .Exceptions / .NativeWorker / .Diagnostics로 나눈 partial class는, "한 클래스인데 파일은 여러 개"로 관심사를 물리적으로 분리하는 실전 기법. 거대 코드베이스를 항해 가능하게 유지하는 정리술.
README가 못박는다: 지금은 어떤 상용 게임도 플레이할 수 없다. 테스트 타이틀(데몬즈 소울 리메이크 등)은 eboot.bin 로드·일부 커널 함수·초기 그래픽 단계까지만 진행되고, 대부분 sceVideoOut(화면 출력) 이전에서 멈춘다. 게다가 실행 경로가 kernel32 직접 호출·Win64 CONTEXT 구조·MSVC SEH에 묶여 있어 사실상 Windows x64에서만 게스트가 돈다. linux-x64·osx-arm64는 GUI만 빌드될 뿐, ARM 호스트에서는 x86-64 게스트를 직접 실행할 수 없다(번역기가 없으므로) — 이게 직접 실행 전략의 근본 한계다.
임의의 게스트 코드를 직접 실행하려고, SharpEmu는 프로세스 보호(CFG/CET/섀도스택)를 일부러 끄고, 게스트 메모리를 읽기·쓰기·실행(RWX) 모두 가능하게 두며, 실행 중 기계어를 손수 써넣는다. 학습용으로는 훌륭하지만 "안전한 샌드박스"와 정반대다. 신뢰할 수 없는 게임 파일을 이런 에뮬레이터에 던지는 것은 위험할 수 있음을 이해하고 봐야 한다.
| 항목 | 요구사항 |
|---|---|
| 빌드 도구 | .NET SDK 10.0.103 (global.json이 고정, rollForward: disable) |
| 실행 플랫폼(게스트) | 사실상 Windows x86-64 전용 — kernel32 직접 P/Invoke, Win64 CONTEXT, MSVC SEH에 의존 |
| GUI 빌드 대상 | win-x64 · linux-x64 · osx-arm64 (단, ARM 호스트는 게스트 실행 불가) |
| 그래픽 | Vulkan 지원 GPU (Silk.NET.Vulkan 2.23) |
| 게임 파일 | 사용자가 소유한 콘솔에서 직접 덤프한 eboot.bin — 저장소는 어떤 게임/펌웨어도 제공하지 않음 |
| 빌드 방법 | git clone → SharpEmu.slnx 열기 → dotnet build 또는 dotnet publish → artifacts/에 산출 |
| 실행 방법 | .\SharpEmu "eboot.bin" 2>&1 | Tee-Object -FilePath "log.txt" |
현실 점검: "게임을 돌려보고 싶다"는 목적이면 이 프로젝트는 아직 이르다(플레이 불가). 반면 "에뮬레이터가 어떻게 시작 파일을 로드하고 OS 함수를 대체하는지 코드로 보고 싶다"가 목적이면, 지금이 오히려 가장 읽기 좋은 시기다 — 코드가 아직 작고 뼈대가 선명하기 때문.
레포를 git clone --depth 1로 받아 Runtime/SharpEmuRuntime.cs의 Run 메서드를 위에서 아래로 읽으며, 4-0의 ASCII 파이프라인 ①~⑥이 어느 코드 줄에 대응하는지 손으로 표시해보라. "로드 → 모듈 → 초기화 → 진입"의 큰 흐름을 몸에 새기는 연습.
파이썬으로 4-2의 공식을 그대로 짜서, sceKernelMapDirectMemory가 정말 L-Q3LEjIbgA가 나오는지 확인하라. 소금값 518D64A6…을 붙여 SHA-1 → 앞 8바이트 리틀엔디언 → base64(+- 변형). 이름 몇 개를 더 넣어보며 난독화 해싱이 어떻게 작동하는지 감을 잡을 것.
표준 ELF64 실행 파일 하나를 골라(예: 간단한 리눅스 hello), 헤더를 파싱해 PT_LOAD 세그먼트의 오프셋·가상주소·크기를 출력하는 100줄짜리 파서를 짜보라. SharpEmu의 ProgramHeader.cs 구조체를 참고. "파일이 어떻게 메모리 레이아웃을 지시하는가"를 손으로 체험하는 과제.
C#(또는 파이썬 데코레이터)으로, [Syscall("함수이름")] 애트리뷰트가 붙은 메서드를 리플렉션으로 모아 이름 → 함수 딕셔너리를 자동 생성하는 미니 레지스트리를 만들라. 문자열 하나로 핸들러가 호출되면 성공. SharpEmu의 ModuleManager.RegisterFromAssembly를 축소 재현한 것.
C#에서 [UnmanagedCallersOnly] 메서드를 만들고, 그 함수 포인터를 네이티브 코드(간단한 C 함수나 손수 emit한 기계어)가 호출하도록 연결해보라. 네이티브→관리형 호출이 실제로 도는 걸 확인하면, 4-3에서 SharpEmu가 왜 그렇게까지 스레드를 격리했는지 몸으로 이해된다.
| 주차 | 주제 | 학습 내용 |
|---|---|---|
| 1주차 | 실행 파일 & 로더 | ELF64 포맷(헤더·프로그램 헤더·세그먼트), 동적 링킹·재배치(reloc), 가상주소 매핑. SelfLoader.cs·ProgramHeader.cs 읽기 + 과제 3. |
| 2주차 | x86-64 & 메모리 | x86-64 레지스터·System V/Win64 호출 규약, 가상메모리·페이지 보호·폴트 처리, VirtualAlloc/mmap. PhysicalVirtualMemory.cs 정독 + 과제 5. |
| 3주차 | HLE & OS 에뮬레이션 | HLE vs LLE, 시스템콜 인터페이스, NID 심볼 해싱·역해싱, 애트리뷰트+리플렉션 디스패치. ModuleManager.cs·generate_aerolib_binary.py + 과제 2·4. |
| 4주차 | GPU & 그래픽 파이프라인 | GPU 명령 스트림(PM4/AGC 패킷), 셰이더 재컴파일(RDNA→SPIR-V), Vulkan 프레젠테이션. AgcExports.cs·Gen5SpirvTranslator.cs·VulkanVideoPresenter.cs 훑기. |
먼저 실행 파일이 어떻게 메모리에 올라가는지(1주차)를 확실히 잡아라. 그게 되면 "올라간 코드를 어떻게 돌리나 → 그 코드가 OS를 부르면 어쩌나(HLE)"가 자연스럽게 이어진다. GPU/그래픽(4주차)은 가장 무겁고 독립적이라 맨 뒤로 미뤄도 된다 — 뼈대(로더·메모리·HLE)를 먼저 완성하는 게 이 레포의 논리적 독해 순서다.
SharpEmu의 README가 감사 인사를 남긴 세 프로젝트를 곁들여 보면 시야가 넓어진다. ShadPS4(PS4 에뮬, C++)는 완성도 높은 참고서이고, Kyty는 몇 안 되는 PS5 에뮬 선행 사례로 네이티브 코드 직접 실행 아이디어의 원류다. Ryujinx(스위치 에뮬, C#)는 "C#으로 에뮬레이터를 어떻게 짜나"의 모범 — 파일시스템·저수준 C# 패턴을 SharpEmu가 참고했다. 같은 문제를 다른 언어·다른 세대로 푼 사례들을 나란히 놓고 비교하면 설계 감각이 빨리 는다.
sceKernel*을 HLE로 채운다.NativeOnly).eboot.bin. 내부는 리눅스 ELF를 소니가 서명·확장한 SELF 포맷. 로더가 이걸 파싱해 메모리에 펼친다.aerolib.bin)으로 역대응시킨다.ModuleManager가 리플렉션으로 수집해 NID→함수 디스패치 테이블을 자동 생성. 새 함수 추가 = 메서드 하나 더.[UnmanagedCallersOnly]를 붙인다. 게스트(네이티브)→HLE(C#) 연결에 필수.sceKernelAllocateDirectMemory). 이후 sceKernelMapDirectMemory로 가상주소에 매핑한다. 2층 메모리 모델..prx/.sprx). 게임 옆폴더에서 로드하되, libkernel 같은 핵심 모듈은 HLE가 대신하므로 스킵한다.| 구분 | 링크 / 위치 |
|---|---|
| GitHub 저장소 | github.com/par274/sharpemu |
| TrendShift | trendshift.io/repositories/79103 |
| 핵심 파일 | src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs (부팅 오케스트레이션) |
| 핵심 파일 | src/SharpEmu.Core/Loader/SelfLoader.cs (SELF/ELF 로더) |
| 핵심 파일 | src/SharpEmu.HLE/ModuleManager.cs · SysAbiExportAttribute.cs (HLE 디스패치) |
| 핵심 파일 | src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend*.cs (직접 실행) |
| 핵심 파일 | src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs (아이덴티티 매핑) |
| NID 생성기 | scripts/generate_aerolib_binary.py (해싱 알고리즘) |
| 형제 프로젝트 | ShadPS4(PS4·C++) · Kyty(PS5 선행) · Ryujinx(스위치·C#) |
| 연관 개념 | ELF/동적링킹 · x86-64 ABI · HLE 에뮬레이션 · Vulkan/SPIR-V · 리버스 P/Invoke |