Win16 Memory Management

Win16 Memory Management

Win16 内存管理

Posted on June 5, 2026 by Michal Necasek 发布于 2026 年 6 月 5 日,作者:Michal Necasek

This is a kind of knowledge base article which resulted from attempts to understand exactly how memory management works in 16-bit Windows. It is not exactly undocumented, but it is also not well documented; even before Windows 3.0 appeared, the assumption was that essentially all application developers were going to use a high-level language and their development tools would take care of the low-level details. 这是一篇知识库文章,源于我试图彻底弄清 16 位 Windows 内存管理机制的尝试。这些信息并非完全没有记录,但确实缺乏详尽的文档;甚至在 Windows 3.0 发布之前,人们就默认所有应用程序开发者都会使用高级语言,而开发工具会自动处理这些底层细节。

Furthermore, nearly all materials for beginning Windows developers focused on the more visible aspects of Windows programming, i.e. windows, icons, menus, and so on. Memory management was glossed over, even though it was absolutely critical to writing a solid Windows application any more complex than a Hello World program. 此外,几乎所有面向 Windows 初学者的资料都集中在 Windows 编程中更直观的方面,例如窗口、图标、菜单等。内存管理往往被一笔带过,尽管对于编写比“Hello World”程序更复杂的稳健 Windows 应用程序而言,它是绝对关键的。

The memory management details and mechanisms are rooted in the 8086 real mode history of Windows 1.x and 2.x, and much of the complexity persisted even when Windows only ran in protected mode starting with Windows 3.1. Unless noted otherwise, in this article “Windows” refers to the 16-bit line of Microsoft products, not Windows NT. 内存管理的细节和机制植根于 Windows 1.x 和 2.x 的 8086 实模式历史中。即便从 Windows 3.1 开始 Windows 仅在保护模式下运行,这些复杂性中的大部分依然存在。除非另有说明,本文中的“Windows”均指微软的 16 位系列产品,而非 Windows NT。

Introduction to Windows Memory Management

Windows 内存管理简介

The key to understanding Windows memory management is that from the very beginning, Windows was among other things a fancy overlay manager. For many years, Windows was too big for typical PCs of the time and needed some way to keep only the most active memory segments in physical RAM, with some mechanism to discard and reload less frequently needed segments on demand. 理解 Windows 内存管理的关键在于:从一开始,Windows 在其他功能之外,本质上还是一个高级的覆盖(overlay)管理器。多年来,Windows 对于当时的典型 PC 来说过于庞大,因此需要一种方法来确保只有最活跃的内存段驻留在物理内存中,并配合某种机制,按需丢弃或重新加载那些不常用的段。

Paging was obviously not used because there was no support for it in 8086 and 80286 systems (and before Windows 3.0, those were very nearly the entirety of the installed base). 分页机制显然没有被使用,因为 8086 和 80286 系统不支持分页(而在 Windows 3.0 之前,这些系统几乎占据了全部装机量)。

In the simplest case of an application with one code segment and one data segment, the movable nature of Windows segments is almost entirely transparent. When the application is running, the CS (code) segment register points to the code segment and the DS (data) and SS (stack) segment registers point to the data segment. As long as the application only uses near calls/jumps within its code segment and near pointers to the data/stack segment, it does not care at all where exactly the segments are in memory, i.e. the actual values loaded into CS/DS/SS registers. Windows can move the segments around and everything will work fine. 在只有一个代码段和一个数据段的简单应用程序中,Windows 段的可移动性几乎是完全透明的。当应用程序运行时,CS(代码)段寄存器指向代码段,DS(数据)和 SS(堆栈)段寄存器指向数据段。只要应用程序仅在其代码段内使用近调用/跳转(near calls/jumps),并使用指向数据/堆栈段的近指针,它就完全不必关心这些段在内存中的确切位置,即加载到 CS/DS/SS 寄存器中的实际值。Windows 可以随意移动这些段,一切仍能正常工作。

But even beginning Windows programmers working through a Hello World style example very quickly start suspecting that life is not so simple in the land of 16-bit Windows. The window procedure must be declared as FAR PASCAL, which is fair enough given that it needs to conform to Windows calling conventions. But it also has to be exported from the application’s executable, otherwise the program won’t work properly. That is a concept entirely unfamiliar to non-Windows developers. 然而,即使是编写“Hello World”示例的 Windows 初学者,也会很快意识到 16 位 Windows 的世界并不那么简单。窗口过程(window procedure)必须声明为 FAR PASCAL,考虑到它需要符合 Windows 的调用约定,这还算合理。但它还必须从应用程序的可执行文件中导出,否则程序将无法正常工作。对于非 Windows 开发者来说,这是一个完全陌生的概念。

To help implement its memory management scheme, Windows adopted and extended the “New Executable” (NE) format first used by “DOS 4”, better known as Multitasking DOS 4.0 and significantly different from PC DOS and MS-DOS 4.0/4.01. Unlike the DOS MZ executable format where an application is effectively a single binary blob, the NE format is segment oriented and each segment is stored on disk separately. That gives Windows the ability to load (or reload) individual segments and move them around in memory. 为了实现其内存管理方案,Windows 采用并扩展了“新可执行文件”(NE)格式。该格式最初由“DOS 4”(即多任务 DOS 4.0,与 PC DOS 和 MS-DOS 4.0/4.01 有显著区别)使用。与 DOS MZ 可执行格式(应用程序实际上是一个单一的二进制块)不同,NE 格式是面向段的,每个段在磁盘上单独存储。这使得 Windows 能够加载(或重新加载)单个段,并在内存中移动它们。

The NE format also supports imports and exports. Imports are used when an application needs to call external code, such as the OS itself. Exports are used for application code which is externally called. A window procedure is one such externally called piece of code. It needs to be exported so that Windows can perform its magic on it. Said magic lets Windows fix up the window procedure prolog (entry sequence) so that it loads the application’s own data segment into the DS register. NE 格式还支持导入和导出。当应用程序需要调用外部代码(如操作系统本身)时使用导入。导出则用于被外部调用的应用程序代码。窗口过程就是这样一段被外部调用的代码。它必须被导出,以便 Windows 对其施展“魔法”。这种魔法允许 Windows 修复窗口过程的序言(入口序列),从而将其应用程序自身的数据段加载到 DS 寄存器中。

Shifting Memory

内存移动

Everything in Windows memory management revolves around segments, contiguous blocks of memory up to 64KB in size. In normal 8086 programming, each segment is identified by its segment address, which directly corresponds to its address in physical memory. Because most segments in Windows can be moved or discarded, they are instead identified by handles. A handle is a 16-bit value which should be considered opaque, even if it might actually a simple index into some table. Windows 内存管理的一切都围绕着“段”展开,即最大 64KB 的连续内存块。在普通的 8086 编程中,每个段由其段地址标识,该地址直接对应于物理内存中的位置。由于 Windows 中的大多数段可以被移动或丢弃,它们改由句柄(handle)来标识。句柄是一个 16 位的值,应被视为不透明的(opaque),即使它实际上可能只是某个表中的简单索引。

For programmers familiar with x86 protected mode, a Windows segment handle is a lot like a protected-mode selector: It is a 16-bit value which uniquely identifies a memory segment, but it is independent of the segment’s location in system memory. The similarity is not coincidental. Steve Wood, the designer of Windows 1.0 memory management, used the Intel 286 protected mode as inspiration for the Windows memory manager (the 286 came out in 1982 and work on Windows started in 1983). A handle refers to a memory segment regardless of where it is in memory, i.e. regardless of what its 8086 segment address is. 对于熟悉 x86 保护模式的程序员来说,Windows 段句柄非常类似于保护模式下的选择子(selector):它是一个唯一标识内存段的 16 位值,但与该段在系统内存中的位置无关。这种相似性并非巧合。Windows 1.0 内存管理的设计者 Steve Wood 将 Intel 286 保护模式作为 Windows 内存管理器的灵感来源(286 于 1982 年发布,而 Windows 的开发始于 1983 年)。句柄指向一个内存段,无论它在内存中的何处,即无论其 8086 段地址是什么。

The GlobalAlloc API allocates contiguous memory from the global heap (possibly more than 64K) and returns a segment handle. Since the 8086 does not support protected mode, approximating protected-mode functionality takes quite a bit of extra work and discipline. Given that a handle is not a segment address, it can’t be used as the segment pointer. GlobalAlloc API 从全局堆中分配连续内存(可能超过 64K)并返回一个段句柄。由于 8086 不支持保护模式,要模拟保护模式的功能需要大量的额外工作和规范。鉴于句柄不是段地址,它不能直接用作段指针。