Windows 95 Memory Allocation for Win32 Threads
Windows 95 introduced preemptive multitasking and a 32-bit flat memory model to the consumer PC market, fundamentally changing how thread-level memory was handled compared to Windows 3.1. This article explains how Windows 95 allocated and managed memory for Win32 application threads, detailing the 4 GB virtual address space layout, dynamic thread stack growth using guard pages, Thread Local Storage (TLS), and shared process heaps.
The 4 GB Virtual Address Space
Every Win32 process in Windows 95 ran within its own 4 GB virtual address space, enabled by the x86 paging mechanism with 4 KB memory pages. Windows 95 partitioned this address space into distinct functional regions:
- 0x00000000 to 0x003FFFFF (4 MB): The DOS compatibility arena. The lowest 64 KB was unmapped to catch NULL pointer dereferences, while the rest accommodated 16-bit real-mode operations and DOS device drivers.
- 0x00400000 to 0x7FFFFFFF (2 GB): The private Win32 process arena. All private code, data, default heaps, and thread stacks for a given application resided exclusively here, isolated from other processes.
- 0x80000000 to 0xBFFFFFFF (1 GB): The shared arena. This region contained shared DLLs, memory-mapped files, and 16-bit Win16 applications to maintain backward compatibility.
- 0xC0000000 to 0xFFFFFFFF (1 GB): The system arena (Ring 0). This area contained the Virtual Machine Manager (VMM), file system drivers, and VxDs.
Because thread execution occurs within a host process, all thread-specific user-mode memory allocations were carved out of the private 2 GB region.
User-Mode Thread Stacks and Dynamic Growth
When a Win32 application created a new thread using the
CreateThread API, the operating system allocated a
dedicated user-mode stack within the process's private 2 GB space. The
allocation worked using a two-step virtual memory strategy: reservation
and commitment.
- Address Reservation: The Virtual Machine Manager reserved a continuous range of virtual addresses (typically 1 MB by default, configured via the Portable Executable header) without immediately allocating physical RAM or swap space.
- Initial Commitment: Windows 95 committed only a small portion of this address space—usually two or three 4 KB pages—as actual physical memory at the base (top) of the stack.
- Guard Pages: Directly adjacent to the committed
pages at the bottom of the stack, the OS placed a page marked with the
PAGE_GUARDattribute.
As the thread executed and pushed data onto the stack, the stack grew
downward toward lower memory addresses. When execution reached the guard
page, the CPU generated a page fault (STATUS_GUARD_PAGE_VIOLATION). The
Windows 95 memory manager intercepted this fault, removed the guard
status from that page, committed a new physical 4 KB page to it, and
placed the PAGE_GUARD flag on the next page down. This
process continued automatically until the reserved address limit was
reached, at which point a stack overflow error occurred.
Ring 0 Kernel Stacks
In addition to the user-mode stack, every Win32 thread had an associated Ring 0 (kernel-mode) stack. This kernel stack was much smaller—typically 8 KB to 16 KB—and resided in the upper memory system arena.
When a thread executed a system call or triggered a hardware interrupt, the CPU switched execution privilege levels from Ring 3 to Ring 0. The operating system shifted execution to the thread's Ring 0 stack, preventing user-mode stack corruption from destabilizing the kernel and ensuring safe execution of device drivers and system services.
The Thread Information Block (TIB)
To track per-thread metadata, Windows 95 created a Thread Information
Block (also called the Thread Environment Block, or TEB) for each
running thread. The CPU’s FS segment register was assigned
a selector pointing directly to the current thread's TIB.
The TIB stored crucial memory-related pointers, including:
- The current top and bottom bounds of the thread's stack.
- The head of the Structured Exception Handling (SEH) frame chain.
- Pointers to Thread Local Storage arrays.
Thread Local Storage (TLS)
Win32 threads running within the same process shared all global and static variables. When an application required unique instance data per thread, Windows 95 managed it through Thread Local Storage via two mechanisms:
- Dynamic TLS: A process called
TlsAlloc()to obtain a process-wide index. The OS maintained an array of 64 TLS slots inside each thread's TIB. When a thread calledTlsSetValue()orTlsGetValue(), the operating system read or wrote directly to that thread's private slot array without inter-thread locking. - Static TLS: Declared using compiler directives such
as
__declspec(thread). When the application loaded, the PE loader scanned the.tlssection, calculated the required data size, and allocated a dedicated block of memory for each thread during thread initialization.
Heaps and Virtual Memory Management
Threads did not automatically possess private dynamic heaps. Instead,
threads within a process shared the default process heap created during
process startup, accessed via APIs like HeapAlloc,
malloc, or new.
Because multiple threads could allocate memory simultaneously, heap
allocations were protected by internal synchronization mechanisms (such
as critical sections). To avoid lock contention, Win32 developers could
use HeapCreate to allocate dedicated, private heaps for
specific worker threads. At the lowest level, all dynamic memory
allocation routed through VirtualAlloc, which interacted
directly with the Windows 95 paging subsystem to assign physical RAM or
swap space to the process's page directory and page tables.