为什么waveOutWrite()会在debugging堆中导致exception?

在研究这个问题的时候,我在网上发现了多个关于以下场景的提及,总是在编程论坛上没有答案的问题。 我希望在这里发表,至less可以logging我的发现。

首先,症状:运行使用waveOutWrite()输出PCMaudio的相当标准的代码时,有时在debugging器下运行时会出现这种情况:

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

在任何人评论之前,是的 – 示例代码播放未初始化的内存。 不要试图将您的扬声器翻过来。

一些debugging显示了以下信息:waveOutPrepareHeader()使用一个指针来填充header.reserved,该指针看上去是一个包含至less两个指针的结构,作为其前两个成员。 第一个指针设置为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()来pipe理正在播放的缓冲区是一个错误。 你会注意到我改变了上面的代码来反映这个build议,以防止更多的人犯同样的错误 – 这没有什么不同。 由waveCompleteHeader()释放的缓冲区不是包含PCM数据的缓冲区,释放PCM缓冲区的责任在于应用程序,并且没有要求以任何特定方式分配。

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

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

您并不孤单与此问题: http : //connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=100589

现在,GlobalHandle()炸弹的原因不是由于堆腐败造成的,因为我首先怀疑这是因为waveOutWrite()返回时没有将内部结构中的第一个指针设置为有效的指针。 我怀疑它返回前释放指针指向的内存,但我还没有反汇编它。

我可以用你的代码在我的系统上重现这一点。 我看到类似于约翰内斯报道的东西。 在对WaveOutWrite的调用之后,hdr-> reserved通常会保存一个指向已分配内存的指针(除了别的以外,它似乎包含了波形输出设备名称)。

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

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

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

所以我认为有一个错误,无论是在Windows或驱动程序,但我认为你可以通过处理WOM_DONE回调函数,而不是Windows消息泵。

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

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

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

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

所以我不同意你的陈述:waveOutWrite()首先存储一个有效的指针; 该指针只会在另一个线程中被破坏。 也许这是相同的线程(mxdmessage),后来试图释放这个堆区,但我还没有找到零字节存储的地步。

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

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

例子包括PortAudio , RtAudio和SDL 。

我要做的第一件事就是检查waveOutX函数的返回值。 如果它们中的任何一个都失败了 – 在你描述的情况下这不是不合理的 – 而且你继续,不管怎么说,事情开始出错是不足为奇的。 我猜测waveOutWrite会在某个时候返回MMSYSERR_NOMEM。

使用应用程序验证器来弄清楚发生了什么,如果你做了一些可疑的事情,它会很早就捕捉到它。

Wine查看Wine的源代码可能会有所帮助,尽管Wine有可能修复了Wine的任何错误,Wine也可能有其他的缺陷。 相关的文件是dlls / winmm / winmm.c,dlls / winmm / lolvldrv.c,还有其他的。 祝你好运!

那么你不允许从回调中调用winmm函数呢? MSDN没有提到有关窗口消息的限制,但窗口消息的使用与回调函数类似。 可能在内部它是作为驱动程序的回调函数实现的,并且回调函数是SendMessage。 在内部,waveout必须维护使用waveOutWrite写入的头的链表; 所以我猜想:

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

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

网络上的多个来源指出,您不需要反复准备/准备相同的标题。 如果您在原始示例中注释掉Prepare / unprepare标题,那么它看起来工作正常,没有任何问题。

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

 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中播放音频