【问题标题】:Why would waveOutWrite() cause an exception in the debug heap?为什么 waveOutWrite() 会在调试堆中导致异常?
【发布时间】:2010-09-16 18:40:57
【问题描述】:

在研究这个问题时,我在网上发现了多次提到以下场景,在编程论坛上总是作为未回答的问题。我希望在此处发布此内容至少可以记录我的发现。

首先,症状:在运行使用 waveOutWrite() 输出 PCM 音频的标准代码时,有时在调试器下运行时会出现此问题:

 ntdll.dll!_DbgBreakPoint@0()   
 ntdll.dll!_RtlpBreakPointHeap@4()  + 0x28 bytes    
 ntdll.dll!_RtlpValidateHeapEntry@12()  + 0x113 bytes   
 ntdll.dll!_RtlDebugGetUserInfoHeap@20()  + 0x96 bytes  
 ntdll.dll!_RtlGetUserInfoHeap@20()  + 0x32743 bytes    
 kernel32.dll!_GlobalHandle@4()  + 0x3a bytes   
 wdmaud.drv!_waveCompleteHeader@4()  + 0x40 bytes   
 wdmaud.drv!_waveThread@4()  + 0x9c bytes   
 kernel32.dll!_BaseThreadStart@8()  + 0x37 bytes    

虽然明显的嫌疑是代码中其他地方的堆损坏,但我发现情况并非如此。此外,我能够使用以下代码重现此问题(这是基于对话框的 MFC 应用程序的一部分:)

void CwaveoutDlg::OnBnClickedButton1()
{
    WAVEFORMATEX wfx;
    wfx.nSamplesPerSec = 44100; /* sample rate */
    wfx.wBitsPerSample = 16; /* sample size */
    wfx.nChannels = 2;
    wfx.cbSize = 0; /* size of _extra_ info */
    wfx.wFormatTag = WAVE_FORMAT_PCM;
    wfx.nBlockAlign = (wfx.wBitsPerSample >> 3) * wfx.nChannels;
    wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;

    waveOutOpen(&hWaveOut, 
                WAVE_MAPPER, 
                &wfx,  
                (DWORD_PTR)m_hWnd, 
                0,
                CALLBACK_WINDOW );

    ZeroMemory(&header, sizeof(header));
    header.dwBufferLength = 4608;
    header.lpData = (LPSTR)GlobalLock(GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, 4608));

    waveOutPrepareHeader(hWaveOut, &header, sizeof(header));
    waveOutWrite(hWaveOut, &header, sizeof(header));
}

afx_msg LRESULT CwaveoutDlg::OnWOMDone(WPARAM wParam, LPARAM lParam)
{
    HWAVEOUT dev = (HWAVEOUT)wParam;
    WAVEHDR *hdr = (WAVEHDR*)lParam;
    waveOutUnprepareHeader(dev, hdr, sizeof(WAVEHDR));
    GlobalFree(GlobalHandle(hdr->lpData));
    ZeroMemory(hdr, sizeof(*hdr));
    hdr->dwBufferLength = 4608;
    hdr->lpData = (LPSTR)GlobalLock(GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, 4608));
    waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
    waveOutWrite(hWaveOut, hdr, sizeof(WAVEHDR));
    return 0;
 }

在任何人对此之前,是的 - 示例代码播放未初始化的内存。请勿在扬声器完全打开的情况下尝试此操作。

一些调试揭示了以下信息:waveOutPrepareHeader() 用一个指针填充 header.reserved,该指针指向一个似乎包含至少两个指针作为其前两个成员的结构。第一个指针设置为 NULL。调用 waveOutWrite() 后,该指针被设置为分配在全局堆上的指针。在伪代码中,它看起来像这样:

struct Undocumented { void *p1, *p2; } /* This might have more members */

MMRESULT waveOutPrepareHeader( handle, LPWAVEHDR hdr, ...) {
    hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));
    /* Do more stuff... */
}

MMRESULT waveOutWrite( handle, LPWAVEHDR hdr, ...) {

    /* The following assignment fails rarely, causing the problem: */
    hdr->reserved->p1 = malloc( /* chunk of private data */ );
    /* Probably more code to initiate playback */
}

通常,标头由 wdmaud.dll 的内部函数 waveCompleteHeader() 返回给应用程序。 waveCompleteHeader() 尝试通过调用 GlobalHandle()/GlobalUnlock() 和朋友来释放由 waveOutWrite() 分配的指针。有时,GlobalHandle() 会爆炸,如上所示。

现在,GlobalHandle() 炸弹的原因并不是堆损坏,正如我一开始所怀疑的那样 - 这是因为 waveOutWrite() 返回时没有将内部结构中的第一个指针设置为有效指针。我怀疑它在返回之前释放了该指针指向的内存,但我还没有反汇编它。

这似乎只发生在波形播放系统的缓冲区不足时,这就是我使用单个标头来重现它的原因。

在这一点上,我有一个很好的理由来反对这是我的应用程序中的一个错误 - 毕竟,我的应用程序甚至没有运行。有人见过这个吗?

我在 Windows XP SP2 上看到了这个。声卡来自SigmaTel,驱动版本为5.10.0.4995。

注意事项:

为了防止将来出现混淆,我想指出,问题在于使用 malloc()/free() 来管理正在播放的缓冲区的答案是完全错误的。您会注意到我更改了上面的代码以反映建议,以防止更多人犯同样的错误——这没有什么区别。 waveCompleteHeader() 释放的缓冲区不是包含 PCM 数据的缓冲区,释放 PCM 缓冲区的责任在于应用程序,不需要以任何特定方式分配。

另外,我确保我使用的所有 waveOut API 调用都不会失败。

我目前假设这是 Windows 或音频驱动程序中的错误。反对意见总是受欢迎的。

【问题讨论】:

  • 我隐约记得在 Windows CE 应用程序中看到过类似的东西。 Deleaker 报告某些 wave*** 函数存在内存泄漏。最后,一切都很好,关闭时,我只需要释放资源。不过我这里没有代码。

标签: c++ windows audio waveoutwrite


【解决方案1】:

现在,GlobalHandle() 的原因 炸弹不是由于堆损坏, 正如我一开始所怀疑的那样 - 这是因为 waveOutWrite() 没有返回 设置第一个指针 指向有效指针的内部结构。 我怀疑它释放了内存 之前由该指针指向 回来了,但我没有拆机 还没有。

我可以在我的系统上使用您的代码重现这一点。我看到了类似于 Johannes 报道的情况。在调用 WaveOutWrite 之后,hdr->reserved 通常保存一个指向已分配内存的指针(其中似乎包含 unicode 中的 wave out 设备名称等)。

但偶尔,在从 WaveOutWrite() 返回后,hdr->reserved 指向的字节被设置为 0。这通常是该指针的最低有效字节。 hdr->reserved 中的其余字节都可以,并且它通常指向的内存块仍被分配且未损坏。

它可能正在被另一个线程破坏 - 我可以在调用 WaveOutWrite() 后立即使用条件断点捕捉更改。并且系统调试断点发生在另一个线程中,而不是消息处理程序中。

但是,如果我使用回调函数而不是 Windows 消息泵,则无法导致系统调试断点发生。 (fdwOpen = CALLBACK_FUNCTION 在 WaveOutOpen() 中) 当我这样做时,我的 OnWOMDone 处理程序由不同的线程调用 - 可能是负责损坏的线程。

所以我认为在 Windows 或驱动程序中存在错误,但我认为您可以通过使用回调函数而不是 Windows 消息泵来处理 WOM_DONE 来解决问题。

【讨论】:

  • 查看 Stefan 的回答,该回答指向一个提供更多相关信息的错误报告。你是对的 - 它要么在 Windows 中,要么在驱动程序中,你的分析几乎与错误报告中的分析相同。我无法从这里的任何人那里得到的最后一点信息:你使用哪个音频驱动程序?
  • 我正在使用 Analog Devices 的 SoundMAX 集成数字音频 5.12.1.4060。在完成大部分工作后,我才阅读错误报告。无论如何,我还是发布了,因为我认为您可能会发现解决方法很有用。
  • 谢谢。我使用的是不同的驱动程序,所以要么两个驱动程序都有相同的错误,要么这毕竟是在 Windows 中。坦率地说,根据错误报告中的描述,我看不出它除了是 Windows 错误之外的其他任何东西。
【解决方案2】:

【讨论】:

  • 很高兴看到该问题已在 2005 年报告,并且已关闭。
  • 它被关闭为“外部”,这意味着它不是来自 MS 的驱动程序内部的错误,或者不是我的 MS 开发的另一个组件。我的猜测是这是一个驱动程序错误 - Windows 中没有很多非驱动程序组件不是来自 MS。
  • 明白了。如果能获得更多关于它的信息,例如是哪个驱动程序导致了这种情况,那就太好了。
  • 我的分析很糟糕,哇! ://
  • 这可能不是驱动程序问题,除非它发生在多个驱动程序上。错误描述还清楚地表明这是一个 Windows 问题 - 对 IsBadWritePtr() 的调用发生在帮助线程中,而不是在驱动程序中。
【解决方案3】:

我也遇到了同样的问题,自己也做了一些分析:

waveOutWrite() 分配(即 GlobalAlloc)一个指向 354 字节堆区域的指针,并将其正确存储在 header.reserved 指向的数据区域中。

但是当这个堆区域再次被释放时(根据你的分析,在 waveCompleteHeader() 中;我自己没有 wdmaud.drv 的符号),指针的最低有效字节已设置为零,从而使指针无效(而堆尚未损坏)。换句话说,发生的事情是这样的:

  • (BYTE *) (header.reserved) = 0

所以我有一点不同意你的说法:waveOutWrite() 首先存储一个有效指针;指针只会在稍后从另一个线程中损坏。 可能是同一线程 (mxdmessage) 稍后尝试释放此堆区域,但我还没有找到存储零字节的点。

这种情况并不经常发生,并且之前已经成功分配和释放了相同的堆区域(相同的地址)。 我非常确信这是系统代码中的某个错误。

【讨论】:

  • Johannes,如果我没记错的话,我看到的指针损坏在整个指针设置为 0 和设置为各种垃圾值之间变化。显然,你看到的是别的东西。我真的很想了解更多关于您的调试工作的信息。
  • 另外,出于好奇:您使用哪个音频驱动程序来重现此内容?
  • 这里的低字节指针损坏对应上面Stefan贴的MS Connect链接
【解决方案4】:

不确定这个特殊问题,但您是否考虑过使用更高级别的跨平台音频库? Windows 音频编程有很多怪癖,这些库可以为您省去很多麻烦。

示例包括 PortAudioRtAudioSDL

【讨论】:

  • 我需要低级访问,而且 Windows 音频编程非常简单 - waveOut API 一直存在,我非常了解它(13 年的使用经验,使用它编写多个声音引擎,从 Windows 3.1 开始)上面的代码几乎就是它所需要的。就是这样。
  • 我来到这里是因为 QAudioOutput 和 PortAudio 崩溃了:D
【解决方案5】:

我要做的第一件事是检查 waveOutX 函数的返回值。如果其中任何一个失败了——考虑到你描述的场景,这并不是不合理的——并且你继续进行,那么事情开始出错也就不足为奇了。我的猜测是 waveOutWrite 会在某个时候返回 MMSYSERR_NOMEM。

【讨论】:

  • 在我的生产代码中,每个 waveOutXXX 函数都有一个 assert() 确保返回值是 MMSYSERR_NOERROR 紧随其后。没有一个调用失败。
  • 另外,你应该注意失败发生在缓冲区播放成功之后,并且在它返回到应用程序之前,所以waveOutWrite()认为它成功了。
  • 这可能是由驱动程序故障引起的。您是否尝试在使用不同的音频硬件时重现相同的问题?您目前使用的是什么设备以及哪个驱动程序版本?
  • SigmaTel - 驱动程序版本 5.10.0.4995。我已经在其他具有其他硬件的机器上看到此故障,但我想在周末验证一下,并提供更多信息。
【解决方案6】:

使用 Application Verifier 找出发生了什么,如果您做了可疑的事情,它会更早地发现它。

【讨论】:

【解决方案7】:

查看source code for Wine 可能会有所帮助,尽管 Wine 可能已经修复了任何错误,而且 Wine 也可能存在其他错误。相关文件是 dlls/winmm/winmm.c、dlls/winmm/lolvldrv.c,可能还有其他文件。祝你好运!

【讨论】:

  • 实际上,我通常会这样做——我过去曾为 Wine 做过贡献,我喜欢将它作为一种文档形式存在。不过,我怀疑 Wine 在这种情况下是否会有相同的错误 - Wine 中的错误兼容性仅此而已。
【解决方案8】:

不允许从回调中调用 winmm 函数这一事实如何? MSDN 没有提到关于窗口消息的这种限制,但是窗口消息的使用类似于回调函数。可能在内部,它被实现为来自驱动程序的回调函数,并且该回调执行 SendMessage。 在内部,waveout 必须维护使用 waveOutWrite 编写的标题的链接列表;所以,我猜:

hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));

设置链表的上一个/下一个指针或类似的东西。如果你写更多的缓冲区,那么如果你检查指针并且如果它们中的任何一个指向另一个,那么我的猜测很可能是正确的。

网络上的多个来源提到您不需要重复地取消准备/准备相同的标题。如果您在原始示例中注释掉 Prepare/unprepare 标头,那么它似乎可以正常工作,没有任何问题。

【讨论】:

  • 窗口消息的使用在一个重要方面与使用回调函数不同:它们不会在中断时调用,您几乎可以使用它们的任何 API。
  • 在任何情况下,即使您将窗口消息视为回调函数,它仍然会引发相同的异常。删除重复的 prepera/unprepare 标头似乎是解决问题的唯一方法。
  • 我必须查看文档,但据我所知,准备/取消准备调用是必要的。
【解决方案9】:

我通过轮询声音播放和延迟解决了这个问题:

WAVEHDR header = { buffer, sizeof(buffer), 0, 0, 0, 0, 0, 0 };
waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR));
/*
* wait a while for the block to play then start trying
* to unprepare the header. this will fail until the block has
* played.
*/
while (waveOutUnprepareHeader(hWaveOut,&header,sizeof(WAVEHDR)) == WAVERR_STILLPLAYING) 
Sleep(100);
waveOutClose(hWaveOut);

Playing Audio in Windows using waveOut Interface

【讨论】:

    猜你喜欢
    • 2011-01-13
    • 1970-01-01
    • 2016-02-24
    • 1970-01-01
    • 1970-01-01
    • 2012-10-29
    • 2010-09-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多