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.

  1. 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.
  2. 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.
  3. Guard Pages: Directly adjacent to the committed pages at the bottom of the stack, the OS placed a page marked with the PAGE_GUARD attribute.

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 called TlsSetValue() or TlsGetValue(), 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 .tls section, 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.