为什么waveOutWrite()会在调试堆中引发异常

Why would waveOutWrite() cause an exception in the debug heap?

本文关键字:异常 调试 waveOutWrite 为什么      更新时间:2023-10-16

在研究这个问题时,我发现网上多次提到以下场景,在编程论坛上都是未回答的问题。我希望在这里张贴这篇文章至少能记录我的发现。

首先,症状:当运行使用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.reReserved,该指针指向的结构似乎至少包含两个指针作为其前两个成员。第一个指针设置为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 */
}

通常情况下,头由waveCompleteHeader()返回给应用程序,这是wdmaud.dll内部的一个函数。waveCompleteHeaders()试图通过调用GlobalHandle()/GlobalUnlock()和friends来释放waveOutWrite()分配的指针。有时,GlobalHandle()会爆炸,如上所示。

现在,GlobalHandle()爆炸的原因并不像我一开始怀疑的那样是由于堆损坏,而是因为waveOutWrite()返回时没有将内部结构中的第一个指针设置为有效指针。我怀疑它在返回之前释放了该指针指向的内存,但我还没有分解它。

只有当波形播放系统的缓冲区不足时才会出现这种情况,这就是为什么我使用单个标头来再现这种情况。

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

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

注:

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

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

我目前认为这要么是Windows中的错误,要么是音频驱动程序中的错误。持不同意见总是受欢迎的。

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

我可以在我的系统上用你的代码复制这个。我看到了一些类似约翰内斯报道的东西。调用WaveOutWrite后,hdr->reserved通常会保存一个指向已分配内存的指针(其中似乎包含unicode中的wave-out设备名称等)。

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

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

但是,如果我使用回调函数而不是windows消息泵,我就不会导致系统调试断点发生。(WaveOutOpen()中的fdwOpen = CALLBACK_FUNCTION)当我这样做时,我的OnWOMDone处理程序会被另一个线程调用——可能是另一个负责损坏的线程。

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

您并不是唯一一个遇到此问题的人:http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=100589

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

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

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

  • (BYTE*)(标头。保留)=0

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

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

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

示例包括PortAudio、RtAudio和SDL。

我要做的第一件事是检查waveOutX函数的返回值。如果他们中的任何一个失败了——考虑到你描述的情况,这并非不合理——而且你不顾一切地继续下去,那么事情开始出错也就不足为奇了。我的猜测是waveOutWrite在某个时刻返回MMSYSERR_NOMEM。

使用应用程序验证程序来了解发生了什么,如果你做了可疑的事情,它会更早地发现。

查看Wine的源代码可能会有所帮助,尽管Wine可能已经修复了存在的任何错误,也可能Wine中有其他错误。相关文件有dlls/winmm/winmm.c、dlls/winnmm/lolvldrv.c,以及可能的其他文件。祝你好运

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

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

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

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

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

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);

使用waveOut接口在Windows中播放音频