How SharpEmu Import Stub Resolution Works: PlayStation NID Handling Explained

SharpEmu resolves imported PlayStation NIDs by creating trap-based import stubs in a dedicated virtual address range, then patching ELF relocations to reference these stubs until the host implements the actual function.

The par274/sharpemu project implements a sophisticated import stub resolution system that bridges the gap between encrypted PlayStation binaries and host execution. When loading a decrypted SELF/ELF image, SharpEmu parses the dynamic section to build relocation descriptors, then generates safe import stubs for unresolved symbols. This mechanism isolates external dependencies while allowing the host to deterministically trap and later resolve native PlayStation function calls.

The Import Stub Resolution Workflow

SharpEmu's resolution process follows a strict eight-step pipeline that transforms raw ELF relocations into executable import stubs.

Locating the Dynamic Segment

The loader begins by identifying the ProgramHeaderType.Dynamic segment in the loaded image. In SelfLoader.cs, the parser reads the dynamic tables—including the string table, symbol table, and RELA/JMPREL tables—to collect all relocations that require external symbol resolution.

Extracting PlayStation NIDs

For each relocation entry that references an imported symbol, SharpEmu extracts the NID (Name Identifier) from the symbol name using the ExtractNid method. The system builds an ordered list of unique NIDs (orderedImportNids) to ensure each imported function receives exactly one stub allocation.

Determining Stub Requirements

Not every NID automatically receives a stub. The ShouldCreateImportStub method evaluates each symbol against two criteria:

  • Non-weak relocations: Always require a stub.
  • Weak relocations: Only receive a stub if IModuleManager.TryGetExport can resolve the symbol to a host implementation.

This selective approach prevents unnecessary memory allocation for symbols that may never be invoked.

Allocating Stub Memory

Approved stubs are allocated in a dedicated virtual address range starting at ImportStubBaseAddress (0x0000_7000_0000_0000). The memory layout uses a fixed stride of ImportStubAddressStride (0x1000_00) with each stub occupying ImportStubSlotSize (0x10) bytes. This sparse allocation scheme prevents accidental execution flow between stubs while maintaining predictable addresses.

Writing Import Stub Bytes

Each stub consists of a minimal trap sequence: a single int 3 instruction (0xCC) followed by a ret (0xC3). This 0xCC … 0xC3 pattern ensures that if guest code jumps to the stub before the host provides a real implementation, the emulator traps safely rather than executing garbage instructions. The stub addresses are stored in the stubsByAddress dictionary for fast lookup.

Patching Relocations

The final phase patches the original ELF relocations to point at the newly created stubs. In ComputeRelocationValue, when processing a relocation that references an imported NID, the loader substitutes the stub address as the symbol value. The result is written back into guest memory via TryWriteRelocationValue. Special handling exists for R_X86_64_TLS_MODID relocations, which patch the TLS module identifier directly, while unsupported types like R_X86_64_COPY trigger exceptions indicating they require full runtime linker support.

Key Source Files and Implementation Details

The import stub system spans several core files in the repository:

Practical Example: Hooking NIDs at Runtime

The following example demonstrates loading a PlayStation binary and interacting with the import stub system:

// Load an ELF image (simplified)
var image = File.ReadAllBytes("eboot.bin");
var vm    = new PhysicalVirtualMemory();
var loader = new SelfLoader();
SelfImage loaded = loader.Load(image, vm, moduleManager);

// Access the import stub mapping after loading
IReadOnlyDictionary<ulong, string> importStubs = loaded.ImportStubs;

// Find the stub address for a specific NID
ulong stubAddr = importStubs.First(kv => kv.Value == "sceKernelOpen").Key;
Console.WriteLine($"Stub for sceKernelOpen → 0x{stubAddr:X}");

// Register a host implementation to replace the stub
moduleManager.RegisterExport("sceKernelOpen", addr =>
{
    // Called when guest execution reaches the stub
    Console.WriteLine($"Guest called sceKernelOpen @ 0x{addr:X}");
    return 0; // Return success to guest
});

This pattern allows developers to implement PlayStation APIs incrementally, with unimplemented functions safely trapping at their stub addresses until host support is added.

Summary

  • SharpEmu creates import stubs in the virtual address range 0x0000_7000_0000_0000 to handle unresolved PlayStation NIDs.
  • Each stub contains a trap sequence (0xCC 0xC3) that safely halts execution if called before the host provides an implementation.
  • The ShouldCreateImportStub logic allocates stubs only for non-weak relocations or resolvable weak symbols.
  • Relocation patching substitutes stub addresses for symbol values via ComputeRelocationValue and TryWriteRelocationValue.
  • The system supports TLS-specific relocations (R_X86_64_TLS_MODID) while rejecting unsupported types like R_X86_64_COPY.

Frequently Asked Questions

What is a PlayStation NID?

A NID (Name Identifier) is a 64-bit hash used by PlayStation binaries to reference imported system functions. SharpEmu extracts these NIDs from ELF symbol names during the loading process, then uses them to look up human-readable names via the Aerolib database and to create corresponding import stubs.

Why does SharpEmu use int 3 (0xCC) for import stubs?

The int 3 instruction triggers a deterministic trap that the emulator can catch immediately. If guest code calls an unimplemented import, execution hits the 0xCC byte first, allowing the host to intercept the call, log the missing implementation, or redirect to a debugger. The following 0xC3 (ret) provides a safe fallback boundary.

How does the host resolve a stub to a real implementation?

The host implements IHostSymbolResolver and registers concrete functions through IModuleManager.RegisterExport. When the guest reaches a stub, the emulator checks IModuleManager.TryGetExport using the NID as a key. If a host implementation exists, the emulator redirects execution to the host function; otherwise, the trap remains active.

What happens when a relocation type is unsupported?

Unsupported relocation types trigger warnings during the loading phase. For types that require complex runtime linker support—such as R_X86_64_COPY, which copies data between segments—the loader throws an exception indicating that the relocation cannot be resolved statically. This prevents silent memory corruption and alerts developers to implement the specific relocation handling in SelfLoader.cs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →