取消初始化错误:访问冲突读取位置0x00000008



下面的代码生成一个线程,该线程在(递归)迭代(递归)前台应用程序中的所有可访问性(小部件)之前等待 5 秒。

如果(在 5 秒延迟期间)我切换到 Windows 10 Metro 应用程序(如 Calc 或 Edge),则在主线程中调用 CoUninitialize 将导致访问冲突。为什么?

#include <future>
#include <chrono>
#include <windows.h>
#include <oleacc.h>
#pragma comment(lib,"Oleacc.lib")
// Adapted from https://msdn.microsoft.com/en-us/library/windows/desktop/dd317975%28v=vs.85%29.aspx
HRESULT WalkTreeWithAccessibleChildren(IAccessible* pAcc, int depth)
{
  HRESULT hr;
  long childCount;
  long returnCount;
  if (!pAcc)
  {
    return E_INVALIDARG;
  }
  hr = pAcc->get_accChildCount(&childCount);
  if (FAILED(hr))
  {
    return hr;
  };
  if (childCount == 0)
  {
    return S_FALSE;
  }
  VARIANT* pArray = new VARIANT[childCount];
  hr = AccessibleChildren(pAcc, 0L, childCount, pArray, &returnCount);
  if (FAILED(hr))
  {
    return hr;
  };
  // Iterate through children.
  for (int x = 0; x < returnCount; x++)
  {
    VARIANT vtChild = pArray[x];
    // If it's an accessible object, get the IAccessible, and recurse.
    if (vtChild.vt == VT_DISPATCH)
    {
      IDispatch* pDisp = vtChild.pdispVal;
      IAccessible* pChild = NULL;
      hr = pDisp->QueryInterface(IID_IAccessible, (void**)&pChild);
      if (hr == S_OK)
      {
        WalkTreeWithAccessibleChildren(pChild, depth + 1);
        pChild->Release();
      }
      pDisp->Release();
    }
  }
  delete[] pArray;
  return S_OK;
}
int main(int argc, char *argv[])
{
  CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
  auto future = std::async(std::launch::async,
    []
    {
      // Switch to a Windows 10 Metro app like the Calculator or Edge.
      std::this_thread::sleep_for(std::chrono::milliseconds(5000));
      auto hwnd = GetForegroundWindow();
      if (!hwnd) abort();
      CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
      IAccessible* pAcc = NULL;
      HRESULT hr = AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, IID_IAccessible, (void**)&pAcc);
      if (hr == S_OK) {
        WalkTreeWithAccessibleChildren(pAcc, 0);
        pAcc->Release();
      }
      CoUninitialize();
    }
  );
  future.wait();
  CoUninitialize();
}

错误消息是:

在 Test.exe: 0xC0000005:访问冲突读取位置0x00000008 0x7722B9E7 (combase.dll) 处未处理的异常。

根据@RemyLebeau的建议,我添加了代码来检查 lambda 中 CoInitialize(nullptr, COINIT_APARTMENTTHREADED) 的返回值。事实证明它因0x80010106而失败(设置后无法更改线程模式)。即使我打乱代码并在 lambda 的最开头进行调用,它也会失败并使用此值。这表明 MSVS 对 std::async 的实现实际上在调用 lambda (wtf!最后,我能够通过直接使用WINAPI来避免这个问题(即 CreateThread ),不会受到这种行为的影响。但是,仅此修复程序不足以防止访问冲突。

我还没有找到正确修复访问违规的方法,但我发现了几个阻止它发生的黑客:

  1. 创建一个窗口并显示它。注意:创建窗口本身是不够的,它实际上需要可见。
  2. 将主线程中的CoInitializeEx配置为COINIT_MULTITHREADED。注: 将工作线程中的CoInitializeEx配置为COINIT_MULTITHREADED无济于事。
  3. 在主线程中执行枚举并完全放弃工作线程。
  4. 在工作线程完成后等待>15 秒,然后再调用 CoUninitialize。
  5. 插入一个额外的(不匹配的)调用CoInitialize,以确保引用计数永远不会下降到 0,因此 COM 永远不会真正取消初始化。

不幸的是,黑客 1-3 在此测试用例所基于的实际代码中是不可行的。我不愿意强迫用户等待>15 秒让应用程序退出。因此,现在我倾向于黑客#5。

客户端本身的任何资源泄漏都不是那么重要,因为进程将退出并且资源将作系统回收(尽管它会挫败任何泄漏测试)。重要的是,它会导致辅助功能服务器(MicrosoftEdge.exe)在我运行测试用例的大多数情况下泄漏几kB的内存。

修订后的代码实现了CreateThread修复以及所有 5 个"黑客"。必须至少启用其中一个黑客以防止访问冲突:

#define HACK 0 // Set this between 1-5 to enable one of the hacks.
#include <future>
#include <chrono>
#include <windows.h>
#include <oleacc.h>
#pragma comment(lib,"Oleacc.lib")
// Adapted from https://msdn.microsoft.com/en-us/library/windows/desktop/dd317975%28v=vs.85%29.aspx
HRESULT WalkTreeWithAccessibleChildren(IAccessible* pAcc, int depth)
{
  HRESULT hr;
  long childCount;
  long returnCount;
  if (!pAcc)
  {
    return E_INVALIDARG;
  }
  hr = pAcc->get_accChildCount(&childCount);
  if (FAILED(hr))
  {
    return hr;
  };
  if (childCount == 0)
  {
    return S_FALSE;
  }
  VARIANT* pArray = new VARIANT[childCount];
  hr = AccessibleChildren(pAcc, 0L, childCount, pArray, &returnCount);
  if (FAILED(hr))
  {
    delete[] pArray;
    return hr;
  };
  // Iterate through children.
  for (int x = 0; x < returnCount; x++)
  {
    VARIANT vtChild = pArray[x];
    // If it's an accessible object, get the IAccessible, and recurse.
    if (vtChild.vt == VT_DISPATCH)
    {
      IDispatch* pDisp = vtChild.pdispVal;
      IAccessible* pChild = NULL;
      hr = pDisp->QueryInterface(IID_IAccessible, (void**)&pChild);
      if (hr == S_OK)
      {
        WalkTreeWithAccessibleChildren(pChild, depth + 1);
        pChild->Release();
      }
      pDisp->Release();
    }
  }
  delete[] pArray;
  return S_OK;
}
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
  HRESULT result{};
  // Switch to a Windows 10 Metro app like the Calculator or Edge.
  std::this_thread::sleep_for(std::chrono::milliseconds(5000));
  auto hwnd = GetForegroundWindow();
  if (!hwnd) {
    abort();
  }
  result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
  if (FAILED(result)) {
    abort();
  }
  IAccessible* pAcc = NULL;
  result = AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, IID_IAccessible, (void**)&pAcc);
  if (result == S_OK) {
    WalkTreeWithAccessibleChildren(pAcc, 0);
    pAcc->Release();
  }
  CoUninitialize();
  return 0;
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
  _In_opt_ HINSTANCE hPrevInstance,
  _In_ LPTSTR    lpCmdLine,
  _In_ int       nCmdShow)
{
  HRESULT result{};
  DWORD dw{};
#if HACK == 1
  HWND hwnd = CreateWindowA("STATIC", nullptr, 0,
    CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT,
    0, 0, 0, nullptr);
  if (!hwnd) {
    abort();
  }
  ShowWindow(hwnd, SW_SHOWNORMAL);
#endif
  result = CoInitializeEx(nullptr,
#if HACK == 2
    COINIT_MULTITHREADED
#else
    COINIT_APARTMENTTHREADED
#endif
  );
  if (FAILED(result)) {
    abort();
  }
#if HACK == 3
  ThreadProc(nullptr);
#else
  HANDLE threadHandle = CreateThread(nullptr, 0, &ThreadProc, nullptr, 0, nullptr);
  if (!threadHandle) {
    auto error = GetLastError();
    abort();
  }
  dw = WaitForSingleObject(threadHandle, INFINITE);
  if (dw == WAIT_FAILED) {
    auto error = GetLastError();
    abort();
  }
#endif
#if HACK == 4
  std::this_thread::sleep_for(std::chrono::milliseconds(16000));
#endif
#if HACK == 5
  CoInitialize(nullptr);
#endif
  CoUninitialize();
  return 0;
}

最新更新