Why NIDs Make the Loader Boundary Work

The relinker has already reconstructed the executable's imports and relocations, but a host loader still needs a stable symbol identity to resolve each call. AnyPS5 uses NIDs for that identity. This chapter explains what a NID represents, why changing an export name requires repairs across several binary tables, and why a successful NID binding is only the beginning of compatibility. The practical result is a way to distinguish “the loader found an address” from “the title received the right function.”

NIDs are the symbol identity layer

A human-readable function name is useful to a developer, but it is not the name that AnyPS5's native PRX boundary exposes to the converted title. The project uses compact NIDs—short identifiers derived from the export name—as the loader-facing names. The title's relocation refers to that identifier, and the patched native library publishes an export with the same identifier. The host loader can then perform an ordinary symbol lookup even though the surrounding executable and library formats have been adapted.

In the current implementation, the NID calculation is deterministic. It takes the symbol name, appends a fixed 16-byte suffix, hashes the resulting bytes with SHA-1, reverses the first eight digest bytes for the encoding step, and represents the result with an 11-character alphabet containing upper- and lower-case letters, digits, plus, and minus. The important property is not that the algorithm is mysterious. It is that the same input name produces the same loader-facing identity wherever the project computes it.

That determinism has two sides. It lets a title's import and a native library's export meet without sharing the original human-readable label, but it also makes every spelling decision part of the ABI. A changed case, suffix, or symbol variant is not editorial cleanup; it can produce a different identifier and therefore a different lookup. The project keeps the calculation in a small shared component so the relinker, the library patcher, and any direct export declaration can use one convention instead of maintaining several local approximations.

The compact representation should not be mistaken for compression that loses meaning. The NID is not the whole function contract; it is the key by which the loader finds the function's address. The information that the title needs at call time—calling convention, argument layout, pointed-to structures, return values, and state changes—still lives in the binary and in the native implementation. A stable key makes the lookup repeatable. It does not make an unknown function known.

The implementation's interface accepts a library-name parameter, but the shown calculation uses the symbol name and fixed suffix as its hash input. That means the source supports a name-derived contract, not an assumption that the library name independently changes the NID domain. This distinction matters when explaining the system: the library relationship is carried through import and export metadata, while the displayed calculation itself does not justify claiming that two libraries necessarily receive different NIDs for the same function name.

A NID is therefore an ABI identity, not a security token and not a semantic proof. It answers a narrow question: which exported address should satisfy this binary reference? It does not answer whether the function's parameters, structure layouts, return codes, side effects, or error precedence match what the title expects. AnyPS5's architecture can make the first question reliable while still recording uncertainty about the second.

sequenceDiagram participant T as Title relocation participant R as Relinker NID reference participant P as Patched PRX export participant L as Host dynamic loader participant I as Native implementation T->>R: Record symbol and relocation R->>P: Compute or expect NID name P-->>L: Publish NID-named export L->>P: Resolve imported NID P-->>L: Return native address L-->>T: Bind relocation to address T->>I: Invoke through bound address

The call remains a normal loader-mediated call. AnyPS5 does not need a special dispatch table for every title call if the converted executable's relocation metadata and the native library's export table agree. The relinker establishes the reference; the patched library supplies the address; the host loader joins them.

Why changing a name reaches deep into a binary

On ELF, an exported symbol's name is not stored as an isolated label next to the function body. The dynamic symbol table points into a dynamic string table. Dynamic entries and version-needed records can also contain offsets into that string table. A GNU hash table accelerates lookups and depends on the order and names of dynamic symbols. Relocation records refer to symbol indexes. Renaming an export without updating those connected structures would leave the loader with stale offsets, stale hashes, or relocation indexes that identify the wrong symbol.

AnyPS5's ELF NID patcher therefore performs a coordinated rewrite. It finds the dynamic symbol and string sections, identifies patchable exported symbols, resolves their NIDs, and builds a replacement string table that preserves names the binary still needs for dependencies and version metadata. It remaps dynamic and version-needed offsets to the new strings. When a GNU hash table is present, it can reorder the movable dynamic symbols, repair relocation symbol indexes according to the new order, and rebuild the hash bloom data, buckets, and chains.

That work explains why an export rename is a binary compatibility operation rather than a text substitution. The function's machine code can remain byte-for-byte unchanged while the tables around it change. The host loader cares about those tables because they are how it discovers a symbol and matches a relocation to an address. A malformed or undersized replacement string table is treated as an error; the patcher does not silently enlarge or restructure a section in a way the rest of the executable may not understand.

The patcher also has to preserve names that are not being converted into exports. Dynamic dependency entries and version-needed records may point into the same string area, so the replacement table is built from both the names that are being changed and the names that must remain available to describe dependencies. The code can share suffixes between strings to keep the rebuilt table within the original section size, but if the required representation will not fit, it stops and reports that a larger section or a different relinking strategy is required.

GNU hash handling adds a second consistency problem. The hash structure is not just a checksum of the strings; it contains a bloom filter, buckets, and chains that refer to the dynamic-symbol ordering. When the patcher reorders the movable symbols to satisfy the hash layout, it records the old-to-new index mapping and applies that mapping to relocation records. It then rebuilds the hash data from the new names. Leaving any one of these layers stale could make a symbol appear absent, resolve to the wrong index, or fail only when a particular loader lookup occurs.

The code also refuses to preserve a GNU version-definition section that it cannot support. That is a useful boundary to expose: a file can be valid ELF and still contain dynamic-linking features outside the patcher's model. The correct response is not to claim that a renamed export is safe because the bytes were writable. The correct response is to stop at the unsupported feature so the resulting process is not built on an unexamined loader assumption.

The PE path has a different file format but the same architectural problem. Its patcher locates the export directory, maps relative virtual addresses to file offsets, replaces the exported names with NIDs, repacks the name strings within the available section space, updates the name pointers, and preserves the name/ordinal relationship after sorting the patched exports. It also rewrites eligible imported names in a PE's import tables. The details differ from ELF because the host loaders differ, but the contract is the same: the title's import vocabulary and the native library's export vocabulary must meet in the format the target operating system understands.

The PE implementation has its own space constraint. It finds the beginning of the export-name strings, calculates whether all of the longer or shorter NID names fit in the raw space available in the export section, writes the new strings, updates the name pointers, and adjusts the section's virtual size. It then sorts the patched names while retaining each name's original ordinal. If the export section has no room to repack the names, the patcher fails rather than writing beyond the section's promised storage.

Imported names are treated selectively as well. The PE patcher rewrites names that carry the project's recognizable PS5 naming forms, but it does not indiscriminately rename every imported string. This matters because a Windows executable can import ordinary host symbols as well as PS5-facing symbols. A useful ABI transformation must identify which names belong to the console boundary and leave unrelated host dependencies understandable to the Windows loader.

The build system makes this contract repeatable. Native libraries are first built as ordinary shared libraries in an unpatched output area. The build then copies each library to a patched output and invokes nid_patcher with the library identity and, where needed, a reference library whose exports must be preserved. The resulting .prx files are the artifacts placed in the runtime library directory. This two-stage process keeps compilation and loader-facing export rewriting distinct, which makes it possible to inspect or test the patcher without confusing it with the library's function bodies.

The preserve-exports step also reveals an ownership boundary. A library should not accidentally claim exports that belong to the common libc layer simply because it can compile a symbol with the same ordinary name. The native library set is meant to reproduce the title's system boundary in organized pieces, not to create ambiguous providers for the host loader. A symbol identity is useful only if the project can also say which library owns it.

Keeping unpatched and patched artifacts separate also improves the meaning of a build result. Compilation answers whether the native implementation can be produced as a shared library. NID patching answers whether its exports can be represented in the binary contract that converted titles request. A failure in the second step does not automatically mean the function body is wrong; it may mean that the export table, string space, hash form, or ownership relationship cannot be adapted safely. Conversely, a successful patch does not test the function body. Treating these as separate stages makes both kinds of failure easier to investigate.

Binding is not behavior

Once the NID-named export is visible, the host loader can resolve a relocation. That is a necessary milestone: without it, the title cannot start or will fail when a required import is bound. But it is not the same as semantic compatibility. A native function can have the right NID and still interpret a structure incorrectly, return the wrong error, omit a side effect, or throw when the title expects a valid result.

This is why AnyPS5's import and progress tools use more than one category of coverage. An imported reference can be implemented, exported only as a throwing stub, absent, or supplied by a title-owned module. Those states answer different questions. “Implemented” is stronger than “exported,” but it still does not mean that every call path has been measured against a PS5. “Module” means the import belongs to the title's own converted library, whose exports are not checked as though they were system PRX implementations.

The same distinction affects unused-import filtering. A NID reference that is removed from the rebuilt dynamic section is not being declared semantically irrelevant by the library layer. It is being judged unreachable or unnecessary by a particular binary analysis. If that analysis cannot model the executable's layout, the filter must retain the reference or fail according to its level. The loader contract is reduced only when the relinker has a defensible structural reason to do so.

There is also an explicit path for incomplete knowledge. The architecture documentation notes that an export whose original name is unknown can be assigned a NID directly. That mechanism can keep an import surface moving when the identifier is known but the human-readable name is not. It does not remove the semantic problem; in fact, it makes the need for later evidence more visible. A directly specified identifier can bind an address, but someone still has to establish the function's signature and behavior.

The trade-off is easy to state. NID patching lets AnyPS5 preserve the title's binary calling relationships while giving the host loader names it can resolve. It avoids rebuilding every title from source and avoids a universal runtime dispatcher. The cost is that the project inherits a strict obligation to keep symbol ownership, relocation metadata, export rewriting, and function semantics aligned. A loader that finds the wrong address can fail more subtly than a loader that finds no address at all.

That is why a NID should be read as the first link in a chain: title relocation, import-module relationship, patched export, host resolution, and native implementation. The next boundary is not another name transformation. It is the set of modules the game itself ships, which must remain title-owned code while being placed into a dependency graph the host loader can initialize without confusing them with AnyPS5's system libraries.

References