The 64KB segment that moved out from under your pointer
How 16-bit Windows moved segments in software, patching function prologs and leaning on GlobalLock discipline because the 8086 and 80286 had no paging hardware.
A GDI function in 16-bit Windows begins, after the module has loaded, with MOV AX,9105 followed by the ordinary INC BP, PUSH BP, MOV BP,SP. The value 9105h is GDI’s own default data segment. On disk, before the loader touches it, those first three bytes are a no-op sequence: push DS, pop it into AX, then an xchg ax,ax that does nothing at all. The compiler emits the harmless version and the Windows loader rewrites it into the useful one at load time. When that data segment later moves, Windows re-patches every exported function that refers to it, so the segment address baked into the prolog stays valid.
That machinery exists because there was no paging hardware to lean on. The 8086 had none, and neither did the 80286; together they made up very nearly the entire installed base before Windows 3.0. Windows was, among other things, an overlay manager: it was too big for a typical PC of the era and had to keep only the active segments in RAM, discarding and reloading the rest on demand. Everything was built around segments, contiguous blocks of up to 64KB.
Handles instead of addresses
Because most segments can be moved or discarded, they are named by opaque 16-bit handles rather than by their physical segment address. Steve Wood, who designed the Windows 1.0 memory manager, took the Intel 286’s protected mode as the model; the 286 shipped in 1982 and work on Windows started in 1983. A handle behaves like a protected-mode selector: it identifies a segment regardless of where that segment currently sits in memory.
The catch is that an 8086 has no protected mode to make that real, so the discipline falls on the programmer. GlobalAlloc hands back a handle. To touch the memory you call GlobalLock, which pins the segment and returns a usable address; when you are done you call GlobalUnlock, and once the lock count hits zero the segment is free to move again. After the unlock the returned address must be treated as dead. The quiet trap here is that Windows will not move a segment until it needs the room, so code that keeps using a pointer past GlobalUnlock often appears to work, right up until memory pressure forces a compaction and the ground shifts under it.
Segments carry two more attributes. Fixed or movable: interrupt handlers have to be fixed so their vectors stay valid, while most code and data wants to be movable so Windows can slide segments together and consolidate the holes that freeing creates. Discardable or not: code segments are read-only and discardable, since they can be reloaded straight from the executable, whereas writable data segments are usually kept because they cannot be reconstructed from disk.
Walking the stack
The INC BP before the push, paired with a DEC BP after the pop, is not decoration. Windows has to decide whether a segment it wants to move is referenced by a stack frame that is already live. To do that it walks the stack, and the incremented BP marks a frame pushed by a far function, the kind that has both a segment and an offset sitting on the stack. The scheme only holds if stacks stay word-aligned, which Windows sets up and which takes real effort to break. The NODATA keyword in a module’s .DEF file tells the loader to leave a prolog alone, for cases like an exported stub that just jumps to another function and never touches the data segment.
DLLs complicate this. They have no stack of their own and run on the caller’s stack, yet they keep their own data segment, so SS and DS do not match. That is why DLL code must be built with /Aw (SS != DS, and do not reload DS on entry because Windows handles it) and why exported far functions need /Gw to get the patchable Windows prolog. Those switches date to Microsoft C 3.0 in 1985 and stayed effectively undocumented for years; even MSC 5.1 lists /Gw and /Aw but refers you to the Windows SDK for what they do.
What the 286 did for OS/2
16-bit OS/2 used the same NE executable format, the same segment-based model, and the same Microsoft tools, but ran in protected mode. There a selector was both the handle and the address, so GlobalLock and GlobalUnlock were gone, window procedures did not need explicit exporting, and the OS never had to unwind application stacks or patch entry points. The 286’s memory hardware moved, discarded, and reloaded segments behind the application’s back. Windows reproduced the behaviour in software and billed the programmer for the difference.
Testing what the hardware could not catch
On the 8086 every address was valid and every read and write succeeded, so nothing trapped a stale pointer or an access into an unlocked segment. Bugs of that kind stay dormant until a segment actually moves. The SDK shipped tools to force the issue: SHAKER, from the Windows 1.0 SDK, shuffled and discarded segments to shake loose locking errors, and HEAPWALK showed the live segments and their owners while also being able to grab all free memory and release it in 1K steps to simulate a starved system. Both still shipped with the Windows 3.0 SDK, because real-mode 3.0 was barely different from 1.0 in this area. The Windows 3.1 SDK, running only in protected mode, retired SHAKER in favour of Stress, aimed at low-resource conditions: tight internal heaps, no disk space, no file handles.
If you are reverse-engineering or maintaining code from this era, the shape of a function’s prolog tells you whether it expects the loader to supply its DS, and a NODATA export tells you it does not. The failure modes to look for are the ones the stress tools were built to surface: a pointer used after GlobalUnlock, or a DLL compiled as though SS equalled DS.
Keep Reading
macOSOctober 2026 teardown of Apple Intelligence's kinship classifier
Apple Intelligence's Personal Knowledge Graph ships an undocumented family classifier and plaintext kinship data on every Mac, readable with Full Disk Access.
zyxelZyxel's June patch still runs attacker commands as root
Zyxel's Patch 2 for the GS1900 switches runs its bounds check too late, leaving the same unauthenticated root exploit open until Patch 3.
workosOn October 30, an AI tool breached WorkOS
WorkOS says an attacker used the ARTEX AI offensive framework to breach an internal sales app and access CRM contacts and customer call summaries.
Latest on the Wire
Full wire →- Archive Documents 356 Processor Die PhotographsHacker News
- Cloud service breach exposes 7.7M Iris Ohyama customer recordsBleepingComputer
- Cybertruck Failure Underscores Tesla's Post-Straubel StrugglesHacker News
- Datasette Stops Firefox From Prefetching Unnecessary PagesSimon Willison
New signal daily · RSS
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.