TrendShift 딥다이브 · 2026-07-13 · Daily #8

SharpEmu 딥다이브
— PS5 게임을 '해석'하지 않고, 게스트 x86-64 코드를 호스트 CPU에서 그대로 실행하는 C# 브리지

SharpEmu플레이스테이션 5 게임을 PC에서 돌리려는 실험적 에뮬레이터를 순수 C#/.NET 10으로 쓴 프로젝트다. 그런데 이 레포의 진짜 배울 점은 "PS5를 흉내 낸다"가 아니라 애초에 CPU를 흉내 내지 않는다는 설계 결정에 있다. PS5의 CPU는 AMD Zen2 x86-64이고, 이 에뮬레이터를 돌리는 당신의 PC도 x86-64다 — 명령어 집합(ISA)이 같으니 한 줄씩 해석(interpret)할 필요 없이, 게스트 코드를 호스트 메모리에 얹고 진짜 CPU가 직접 실행하게 둔다. 이 문서는 "PS5 에뮬을 쓰는 법"이 아니라, 실행 파일 로더 · OS 함수 대체(HLE) · 네이티브 코드 직접 실행 브리지를 어떻게 설계하는가를 공부하기 위한 자료다 — 에뮬레이터·리버스 엔지니어링·저수준 시스템 프로그래밍의 살아있는 교재. (저장소: par274/sharpemu · C# 99.8% · GPL-2.0 · v0.0.1 · 154 커밋 · TrendShift 오늘 Daily #8)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 하드웨어 · 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

이름부터 뜯어보기 — Sharp(C#) + Emu(emulator) = "샤프 에뮬레이터"

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이고, 이 레포 코드의 대부분을 차지한다.

핵심 용어
HLE (High-Level Emulation, 고수준 에뮬레이션)
콘솔의 운영체제·시스템 라이브러리를 바이트 단위로 똑같이 재현(LLE)하지 않고, "이 함수가 하려던 일"을 호스트 코드로 새로 구현해 가로채는 방식. 예를 들어 게임이 PS5의 sceKernelMapDirectMemory(메모리 매핑)를 호출하면, SharpEmu는 진짜 PS5 커널 대신 자기가 만든 C# 함수로 응답한다. 펌웨어 없이 돌릴 수 있고 빠르지만, 함수 하나하나를 직접 구현해야 해서 손이 많이 간다.
핵심 용어
eboot.bin (게임 실행 파일)
PS4/PS5 게임의 메인 실행 파일. 내부적으로는 SELF(Signed ELF)라는, 리눅스의 ELF 실행 파일 포맷을 소니가 변형·서명한 형태다. SharpEmu가 게임을 구동할 때 가장 먼저 하는 일이 이 eboot.bin을 열어 헤더를 읽고, 코드·데이터 조각(세그먼트)을 메모리에 배치하고, 시작 주소로 점프하는 것이다.

2왜 지금 주목받는가

TrendShift Daily #8 · "PS5 에뮬"이라는 성배 + 이례적인 C# 선택 + 대담한 실행 전략

콘솔 에뮬레이션에서 최신 세대(PS5)는 사실상 성배다. PS4용으로는 완성도 높은 ShadPS4가 있지만, PS5는 아직 제대로 게임이 돌아가는 에뮬레이터가 거의 없다. SharpEmu가 별 5개에 불과한 극초기 프로젝트인데도 TrendShift Daily 상위에 오른 건, "누군가 PS5 에뮬을 진지하게 C#으로 시도한다"는 사실 자체가 X·해커뉴스에서 화제가 됐기 때문이다. (TrendShift의 굵은 숫자는 별 수가 아니라 사회적 언급 모멘텀이다.)

주목 포인트 1 — CPU를 "해석"하지 않는 대담한 전략

대부분의 에뮬레이터는 인터프리터나 JIT로 게스트 명령어를 호스트 명령어로 번역한다. SharpEmu는 PS5도 PC도 x86-64라는 사실을 이용해 그 단계를 통째로 건너뛴다. 게스트 코드를 호스트 메모리에 실행 가능 상태로 얹고, 진짜 CPU가 직접 실행하게 한다. 코드에 실제로 CPU 엔진은 NativeOnly 하나뿐이고, 유명한 x86 디스어셈블러 Iced실행이 아니라 로그·진단용으로만 쓴다. 이 한 가지 결정이 이 레포의 모든 것을 설명한다.

주목 포인트 2 — 콘솔 에뮬을 C#/.NET 10으로

DolphinㆍRPCS3ㆍShadPS4 등 주류 에뮬레이터는 성능을 위해 C++이 정석이다. SharpEmu는 관리형 언어(managed) C#으로 이 영역에 뛰어들었다. 관리형 언어의 가비지 컬렉터(GC)와 네이티브 게스트 코드가 충돌하는 까다로운 문제를, [UnmanagedCallersOnly] · 손수 emit한 기계어 · 원시 OS 스레드로 정면 돌파한다. "C#으로 이런 것까지 되나?"라는 호기심이 개발자들을 끌어들이는 지점.

주목 포인트 3 — 뼈대가 놀랍도록 깔끔하다 (v0.0.1인데도)

고작 154 커밋짜리 v0.0.1이지만, 코드가 인터페이스로 잘 쪼개져(ISelfLoader·IVirtualMemory·IModuleManager…) 있고, HLE 함수를 [SysAbiExport] 애트리뷰트 하나로 등록하는 플러그인 구조, 8천 줄짜리 실행 백엔드를 partial class로 5조각 나눈 정리까지 — 교재로 삼기 좋은 설계 패턴이 곳곳에 있다. "작동하는 PS5 에뮬"로서가 아니라 "에뮬레이터 아키텍처 학습 자료"로서 값지다.

주목 포인트 4 — 명시적 반(反)불법복제 스탠스

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에서는 원천적으로 불가능하고, 게스트 코드가 호스트를 직접 건드리니 보안 격리(샌드박싱)를 스스로 포기해야 한다(뒤에서 자세히).

3기술 스택 전체 지도

.NET 10 · 6개 프로젝트 · Iced 디스어셈블러 · Silk.NET(Vulkan) · Avalonia GUI

SharpEmu는 순수 .NET 솔루션이다. global.json이 SDK를 10.0.103으로 못박고(rollForward: disable), 타깃 프레임워크는 net10.0, nullable·ImplicitUsings 켜짐, 그리고 네 개 프로젝트에서 AllowUnsafeBlocks가 켜져 있다(게스트 메모리를 포인터로 직접 만지기 때문). AOT는 쓰지 않고 JIT + SelfContained + PublishSingleFile로 배포한다.

솔루션 = 6개 프로젝트로 역할 분담

SharpEmu.slnx에 6개 프로젝트가 있고, 의존 방향은 Logging ← HLE ← Libs ← Core ← CLI (GUI는 Logging에만 의존, CLI가 GUI를 호스팅).

프로젝트역할
SharpEmu.CLI진입점(Program.cs). 인자 파싱, 콘솔/UTF-8 설정, 실행 오케스트레이션
SharpEmu.Core심장부 — CPU 디스패처 · 네이티브 실행 백엔드 · SELF 로더 · 메모리 관리 · 런타임
SharpEmu.HLEHLE 계약(인터페이스) + NID 엔진. CpuContext(게스트 레지스터), 애트리뷰트 기반 디스패치 테이블
SharpEmu.LibsPS5 시스템 라이브러리 구현 589개([SysAbiExport] 함수). Kernel·GPU(Agc)·VideoOut·Audio·Pad(듀얼센스)…
SharpEmu.GUIAvalonia 프런트엔드 + ATRAC9 오디오 디코더(MIT) + 컨트롤러 리더
SharpEmu.Logging로그 싱크(Console/File/Composite), LogLevel, 빌드 배너

핵심 NuGet 패키지 — 무엇으로 무엇을 하나

영역패키지 (버전)역할 / 왜 골랐나
x86 디스어셈블러Iced 1.21.0기계어 ↔ 사람이 읽는 어셈블리 변환. 실행이 아니라 진단·로그·패턴 스캔용
그래픽(Vulkan)Silk.NET.Vulkan 2.23.0 (+KHR/EXT/Windowing)PS5 GPU 명령을 번역해 Vulkan으로 화면 출력
GUIAvalonia 11.3.18 (+Desktop/Fluent/Inter)크로스플랫폼 데스크톱 UI(게임 목록·설정)
리눅스 전송Tmds.DBus.Protocol 0.21.3보안 픽스 위해 고정된 전이 의존성
핵심 용어
디스어셈블러 (Disassembler) / Iced
기계어 바이트(48 8B 04 25 …)를 사람이 읽는 어셈블리(mov rax, ...)로 되돌리는 도구. Iced는 .NET에서 가장 널리 쓰이는 x86/x64 디스어셈블러다. 중요한 점: SharpEmu는 이걸로 코드를 실행하지 않는다. 게스트가 뭘 하려는지 사람이 로그로 보고 디버깅하기 위해, 그리고 특정 명령어 패턴(예: TLS 접근)을 찾아 패치하기 위해서만 쓴다.
핵심 용어
Vulkan / Silk.NET
Vulkan은 GPU에 그림을 그리라고 명령하는 저수준 그래픽 API(OpenGL의 현대판). PS5의 GPU는 자체 명령 포맷(AGC/PM4 패킷)과 셰이더를 쓰는데, SharpEmu는 이를 해석해 Vulkan 명령으로 다시 옮긴다. Silk.NET은 C#에서 Vulkan을 호출할 수 있게 해주는 바인딩 라이브러리다.

라이선스 구성도 학습거리다. 프로젝트 전체는 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)은 소니 코드가 아니라 함수 이름↔해시 대응표일 뿐이라 저작권 문제가 없다.

4아키텍처 심화 분석

직접 실행 브리지 · SELF 로더 · NID/HLE 디스패치 · 원시 스레드 + 리버스 P/Invoke · 아이덴티티 메모리 매핑

이 섹션의 첫 문장만 기억해도 이 레포의 90%를 이해한 것이다: SharpEmu는 CPU 에뮬레이터가 아니라 "로더 + HLE + 네이티브 실행 브리지"다. 게스트 PS5 코드는 호스트 x86-64 CPU 위에서 직접 돈다. 그래서 이 프로젝트의 어려움은 "명령어를 어떻게 흉내 내나"가 아니라 "내 코드(관리형 C#)와 게스트 코드(네이티브)를 어떻게 안전하게 오가나"에 집중돼 있다.

4-0. 전체 부팅 흐름 한눈에

SharpEmu.CLI "eboot.bin"을 실행하면 SharpEmuRuntime.Run이 아래 파이프라인을 태운다.

┌──────────────────────────────────────────────────────────────┐ │ 사용자: SharpEmu.CLI "eboot.bin" │ └───────────────────────────┬──────────────────────────────────┘ ▼ ① CLI 준비 (Program.cs) · 콘솔 붙이기 + UTF-8 고정 · 자기 자신을 "완화정책 끈 자식 프로세스"로 재실행 (CFG/CET/섀도스택 OFF → 게스트 코드 자유 실행 위해) ▼ ② 객체 그래프 조립 (SharpEmuRuntime.CreateDefault) ModuleManager ← RegisterFromAssembly(Libs, Kernel) [589개 함수] + PhysicalVirtualMemory + SelfLoader + CpuDispatcher ▼ ③ 이미지 로드 (SelfLoader.Load) · SELF/ELF 헤더 파싱 → PT_LOAD 세그먼트를 게스트 VA에 매핑 · 동적 테이블 + 재배치(reloc) 처리 → import 스텁 생성 · param.json 읽어 Title / TitleId / Version 취득 ▼ ④ 모듈 로드 (LoadAdjacentSceModules) · 옆폴더 sce_module/*.prx 로드 (libkernel*은 HLE가 대체하므로 스킵) · 세대 판정: ELF AbiVersion==2 ? Gen5 : Gen4 ▼ ⑤ 초기화 실행 → 진입 (CpuDispatcher.DispatchEntry) · 게스트 스택 + TLS 매핑, "호스트로 복귀" 센티넬 스텁 배치 · Orbis(FreeBSD) 프로세스 진입 프레임 구성 (RDI=진입인자) · Rip=진입점 → DirectExecutionBackend.TryExecute ▼ ⑥ 게스트 코드가 "원시 OS 스레드" 위에서 네이티브로 실행 · 라이브러리 호출 → 트램펄린 → 관리형 HLE 함수로 우회 · TLS 접근 명령 → 패치, CPU 폴트 → VEH로 포착

4-1. SELF/ELF 로더 — 게임 실행 파일을 메모리에 펼치기

Core/Loader/SelfLoader.cs(2,421줄)가 담당한다. PS5 실행 파일은 리눅스 ELF의 소니 변형(SELF)이다. 로더의 일:

핵심 용어
ELF / SELF / 세그먼트 매핑
ELF는 리눅스의 표준 실행 파일 포맷. 헤더가 "이 코드 조각을 메모리 어느 주소에 올려라"를 프로그램 헤더(Phdr)로 지시한다. SELF는 여기에 소니가 서명·확장을 붙인 것. 세그먼트 매핑이란 파일 속 조각들을 지시된 가상주소(VA)에 실제로 배치하는 작업이다. 로더의 본질은 "파일 → 실행 가능한 메모리 레이아웃" 변환기.

4-2. NID 해싱 + HLE 디스패치 — "집주인 대역"을 등록하는 법

게임이 sceKernelMapDirectMemory 같은 시스템 함수를 부르면, 이걸 SharpEmu의 C# 구현으로 연결해야 한다. 그런데 PS5 실행 파일에는 함수 이름이 아니라 NID라는 해시가 들어있다. 그래서 두 가지가 필요하다: (1) NID↔이름 대응표, (2) 이름으로 등록된 내 C# 함수를 찾는 디스패치 테이블.

핵심 용어
NID (Name ID, 심볼 해시)
소니가 함수 이름을 감추려고 쓰는 방식. 함수 이름 뒤에 고정 소금값을 붙여 SHA-1 해시한 뒤 앞 8바이트를 base64로 인코딩한 11글자 문자열이 NID다. 실행 파일에는 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.txt148,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 경계에서만 채워진다.

4-3. 직접 실행 백엔드 — 관리형과 네이티브의 위험한 악수

Core/Cpu/Native/DirectExecutionBackend.cs(4,796줄, partial class로 5조각 분할)가 실제로 게스트 코드를 돌린다. 여기서 C#의 근본 문제와 마주친다: 가비지 컬렉터(GC)가 스택을 훑을 때, 관리형이 아닌 네이티브 게스트 프레임을 만나면 크래시한다는 것.

핵심 용어
리버스 P/Invoke · [UnmanagedCallersOnly]
보통 C#이 네이티브 함수를 부르는 걸 P/Invoke라 한다. 반대로 네이티브 코드가 C# 함수를 부르는 것이 리버스 P/Invoke이고, 그 대상 메서드에 [UnmanagedCallersOnly]를 붙인다. SharpEmu는 게스트(네이티브)가 HLE(관리형 C#)를 호출해야 하므로 이 다리가 필수다.

SharpEmu의 해법은 대담하다: 게스트 코드를 CLR 스레드가 아닌 원시 OS 스레드(CreateThread) 위에서 돌리고, 그 스레드의 실행 루프를 런타임에 손으로 emit한 x86-64 기계어로 만든다. 실제로 sub rsp,0x28 / WaitForSingleObject / call <진입 스텁> / SetEvent 같은 바이트를 코드가 직접 써넣는다. 이렇게 하면 게스트가 도는 동안 그 스레드에는 관리형 프레임이 하나도 없어 GC의 스택 워킹이 건드리지 않는다.

배울 점 — 문제를 "회피"하는 설계
GC와 싸우지 않고, GC가 볼 수 없는 곳으로 게스트를 옮긴다

관리형 런타임 위에서 임의의 네이티브 코드를 돌리는 정공법은 극악하게 어렵다. SharpEmu는 정면 대결 대신 "게스트 실행 구간에는 관리형 프레임이 0이 되도록 스레드를 격리"하는 우회를 택했다. NativeWorker 파일 상단 주석이 이 GC/스택워크 제약을 훌륭히 설명한다 — 남의 코드에서 "왜 이렇게까지 했나"의 이유를 읽는 것 자체가 공부다.

4-4. 라이브러리 호출이 C#에 도달하기까지 (import 브리징)

게스트가 sceKernelMapDirectMemory를 호출하는 순간을 추적하면 전체 그림이 완성된다. 로더가 미리 심어둔 스텁이 트램펄린을 거쳐 관리형 게이트웨이로 점프한다:

[로더] JUMP_SLOT 재배치가 NID를 가리킴 → 스텁 슬롯 할당(0x0000_7000_0000_0000+), 바이트 [0xCC 0xC3](int3;ret) 기록 → importStubs[주소] = NID 로 기억, GOT를 이 스텁 주소로 패치 [백엔드 TryExecute] SetupImportStubs → CreateImportHandlerTrampoline: 네이티브 코드 emit (RDI..R15 + XMM + 스택을 argPack으로 포장 → ImportDispatchGateway 호출) → PatchImportStub: 슬롯을 "movabs rax,<트램펄린> ; jmp rax"로 덮어씀 [런타임] 게스트가 import 호출 게스트 → 스텁 → 트램펄린 → ImportDispatchGatewayManaged(핸들, index, argPackPtr) → argPack을 풀어 CpuContext 레지스터에 채움 → ModuleManager.TryDispatch(NID, ctx) ← [SysAbiExport] 메서드 실행 → 반환값을 게스트 RAX에 써넣고 게스트로 복귀

여기 0xCC 0xC3(int3; ret) 스텁이 영리하다. 아직 패치 안 된 함수를 게스트가 부르면 int3즉시 폴트를 내 "미구현 함수 호출"이 바로 잡힌다. 구현된 함수는 jmp 트램펄린으로 덮여 관리형 HLE로 우회된다.

4-5. 메모리 관리 — "아이덴티티 매핑"이 강제되는 이유

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 · 로더)
핵심 용어
아이덴티티 매핑 · VEH(벡터드 예외 핸들러)
아이덴티티 매핑: 게스트가 주소 0x8_0000_0000을 쓰면 호스트도 바로 그 주소에 메모리를 둬야, 게스트 포인터가 그대로 유효해진다. VEH: 게스트 코드가 아직 커밋 안 된 메모리를 건드려 접근 위반(폴트)이 나면, Windows의 벡터드 예외 핸들러가 이를 가로채 그 자리에서 메모리를 지연 할당(lazy commit)하거나 실패를 처리한다. "폴트를 정상 흐름의 일부로 쓰는" 저수준 기법.

5디렉토리 구조 해부

src/ 아래 ~7.4만 줄 C# — "어디에 뭐가 있나"
sharpemu/ ├── global.json # .NET SDK 10.0.103 고정 ├── Directory.Build.props # net10.0 · nullable · unsafe ├── Directory.Packages.props # NuGet 중앙 버전 관리(Iced·Silk.NET·Avalonia) ├── SharpEmu.slnx # 6개 프로젝트 솔루션 ├── REUSE.toml + LICENSES/ # SPDX/REUSE 라이선스 규격 ├── scripts/ │ └── generate_aerolib_binary.py # NID 해시 계산 → aerolib.bin 생성 └── src/ ├── SharpEmu.CLI/Program.cs (831) # 진입점·인자파싱·완화정책 자식프로세스 ├── SharpEmu.Logging/ # Console/File/Composite 로그 싱크 ├── SharpEmu.HLE/ │ ├── CpuContext.cs (298) # 게스트 레지스터(GPR16+XMM/YMM) 그릇 │ ├── CpuRegister.cs # enum Rax..R15 │ ├── ModuleManager.cs (239) # 리플렉션→NID 디스패치 테이블 │ ├── SysAbiExportAttribute.cs # HLE 함수 등록용 애트리뷰트 │ └── Aerolib/Aerolib.cs + aerolib.bin # 12.3MB NID↔이름 표(148,678개) ├── SharpEmu.Core/ │ ├── Cpu/ │ │ ├── CpuDispatcher.cs (665) # 스택/TLS/진입프레임 구성→백엔드 호출 │ │ ├── Disasm/IcedDecoder.cs (213) # Iced 래퍼(진단 전용) │ │ └── Native/ │ │ ├── DirectExecutionBackend.cs (4796) # ★ 네이티브 실행 심장부 │ │ ├── DirectExecutionBackend.Imports.cs (1584) # import→HLE 우회 │ │ ├── DirectExecutionBackend.Exceptions.cs (1167)# Win32 VEH 폴트 처리 │ │ └── DirectExecutionBackend.NativeWorker.cs (582)# 원시 OS 스레드 워커 │ ├── Loader/SelfLoader.cs (2421) # ★ SELF/ELF 파싱·매핑·재배치 │ ├── Memory/PhysicalVirtualMemory.cs (1091) # ★ 아이덴티티 매핑 게스트 메모리 │ └── Runtime/SharpEmuRuntime.cs (1097) # 전체 부팅 오케스트레이션 ├── SharpEmu.Libs/ # PS5 시스템 라이브러리 589개 구현 │ ├── Kernel/ KernelMemoryCompatExports.cs (6644) # 메모리/스레드/이벤트/세마포어 │ ├── Agc/ AgcExports.cs (5860) + Gen5SpirvTranslator.cs (2569) # GPU→SPIR-V │ ├── VideoOut/ VulkanVideoPresenter.cs (6770) # 화면 출력(Vulkan) │ ├── Audio/ · Pad/(듀얼센스) · Np/ · Network/ · SaveData/ · PlayGo/ ... │ └── (Fiber·Ngs2·AvPlayer·Ime·Rtc·UserService … 수십 개 라이브러리) └── SharpEmu.GUI/ # Avalonia UI + Atrac9 디코더(MIT)

구조를 읽는 요령: 세 개의 별표(★) 파일이 시스템의 뼈대다 — SelfLoader.cs(파일→메모리), DirectExecutionBackend.cs(코드 실행), PhysicalVirtualMemory.cs(메모리 모델). 그리고 Libs/의 거대한 파일들은 "PS5 OS 함수를 하나씩 채워 넣은" 노가다 영역이라, 처음엔 건너뛰고 뼈대부터 읽는 게 낫다.

정독 우선순위파일배우는 것
★★★Runtime/SharpEmuRuntime.cs전체 부팅 파이프라인 — 큰 그림을 여기서 잡는다
★★★Loader/SelfLoader.csELF/SELF 파싱 · 세그먼트 매핑 · 동적 재배치
★★★HLE/ModuleManager.cs + SysAbiExportAttribute애트리뷰트+리플렉션 플러그인 디스패치
★★Cpu/Native/DirectExecutionBackend.NativeWorker.csGC 회피 · 원시 스레드 · 리버스 P/Invoke(주석이 명품)
★★Memory/PhysicalVirtualMemory.cs아이덴티티 매핑 · 지연 커밋 · VEH
scripts/generate_aerolib_binary.pyNID 해싱 알고리즘(소금값+SHA-1+base64)

6학습 포인트

이 레포에서 무엇을 훔쳐 배울 것인가 — 기술별로

HLE vs LLE — "얼마나 정확히 흉내 낼 것인가"의 스펙트럼

에뮬레이션의 근본 선택이다. LLE(저수준)는 실제 펌웨어/하드웨어를 바이트 단위로 재현해 정확하지만 느리고 펌웨어가 필요하다. HLE(고수준)는 "이 함수가 하려던 일"을 새로 구현해 빠르지만, 함수 하나하나를 손으로 채워야 하고 미묘한 동작 차이가 버그가 된다. SharpEmu가 sceKernel* 589개를 HLE로 채우는 걸 보면, 이 트레이드오프가 코드량으로 실감난다. 에뮬·가상화·API 셰임(shim)을 다루는 모든 사람에게 보편적인 개념.

애트리뷰트 + 리플렉션 플러그인 레지스트리

[SysAbiExport]를 붙인 메서드를 리플렉션으로 자동 수집해 디스패치 테이블을 만드는 패턴은, 에뮬레이터를 넘어 플러그인 시스템·명령 핸들러·라우터·직렬화기 어디에나 쓰인다. "선언(애트리뷰트)과 등록(리플렉션)을 분리"하면, 새 기능 추가가 "메서드 하나 더"로 끝난다. C#·Java·Python 데코레이터 모두 같은 발상.

관리형 런타임에서 네이티브 코드 다루기 (고급)

[UnmanagedCallersOnly] · GCHandle · 손수 emit한 트램펄린 · GC 스택워크 제약 — 이건 C# 상급 주제의 집합이다. 대부분의 개발자는 평생 만질 일 없지만, "런타임이 내 발밑에서 무엇을 가정하는가"를 이해하는 데 이만한 사례가 없다. 상호운용(interop)·네이티브 AOT·게임 엔진 바인딩을 하는 사람에게 특히.

심볼 난독화와 역해싱 (NID)

이름을 해시로 감추고(소금+SHA-1), 에뮬레이터가 거대한 이름 사전으로 역대응시키는 구조. 역공학·바이너리 분석·심볼 복원의 축소판이다. "왜 그냥 이름을 안 쓰고 해시를 쓰나", "그 해시를 어떻게 되돌리나"를 손으로 재현(실습 참고)하면 리버싱 감각이 크게 는다.

거대한 파일을 다루는 법 — partial class 분할

8천 줄 백엔드를 .cs / .Imports / .Exceptions / .NativeWorker / .Diagnostics로 나눈 partial class는, "한 클래스인데 파일은 여러 개"로 관심사를 물리적으로 분리하는 실전 기법. 거대 코드베이스를 항해 가능하게 유지하는 정리술.

주의 — 이건 "돌아가는 PS5 에뮬"이 아니다
v0.0.1 · 게임 플레이 불가 · 근본적으로 Windows/x86-64 전용

README가 못박는다: 지금은 어떤 상용 게임도 플레이할 수 없다. 테스트 타이틀(데몬즈 소울 리메이크 등)은 eboot.bin 로드·일부 커널 함수·초기 그래픽 단계까지만 진행되고, 대부분 sceVideoOut(화면 출력) 이전에서 멈춘다. 게다가 실행 경로가 kernel32 직접 호출·Win64 CONTEXT 구조·MSVC SEH에 묶여 있어 사실상 Windows x64에서만 게스트가 돈다. linux-x64·osx-arm64는 GUI만 빌드될 뿐, ARM 호스트에서는 x86-64 게스트를 직접 실행할 수 없다(번역기가 없으므로) — 이게 직접 실행 전략의 근본 한계다.

주의 — 안전 격리를 스스로 포기한다
CFG/CET 완화정책 OFF · RWX 메모리 · 자기수정 코드

임의의 게스트 코드를 직접 실행하려고, SharpEmu는 프로세스 보호(CFG/CET/섀도스택)를 일부러 끄고, 게스트 메모리를 읽기·쓰기·실행(RWX) 모두 가능하게 두며, 실행 중 기계어를 손수 써넣는다. 학습용으로는 훌륭하지만 "안전한 샌드박스"와 정반대다. 신뢰할 수 없는 게임 파일을 이런 에뮬레이터에 던지는 것은 위험할 수 있음을 이해하고 봐야 한다.

7하드웨어 · 시스템 요구사항

.NET 10 SDK · 사실상 Windows x64 · 합법 덤프 필수
항목요구사항
빌드 도구.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 cloneSharpEmu.slnx 열기 → dotnet build 또는 dotnet publishartifacts/에 산출
실행 방법.\SharpEmu "eboot.bin" 2>&1 | Tee-Object -FilePath "log.txt"
주의 — 완화정책 자식 프로세스
실행 시 자기 자신을 다시 띄운다
CLI는 시작할 때 보안 완화정책(CFG/CET/섀도스택)을 끈 자식 프로세스로 자신을 재실행한다. 게스트 코드를 자유롭게 실행하기 위한 조치지만, 부작용으로 프로세스가 두 번 뜨는 것처럼 보일 수 있다. 이것이 정상 동작임을 알고 봐야 로그를 헷갈리지 않는다.

현실 점검: "게임을 돌려보고 싶다"는 목적이면 이 프로젝트는 아직 이르다(플레이 불가). 반면 "에뮬레이터가 어떻게 시작 파일을 로드하고 OS 함수를 대체하는지 코드로 보고 싶다"가 목적이면, 지금이 오히려 가장 읽기 좋은 시기다 — 코드가 아직 작고 뼈대가 선명하기 때문.

8직접 해볼 수 있는 실습 과제

SharpEmu를 "쓰는" 것을 넘어, 그 조각들을 "재현"해 보는 과제들
난이도 ★ — 읽기 실습

1. 부팅 파이프라인 코드로 따라가기

레포를 git clone --depth 1로 받아 Runtime/SharpEmuRuntime.csRun 메서드를 위에서 아래로 읽으며, 4-0의 ASCII 파이프라인 ①~⑥이 어느 코드 줄에 대응하는지 손으로 표시해보라. "로드 → 모듈 → 초기화 → 진입"의 큰 흐름을 몸에 새기는 연습.

난이도 ★★ — 알고리즘 재현

2. NID 해시 직접 계산해보기

파이썬으로 4-2의 공식을 그대로 짜서, sceKernelMapDirectMemory가 정말 L-Q3LEjIbgA가 나오는지 확인하라. 소금값 518D64A6…을 붙여 SHA-1 → 앞 8바이트 리틀엔디언 → base64(+- 변형). 이름 몇 개를 더 넣어보며 난독화 해싱이 어떻게 작동하는지 감을 잡을 것.

난이도 ★★ — 미니 구현

3. 최소 ELF 로더 만들기

표준 ELF64 실행 파일 하나를 골라(예: 간단한 리눅스 hello), 헤더를 파싱해 PT_LOAD 세그먼트의 오프셋·가상주소·크기를 출력하는 100줄짜리 파서를 짜보라. SharpEmu의 ProgramHeader.cs 구조체를 참고. "파일이 어떻게 메모리 레이아웃을 지시하는가"를 손으로 체험하는 과제.

난이도 ★★★ — 설계 패턴 재현

4. 애트리뷰트 기반 디스패치 테이블

C#(또는 파이썬 데코레이터)으로, [Syscall("함수이름")] 애트리뷰트가 붙은 메서드를 리플렉션으로 모아 이름 → 함수 딕셔너리를 자동 생성하는 미니 레지스트리를 만들라. 문자열 하나로 핸들러가 호출되면 성공. SharpEmu의 ModuleManager.RegisterFromAssembly를 축소 재현한 것.

난이도 ★★★ — 상호운용 실험

5. 리버스 P/Invoke 맛보기

C#에서 [UnmanagedCallersOnly] 메서드를 만들고, 그 함수 포인터를 네이티브 코드(간단한 C 함수나 손수 emit한 기계어)가 호출하도록 연결해보라. 네이티브→관리형 호출이 실제로 도는 걸 확인하면, 4-3에서 SharpEmu가 왜 그렇게까지 스레드를 격리했는지 몸으로 이해된다.

9관련 기술 심화 학습 로드맵

SharpEmu를 완전히 소화하기 위한 4주 코스
주차주제학습 내용
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 훑기.
학습 순서 팁
"로더 → 메모리 → HLE → GPU" 순으로

먼저 실행 파일이 어떻게 메모리에 올라가는지(1주차)를 확실히 잡아라. 그게 되면 "올라간 코드를 어떻게 돌리나 → 그 코드가 OS를 부르면 어쩌나(HLE)"가 자연스럽게 이어진다. GPU/그래픽(4주차)은 가장 무겁고 독립적이라 맨 뒤로 미뤄도 된다 — 뼈대(로더·메모리·HLE)를 먼저 완성하는 게 이 레포의 논리적 독해 순서다.

비교하며 배우기 — 형제 프로젝트들

SharpEmu의 README가 감사 인사를 남긴 세 프로젝트를 곁들여 보면 시야가 넓어진다. ShadPS4(PS4 에뮬, C++)는 완성도 높은 참고서이고, Kyty는 몇 안 되는 PS5 에뮬 선행 사례로 네이티브 코드 직접 실행 아이디어의 원류다. Ryujinx(스위치 에뮬, C#)는 "C#으로 에뮬레이터를 어떻게 짜나"의 모범 — 파일시스템·저수준 C# 패턴을 SharpEmu가 참고했다. 같은 문제를 다른 언어·다른 세대로 푼 사례들을 나란히 놓고 비교하면 설계 감각이 빨리 는다.

10핵심 키워드 사전

문서에 나온 용어를 한눈에
용어
HLE / LLE
High/Low-Level Emulation. HLE는 OS 함수를 "결과만 맞게" 새로 구현(빠름·펌웨어 불요), LLE는 실제 펌웨어를 바이트 단위로 실행(정확·느림). SharpEmu는 sceKernel*을 HLE로 채운다.
용어
직접 실행(Native Execution)
게스트 코드를 번역하지 않고 호스트 CPU가 그대로 실행. PS5도 PC도 x86-64라 가능. SharpEmu의 단 하나뿐인 CPU 엔진(NativeOnly).
용어
eboot.bin / SELF / ELF
PS5 게임 메인 실행 파일이 eboot.bin. 내부는 리눅스 ELF를 소니가 서명·확장한 SELF 포맷. 로더가 이걸 파싱해 메모리에 펼친다.
용어
NID (심볼 해시)
함수 이름을 소금+SHA-1로 해시한 11글자 문자열. 실행 파일엔 이름 대신 NID만 있어, 에뮬레이터가 이름 사전(aerolib.bin)으로 역대응시킨다.
용어
SysAbiExport (애트리뷰트)
HLE 함수를 등록하는 C# 애트리뷰트. ModuleManager가 리플렉션으로 수집해 NID→함수 디스패치 테이블을 자동 생성. 새 함수 추가 = 메서드 하나 더.
용어
리버스 P/Invoke · UnmanagedCallersOnly
네이티브 코드가 관리형 C# 함수를 호출하는 다리. 대상 메서드에 [UnmanagedCallersOnly]를 붙인다. 게스트(네이티브)→HLE(C#) 연결에 필수.
용어
아이덴티티 매핑
게스트 가상주소에 실제 호스트 메모리를 "바로 그 주소"에 할당. 게스트 포인터가 그대로 유효해야 직접 실행이 되기 때문에 강제된다.
용어
VEH (벡터드 예외 핸들러)
Windows에서 예외/폴트를 가로채는 핸들러. 게스트가 미커밋 메모리를 건드려 접근 위반이 나면 그 자리에서 지연 할당하거나 오류를 처리한다.
용어
Direct Memory (직접 메모리)
PS5의 물리 메모리 풀을 직접 예약하는 개념(sceKernelAllocateDirectMemory). 이후 sceKernelMapDirectMemory로 가상주소에 매핑한다. 2층 메모리 모델.
용어
AGC / SPIR-V
AGC는 PS5 GPU의 명령/셰이더 포맷. SPIR-V는 Vulkan의 중간 셰이더 표현. SharpEmu는 PS5 셰이더(RDNA)를 SPIR-V로 재컴파일해 Vulkan으로 그린다.
용어
Iced
.NET용 x86/x64 디스어셈블러. SharpEmu에서는 실행이 아니라 진단·로그·명령어 패턴 스캔에만 쓴다.
용어
prx / sys_module
PS5의 시스템 라이브러리 모듈 파일(.prx/.sprx). 게임 옆폴더에서 로드하되, libkernel 같은 핵심 모듈은 HLE가 대신하므로 스킵한다.

11참고 링크

공식 소스와 함께 볼 자료
구분링크 / 위치
GitHub 저장소github.com/par274/sharpemu
TrendShifttrendshift.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