Hotdry.

Article

Win16分段内存模型解析:Selector映射、NE重定位与兼容层实现

深入解析Win16分段内存架构的核心机制,包括selector/offset映射、NE格式重定位表处理及全局堆锁定策略,为现代兼容层实现提供可落地的技术参数。

2026-06-07systems

背景与核心问题

16 位 Windows(1.x 至 3.x)诞生于 8086 实模式时代,面对当时典型 PC 的 640KB 内存限制,微软设计了一套复杂的分段内存管理系统。这套系统的核心挑战在于:如何在缺乏硬件内存管理单元(MMU)支持的环境下,实现类似虚拟内存的段移动、丢弃与按需加载功能。

与当时主流的 DOS 程序不同,Windows 应用程序需要面对一个根本性的编程范式转变:内存地址不再是静态的。代码段和数据段可能在运行时被操作系统移动甚至丢弃,这要求开发者遵循严格的内存访问规则。

Selector 与 Handle 的抽象映射

Win16 内存管理的基石是 ** 句柄(handle)** 机制。每个内存段分配后返回一个 16 位句柄,而非直接的段地址。这一设计借鉴了 Intel 286 保护模式的 selector 概念 —— 句柄作为段的逻辑标识符,与其物理位置解耦。

应用程序通过GlobalAlloc申请内存时获得句柄,但若要实际访问该内存,必须调用GlobalLock将其转换为可用的段地址:

HANDLE hMem = GlobalAlloc(GMEM_MOVEABLE, size);
// hMem 是句柄,不能直接用于内存访问
LPSTR lpMem = GlobalLock(hMem);
// lpMem 现在包含有效的段:偏移地址
// 可以安全地读写内存...
GlobalUnlock(hMem);  // 释放锁定,段可能再次被移动

这种设计引入了 ** 锁计数(lock count)** 机制。每次GlobalLock使计数加一,GlobalUnlock使其减一。当计数归零,Windows 获得移动该段的许可。这一机制在 8086 实模式下完全由软件实现,因为硬件本身不支持内存保护或重定位。

NE 格式的段导向设计

Windows 采用 "New Executable"(NE)格式替代传统的 DOS MZ 格式,这是实现灵活内存管理的关键。与 MZ 格式将整个可执行文件视为单一二进制块不同,NE 格式将代码和数据组织为独立的段(segment),每个段在磁盘上单独存储。

NE 格式的核心特性包括:

  1. 段表(Segment Table):描述每个段的属性(可移动、可丢弃、预加载等)
  2. 重定位表(Relocation Table):记录段内的 far 指针引用,支持运行时重定位
  3. 导入 / 导出表(Import/Export Table):支持动态链接与回调函数导出

当 Windows 需要整理内存碎片时,可以移动可移动段,并通过重定位表更新所有引用该段的 far 指针。对于可丢弃段(主要是代码段和资源),Windows 可直接释放其占用的内存,需要时从原始可执行文件重新加载。

导出函数的 Prolog 修补机制

Win16 编程中最令人困惑的概念之一是导出函数的声明要求。窗口过程必须声明为FAR PASCAL并显式导出,原因在于 Windows 需要修补函数的入口序列(prolog)。

典型的导出函数 prolog 如下:

push    ds
pop     ax          ; 被Windows loader修补为: mov ax, <data_segment_selector>
xchg    ax,ax       ; NOP占位
inc     bp          ; 标记far调用
push    bp
mov     bp,sp
push    ds
mov     ds,ax       ; 加载正确的数据段

Windows loader 在模块加载时会将前三个字节修补为mov ax, <模块默认数据段选择符>。这意味着如果数据段被移动,Windows 只需重新修补这些入口点,而无需遍历整个程序的代码。

inc bp/dec bp指令对则服务于栈遍历需求。Windows 内存管理器需要识别栈中的 far 调用帧以正确处理段引用,BP 的奇数值标记了 far 函数边界。

兼容层实现要点

在现代系统上实现 Win16 兼容层(如 Wine 或虚拟机方案),需要准确模拟以下机制:

1. Handle 到线性地址的映射表

维护句柄到实际内存地址的映射。建议实现为固定大小的数组或哈希表:

字段 类型 说明
handle uint16_t 句柄值(通常高位字节非零)
linear_addr void* 实际内存地址
lock_count uint16_t 锁定计数
flags uint16_t 段属性(可移动 / 可丢弃 / 固定)
owner HMODULE 所属模块

2. 段属性标志位

#define SEG_MOVABLE     0x0001
#define SEG_DISCARDABLE 0x0002
#define SEG_FIXED       0x0004
#define SEG_PRELOAD     0x0040
#define SEG_ITERATED    0x0080

3. 内存压缩策略

当全局堆空闲空间不足时,按以下优先级执行:

  1. 丢弃所有 lock_count=0 的可丢弃段
  2. 移动所有 lock_count=0 的可移动段以消除碎片
  3. 如仍不足,返回 OOM 错误

4. NE 重定位处理

解析 NE 重定位表时,需支持以下重定位类型:

  • 内部段引用(INTERNALREF):指向同一模块内的其他段
  • 导入名称(IMPORTNAME):通过名称导入 DLL 函数
  • 导入序号(IMPORTORDINAL):通过序号导入 DLL 函数
  • OSFIXUP:操作系统特定的修复(通常忽略)

调试与验证工具

Windows SDK 提供了专门用于验证内存管理正确性的工具:

  • HeapWalker:显示已分配段的详细信息,可模拟低内存条件
  • Shaker/Stress:强制移动和丢弃段以暴露潜在的锁定错误

这些工具的重要性在于:在内存充裕的环境中,未正确配对的GlobalLock/GlobalUnlock调用可能不会立即导致崩溃,因为段往往不会被实际移动。只有在内存压力下,这些隐藏的错误才会显现。

结语

Win16 的分段内存模型代表了在受限硬件条件下的精巧工程设计。通过句柄抽象、NE 格式的段导向结构以及编译器与操作系统的紧密协作,Windows 在 8086 实模式上实现了近似保护模式的内存管理能力。对于现代兼容层开发者而言,理解这些机制的细节 —— 特别是句柄生命周期管理、重定位表解析和栈遍历约定 —— 是确保旧版软件正确运行的关键。

参考资料

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com