Windows Memory Architecture
September 10, 2011 Leave a comment
Content
- How a Virtual Address Space Is Partitioned
- Regions in an Address Space
- Committing Physical Storage Within a Region
- Physical Storage and the Paging File
- Page Protection Attributes
- The Importance of Data Alignment
Introduction
Every process is given its very own virtual address space. For 32-bit processes, this address space is 4 GB because a 32-bit pointer can have any value from 0×00000000 through 0xFFFFFFFF.
Every process has its own private address space. Process A can have a data structure stored in its address space at address 0×12345678, while Process B can have a totally different data structure stored in its address space—at address 0×12345678. When threads running in Process A access memory at address 0×12345678, these threads are accessing Process A’s data structure. When threads running in Process B access memory at address 0×12345678, these threads are accessing Process B’s data structure. Threads running in Process A cannot access the data structure in Process B’s address space, and vice versa.
This address space is simply a range of memory addresses. Physical storage needs to be assigned or mapped to portions of the address space before you can successfully access data without raising access violations.
How a Virtual Address Space Is Partitioned
Each process’ virtual address space is split into partitions. The address space is partitioned based on the underlying implementation of the operating system.
The partition of the process’ address space from 0×00000000 to 0x0000FFFF inclusive is set aside to help programmers catch NULL-pointer assignments. If a thread in your process attempts to read from or write to a memory address in this partition, an access violation is raised.
This User-Mode partition is where the process’ address space resides. The usable address range and approximate size of the user-mode partition depends on the CPU architecture.
This Kernel-Mode partition is where the operating system’s code resides. The code for thread scheduling, memory management, file systems support, networking support, and all device drivers is loaded in this partition. Everything residing in this partition is shared among all processes. Although this partition is just above the user-mode partition in every process, all code and data in this partition is completely protected. If your application code attempts to read or write to a memory address in this partition, your thread raises an access violation.
Regions in an Address Space
When a process is created and given its address space, the bulk of this usable address space is free, or unallocated. To use portions of this address space, you must allocate regions within it by calling VirtualAlloc. the act of allocating a region is called reserving.
Whenever you reserve a region of address space:
- The system ensures that the region begins on an allocation granularity boundary. All the CPU platforms use the same allocation granularity of 64 KB—that is, allocation requests are rounded to a 64-KB boundary.
- The system ensures that the size of the region is a multiple of the system’s page size. A page is a unit of memory that the system uses in managing memory. Like the allocation granularity. The x86 and x64 systems use a 4-KB page size, but the IA-64 uses an 8-KB page size.
If you attempt to reserve a 10-KB region of address space, the system will automatically round up your request and reserve a region whose size is a multiple of the page size. This means that on x86 and x64 systems, the system will reserve a region that is 12 KB.
When your program’s algorithms no longer need to access a reserved region of address space, the region should be freed. This process is called releasing the region of address space and is accomplished by calling the VirtualFree function.
Committing Physical Storage Within a Region
To use a reserved region of address space, you must allocate physical storage and then map this storage to the reserved region. This process is called committing physical storage. Physical storage is always committed in pages. To commit physical storage to a reserved region, you again call the VirtualAlloc function.
When your program’s algorithms no longer need to access committed physical storage in the reserved region, the physical storage should be freed. This process is called decommitting the physical storage and is accomplished by calling the VirtualFree function.
Physical Storage and the Paging File
The file on the disk is typically called a paging file, and it contains the virtual memory that is available to all processes.
when an application commits physical storage to a region of address space by calling the VirtualAlloc function, space is actually allocated from a file on the hard disk. The size of the system’s paging file is the most important factor in determining how much physical storage is available to applications; the amount of RAM you have has very little effect.
Now when a thread in your process attempts to access a block of data in the process’ address space.
physical address in memory, and then the desired access is performed.
In the second possibility, the data that the thread is attempting to access is not in RAM but is contained somewhere in the paging file. In this case, the attempted access is called a page fault, and the CPU notifies the operating system of the attempted access. The operating system then locates a free page of memory in RAM; if a free page cannot be found, the system must free one. If a page has not been modified, the system can simply free the page. But if the system needs to free a page that was modified, it must first copy the page from RAM to the paging file. Next the system goes to the paging file, locates the block of data that needs to be accessed, and loads the data into the free page of memory. The operating system then updates its table indicating that the data’s virtual memory address now maps to the appropriate physical memory address in RAM. The CPU now retries the instruction that generated the initial page fault, but this time the CPU is able to map the virtual memory address to a physical RAM address and access the block of data.
The more often the system needs to copy pages of memory to the paging file and vice versa, the more your hard disk thrashes and the slower the system runs. (Thrashing means that the operating system spends all its time swapping pages in and out of memory instead of running programs.)
When you invoke an application, the system opens the application’s .exe file and determines the size of the application’s code and data. Then the system reserves a region of address space and notes that the physical storage associated with this region is the .exe file itself. That’s right—instead of allocating space from the paging file, the system uses the actual contents, or image, of the .exe file as the program’s reserved region of address space. This, of course, makes loading an application very fast and allows the size of the paging file to remain small.
When a program’s file image (that is, an .exe or a DLL file) on the hard disk is used as the physical storage for a region of address space, it is called a memory-mapped file. When an .exe or a DLL is loaded, the system automatically reserves a region of address space and maps the file’s image to this region.
Page Protection Attributes
Individual pages of physical storage allocated can be assigned different protection attributes.
Some malware applications write code into areas of memory intended for data (such as a thread’s stack) and then the application executes the malicious code. Windows’ Data Execution Prevention (DEP) feature provides protection against this type of malware attack. With DEP enabled, the operating system uses the PAGE_EXECUTE_* protections only on regions of memory that are intended to have code execute; other protections (typically PAGE_READWRITE) are used for regions of memory intended to have data in them (such as thread stacks and the application’s heaps).
Windows supports a mechanism that allows two or more processes to share a single block of storage. So if 10 instances of Notepad are running, all instances share the application’s code and data pages.
When an .exe or a .dll module is mapped into an address space, the system calculates how many pages are writable. (Usually, the pages containing code are marked as PAGE_EXECUTE_READ while the pages containing data are marked PAGE_READWRITE.) Then the system allocates storage from the paging file to accommodate these writable pages. This paging file storage is not used unless the module’s writable pages are actually written to.
When a thread in one process attempts to write to a shared block, the system intervenes and performs the following steps:
-
The system finds a free page of memory in RAM.
-
The system copies the contents of the page attempting to be modified (in the image) to the free page found in step 1. This free page will be assigned either PAGE_READWRITE or PAGE_EXECUTE_READWRITE protection. The original page’s protection and data does not change at all.
-
The system then updates the process’ page tables so that the accessed virtual address now translates to the new page of RAM.
After the system has performed these steps, the process can access its own private instance of this page of storage.
A memory block is a set of contiguous pages that all have the same protection attributes and that are all backed by the same type of physical storage.
Protection attributes are given to a region for the sake of efficiency only, and they are always overridden by protection attributes assigned to physical storage.
A block’s protection attributes override the protection attributes of the region that contains the block.
The Importance of Data Alignment
Data alignment is not so much a part of the operating system’s memory architecture as it is a part of the CPU’s architecture.
CPUs operate most efficiently when they access properly aligned data. Data is aligned when the memory address of the data modulo of the data’s size is 0. For example, a WORD value should always start on an address that is evenly divided by 2, a DWORD value should always start on an address that is evenly divided by 4, and so on. When the CPU attempts to read a data value that is not properly aligned, the CPU will do one of two things. It will either raise an exception or the CPU will perform multiple, aligned memory accesses to read the full misaligned data value.
Here is some code that accesses misaligned data:
VOID SomeFunc(PVOID pvDataBuffer) {
// The first byte in the buffer is some byte of information
char c = * (PBYTE) pvDataBuffer;
// Increment past the first byte in the buffer
pvDataBuffer = (PVOID)((PBYTE) pvDataBuffer + 1);
// Bytes 2-5 contain a double-word value
DWORD dw = * (DWORD *) pvDataBuffer;
// The line above raises a data misalignment exception on some CPUs
...
Obviously, if the CPU performs multiple memory accesses, the performance of your application is hampered. At best, it will take the system twice as long to access a misaligned value as it will to access an aligned value—but the access time could be even worse! To get the best performance for your application, you’ll want to write your code so that the data is properly aligned.