Skip to lesson
Game Hacking Academy
Preparing lesson text…
Exit reader mode
Voice and speed

Preparing read-aloud controls…

Lesson 6.7 Listening edition

What a D L L, dynamic link library, Function Promises Its Caller

Infer an exported function's parameters, calling convention, result, and responsibility for memory cleanup before wrapping it in a small typed API.

About 28 minutes

The executable-analysis chapter showed how a D L L export name leads to a function address. Finding that address does not tell us how to call the function. Before our code becomes a library inside another process, we need the agreement between caller and callee: what values cross the boundary, where they arrive, and who owns any memory they refer to.

The earlier text lesson supplied one part of this agreement: a string pointer had to refer to the right encoding and remain valid for the call. A D L L export requires the same care even when its name is visible.

Section: A function name is not a contract.

A D L L export table may reveal a name such as gha_start, GetPlayer, or FormatMessage. That name only tells you that other code can locate the function. It does not fully explain:

which arguments the caller passes;

where those arguments arrive;

what the return value means;

which side owns pointed-to memory;

whether the call changes global state;

whether it is safe to call more than once.

Recovering that call contract is interface reversing. We will practice on this book’s own Windows D L L, where the source is available afterward for checking our conclusions.

Section: Reconstruct four parts of every boundary.

Treat a machine-level function boundary as four connected contracts:

Representation: How many bits does each value use, and is it an integer, float, pointer, flag set, or structure?

Transport: Which registers or stack locations carry arguments and results?

Ownership: Who allocates, may mutate, and eventually releases pointed-to data?

Lifetime: For how long do those addresses and returned views remain valid?

Calling conventions mainly answer transport. They do not answer ownership or lifetime. A function can correctly receive a pointer in r c x and still crash later because the caller freed the buffer too early. Likewise, a return value in r a x might be a borrowed pointer, an owned allocation, a handle, or merely a status code.

In brief: A callable address still needs a correct signature, memory ownership, and a lifetime agreement.

Each part of the contract follows from something the machine code does. A callee that reads four bytes through the pointer and never stores it needs only a short read-only borrow. A callee that saves the pointer in a global needs the data to outlive the call.

A saved pointer can outlive the data it points to.

Section: Begin with the caller.

On 64-bit Windows, the first four integer or pointer arguments normally arrive in r c x, r d x, r 8, and r 9. A return value normally leaves in r a x. On 32-bit Windows, several calling conventions exist, so also inspect stack cleanup and decorated export names.

Suppose a call site looks like this:

Lesson diagram

These instructions are the machine-level version of that path. Read them in order: clear e c x, call the export through its import slot, test the returned e a x, and branch on a nonzero result.

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Read in plain English, before any types are guessed, the call site says:

The caller puts zero in the first argument register.

It makes an indirect call through an imported function slot.

It tests the low 32 bits of the result.

Zero takes the success path; nonzero takes the failure path.

That supports a tentative contract:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

extern "system" selects the platform’s Windows A B I, application binary interface,—application binary interface, the complete machine-level agreement between compiled pieces: which registers or stack slots carry arguments, who cleans up the stack, and where the return value lands. It is the compiled counterpart to a source-level A P I, application programming interface. unsafe is honest: the compiler cannot prove that a raw address really points to a function with this exact contract.

Section: Where each part of the contract shows up.

A decompiler’s guessed types are only a starting point. Each question about the contract has a place in the machine code that settles it:

Where each part of the contract shows up

Lesson table

One call with a null argument does not prove the argument must always be null. Find a second caller or change the controlled test program.

Narration: One successful call can hide a special case. A second caller may reveal a buffer, size, or cleanup obligation the first never exercised.

Section: Separate lookup from calling.

GetProcAddress returns an untyped address. Keep the dangerous conversion in one function:

Lesson diagram

The resolver below takes the loaded module handle, asks for the exact gha_start export, and converts the returned address to one typed function pointer. A missing export must return an error; a found address is still safe to call only if the contract inferred above is correct for this D L L build.

Code example. The code’s comments explain: SAFETY: the byte string is NUL-terminated and lives for this call. SAFETY: our debugger evidence and matching test D L L establish this A B I.

Why not scatter transmute through the program? Because every conversion is a promise about A B I and types. One narrow boundary gives reviewers one place to check that promise.

Section: Wrap machine results in named variants.

The course D L L returns:

0 when the worker started;

1 when it was already running;

2 when the worker thread could not start.

Give those numbers names:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Keep Unknown. A future D L L can add a status without making your wrapper silently call it success.

Section: Prove the calling convention.

A source-level A P I can say that a function accepts one address and returns a number. The A B I answers the machine-level question of where that address arrives. On a 32-bit build, one convention might put it in a register while another expects it on the stack. The functions can look identical at the source level, yet the callee reads the wrong location if caller and callee disagree.

Alternative: Picture the A B I as labelled delivery slots: agreeing on the parcel means little if each side uses a different slot.

Lesson diagram

A wrong A B I can appear to work in one small test and still corrupt registers or the stack. The call itself may return without crashing; the damage shows up later, when an unrelated function reads garbage from a register it expected the callee to preserve, or when a mismatched stack-cleanup convention leaves e s p off by a few bytes and a later ret lands somewhere nonsensical. Check:

Which architecture is the caller and D L L?

Who removes stack arguments on 32-bit builds?

Does the callee return with a plain ret or ret N?

Does a decorated name encode argument bytes?

Do several calls return with the stack pointer unchanged?

For x64 Windows there is one main user-mode convention, but argument type and ownership can still be wrong.

Section: Test with a contract matrix.

Test against the D L L from windows-labs rather than an unknown third-party export. Each test has an expected result:

Lesson table

The source in windows-labs/src/windows_impl/dll.rs then shows the real contract, and any difference from the recovered one points at the exact part of the machine code that was misread.

Section: Working practices for an undocumented D L L.

Never guess a function pointer type and “try it” in an important process.

Use the disposable course fixture; a contract recovered from it applies to that exact build.

Treat every pointer’s size, mutability, lifetime, and ownership as a separate question.

Preserve an Unknown result path.

Keep loading, resolving, converting, calling, and unloading as visible stages.

This same method works for undocumented game plug-ins, supported mod SDKs, and old middleware. The goal is not merely to make one call succeed. The goal is to explain the interface well enough that a small wrapper can enforce it.

The contract gives a function address meaning: it tells a caller which values are allowed and which obligations survive the call. In-Process DLLs and Loader Boundaries builds the small D L L used here. Its explicit start and stop functions turn the recovered agreement into an interface whose loading, work, and cleanup can be followed in order.