为什么注册类失败并ERROR_NOT_ENOUGH_MEMORY

Why RegisterClass fails with ERROR_NOT_ENOUGH_MEMORY?

本文关键字:NOT ENOUGH MEMORY ERROR 注册 失败 为什么      更新时间:2023-10-16

简而言之,我的问题是,当有大量可用内存时,为什么 WinAPI RegisterClass会因ERROR_NOT_ENOUGH_MEMORY而失败,我该怎么做才能防止它?

背景:我正在开发一个应用程序(WinSCP FTP/SFTP 客户端),许多人用它来自动化文件传输。有些人每分钟,每天从Windows调度程序运行它。

我收到很多报告,说在运行一定次数后,应用程序停止工作。触发问题的运行次数似乎并不准确,但它在数万到几十万的范围内。此外,似乎仅在Windows计划程序下运行时才会出现问题,而不是在常规Windows会话中运行时。虽然我不能 100% 确认这一点。

此外,所有报告似乎都是针对Windows 2008 R2 +一些针对Windows 7的报告。同样,这可能只是一个巧合。

我自己能够在Windows 7上重现该问题。一旦系统进入此状态,我的应用程序将不再在调度程序的会话中启动。但它在正常的常规会话中开始得很好。其他一些应用程序(不一定是全部)甚至在调度程序的会话中启动。同样在这种状态下,我无法调试应用程序,因为它甚至在调试器(或进程监视器等工具)运行时也不会加载。

该应用程序正在使用Embarcadero(前Borland)C++Builder VCL库。它在 VCL 初始化代码中的某个地方崩溃(我的WinMain甚至没有启动)并以代码 3 退出。检查初始化代码在做什么,我可能能够识别触发崩溃的代码(尽管它可能只是许多可能的原因之一)。

罪魁祸首似乎是返回8ERROR_NOT_ENOUGH_MEMORY)的WinAPI函数RegisterClass。发生这种情况时,VCL 代码会引发异常;由于还没有异常处理程序,它会使应用程序崩溃。

我已经使用VS 2012中开发的非常简单的C++控制台应用程序(将问题与C++生成器和VCL隔离开来)对此进行了验证。核心代码是:

SetLastError(ERROR_SUCCESS);
fout << L"Registering class" << std::endl;
WNDCLASS WndClass;
memset(&WndClass, 0, sizeof(WndClass));
WndClass.lpfnWndProc = &DefWindowProc;
WndClass.lpszClassName = L"TestClass";
WndClass.hInstance = GetModuleHandle(NULL);
ATOM Atom = RegisterClass(&WndClass);
DWORD Error = GetLastError();
// The Atom is NULL and Error is ERROR_NOT_ENOUGH_MEMORY here

(测试应用程序的完整代码在末尾)

尽管有错误,但它似乎不是内存问题。我在RegisterClass调用之前和之后通过分配 10 MB 内存来验证的内容(可以在最后的完整测试代码中看到)。

绝望之下,我甚至偷看了Wine的实现RegisterClass。它确实会失败ERROR_NOT_ENOUGH_MEMORY,但只有当它无法为类注册分配内存时。什么是几个字节。它也确实使用HeapAlloc分配内存。Wine 不会因为任何其他原因以及任何其他错误代码而使RegisterClass失败。

对我来说,它首先看起来像Windows中的一个错误。我相信Windows应该在进程退出时释放进程分配的所有资源。因此,无论应用程序的实现有多糟糕,以前的运行都不应在资源(如内存)方面对后续运行产生任何影响。无论如何,我很乐意找到解决方法。

更多事实:测试系统除了标准系统过程(总共约50个)外,没有运行任何特殊内容。就我而言,它是VMware虚拟机,尽管我的用户显然在真实的物理机器上看到了问题。该过程的先前实例已消失,因此它们没有正确终止,这会阻止系统释放资源。大约有 500 MB 的可用内存(占总数的一半)。仅分配了大约 16000 个句柄。


测试 VS 应用程序的完整代码:

#include "stdafx.h"
#include "windows.h"
#include <fstream>
int _tmain(int argc, _TCHAR* argv[])
{
    std::wofstream fout;
    fout.open(L"log.txt",std::ios::app);
    SetLastError(ERROR_SUCCESS);
    fout << L"Allocating heap" << std::endl;
    LPVOID Mem = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 10 * 1024 * 1024);
    DWORD Error = GetLastError();
    fout << L"HeapAlloc [" << std::hex << intptr_t(Mem) << std::dec 
         << L"] Error [" << Error << "]" << std::endl;
    // ===== Main testing code begins =====
    SetLastError(ERROR_SUCCESS);
    fout << L"Registering class" << std::endl;
    WNDCLASS WndClass;
    memset(&WndClass, 0, sizeof(WndClass));
    WndClass.lpfnWndProc = &DefWindowProc;
    WndClass.lpszClassName = L"TestClass";
    WndClass.hInstance = GetModuleHandle(NULL);
    ATOM Atom = RegisterClass(&WndClass);
    Error = GetLastError();
    fout << L"RegisterClass [" << std::hex << intptr_t(Atom) << std::dec 
         << L"] Error [" << Error << "]" << std::endl;
    // ===== Main testing code ends =====
    SetLastError(ERROR_SUCCESS);
    fout << L"Allocating heap" << std::endl;
    Mem = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 10 * 1024 * 1024);
    Error = GetLastError();
    fout << L"HeapAlloc [" << std::hex << intptr_t(Mem) << std::dec 
         << L"] Error [" << Error << "]" << std::endl;
    fout << L"Done" << std::endl;
    return 0;
}

输出是(当从Windows 7系统上的Windows调度程序运行时,通过数万次运行我的应用程序进入上述状态):

Allocating heap
HeapAlloc [ec0020] Error [0]
Registering class
RegisterClass [0] Error [8]
Allocating heap
HeapAlloc [18d0020] Error [0]
Done
  1. 在 RAM 用完之前,您可能会用完可用的虚拟地址空间(尤其是对于 32 位进程)。然而,这里的情况似乎并非如此。
  2. 该错误可能是指用完实际 RAM 以外的其他资源,例如原子。由于ATOM是 16 位类型,因此只有 65536 个可能的原子值。然而,像窗口类这样的全局原子的范围更加有限 - 0xC000到0xFFFF,理论上最多只有0x4000(16384)个注册类(在实践中可能更少)。

检查您从RegisterClass()获得的原子值。如果他们在出错之前接近FFFF,那可能是您的问题。

编辑:似乎其他人遇到了同样的问题并确定了罪魁祸首:

VCL中有一个严重的错误,它会吞噬 专用原子表。Windows 在 私有原子表 (32767),这由 Windows 类共享, 视窗消息、剪贴板格式等每次控制 模块初始化后,它会创建一个新的 Windows 消息:

 ControlAtomString := Format('ControlOfs%.8X%.8X', 
                              [HInstance, GetCurrentThreadID]); 
 ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
 RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));

问题乘以应用程序的DLL数量 包含包括控件模块。如果您有 10 个 dll,并且 一个应用程序,此代码每次运行时将消耗 11 个原子。

当系统用完专用原子表中的原子时,否 可以注册窗口类。这意味着,没有窗口程序会 能够在专用原子表已满后打开。

还可以使用 WinDbg 转储原子表,并自行检查此模式。