Почему waveOutWrite() вызывает исключение в куче отладки?
Исследуя эту проблему, я обнаружил несколько упоминаний о следующем сценарии в Интернете, неизменно как оставшиеся без ответа вопросы на форумах по программированию. Я надеюсь, что размещение этого здесь, по крайней мере, послужит документированием моих выводов.
Во-первых, симптом: во время работы довольно стандартного кода, который использует 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 */
}
Обычно заголовок возвращается приложению waveCompleteHeader(), функцией, внутренней для wdmaud.dll. waveCompleteHeader() пытается освободить указатель, выделенный waveOutWrite(), вызывая GlobalHandle()/GlobalUnlock() и друзей. Иногда GlobalHandle () бомбит, как показано выше.
Теперь причина в том, что бомбы GlobalHandle () не связаны с повреждением кучи, как я сначала подозревал, а потому, что waveOutWrite() возвратился без установки действительного указателя на первый указатель во внутренней структуре. Я подозреваю, что это освобождает память, на которую указывает этот указатель перед возвратом, но я еще не разобрал ее.
Похоже, это происходит только тогда, когда в системе волнового воспроизведения недостаточно буферов, поэтому я использую один заголовок для воспроизведения этого.
На данный момент у меня есть довольно веское доказательство того, что это ошибка в моем приложении - в конце концов, мое приложение даже не запущено. Кто-нибудь видел это раньше?
Я вижу это на Windows XP SP2. Звуковая карта от SigmaTel, версия драйвера 5.10.0.4995.
Заметки:
Чтобы избежать путаницы в будущем, я хотел бы отметить, что ответ, предполагающий, что проблема заключается в использовании malloc()/free() для управления воспроизводимыми буферами, просто неверен. Вы заметите, что я изменил приведенный выше код, чтобы отразить это предложение, чтобы больше людей не совершали одну и ту же ошибку - это не имеет значения. Буфер, освобождаемый waveCompleteHeader(), не является буфером, содержащим данные PCM, ответственность за освобождение буфера PCM лежит на приложении, и не требуется, чтобы он был выделен каким-либо определенным образом.
Кроме того, я удостоверяюсь, что ни один из вызовов API waveOut, которые я использую, не завершился неудачей.
В настоящее время я предполагаю, что это либо ошибка в Windows, либо в драйвере аудио. Особые мнения всегда приветствуются.
9 ответов
Вы не одиноки с этой проблемой: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=100589
Теперь причина в том, что бомбы GlobalHandle() не связаны с повреждением кучи, как я сначала подозревал, а потому, что waveOutWrite() возвратился без установки действительного указателя на первый указатель во внутренней структуре. Я подозреваю, что это освобождает память, на которую указывает этот указатель перед возвратом, но я еще не разобрал ее.
Я могу воспроизвести это с вашим кодом в моей системе. Я вижу нечто похожее на то, что сообщил Йоханнес. После вызова WaveOutWrite hdr->reserved обычно содержит указатель на выделенную память (который, помимо прочего, содержит имя устройства для вывода данных в юникоде).
Но иногда, после возврата из WaveOutWrite(), байт, на который указывает hdr->reserved
устанавливается в 0. Обычно это наименее значимый байт этого указателя. Остальные байты в hdr->reserved
все в порядке, и блок памяти, на который он обычно указывает, все еще выделен и не поврежден.
Вероятно, это засоряется другим потоком - я могу поймать изменение с условной точкой останова сразу после вызова WaveOutWrite(). И точка останова системной отладки происходит в другом потоке, а не в обработчике сообщений.
Тем не менее, я не могу заставить точку останова отладки системы произойти, если я использую функцию обратного вызова вместо насоса сообщений Windows. (fdwOpen = CALLBACK_FUNCTION
в WaveOutOpen()) Когда я делаю это таким образом, мой обработчик OnWOMDone вызывается другим потоком - возможно, тем, который в противном случае ответственен за повреждение.
Поэтому я думаю, что есть ошибка, либо в Windows, либо в драйвере, но я думаю, что вы можете обойти это, обработав WOM_DONE с помощью функции обратного вызова вместо насоса сообщений Windows.
Я вижу ту же проблему и сам провел некоторый анализ:
waveOutWrite () выделяет (т.е. GlobalAlloc) указатель на область кучи размером 354 байта и правильно сохраняет его в области данных, на которую указывает header.reserved.
Но когда эта область кучи должна быть снова освобождена (в waveCompleteHeader (), согласно вашему анализу; у меня нет символов для wdmaud.drv), младший байт указателя был установлен равным нулю, что делает недействительным указатель (пока куча еще не повреждена). Другими словами, происходит что-то вроде:
- (BYTE *) (header.reserved) = 0
Поэтому я не согласен с вашими утверждениями в одной точке: waveOutWrite() сначала сохраняет действительный указатель; указатель будет позже поврежден только из другого потока. Возможно, это тот же поток (mxdmessage), который позже пытается освободить эту область кучи, но я пока не нашел точку, где хранится нулевой байт.
Это происходит не очень часто, и одна и та же область кучи (тот же адрес) была успешно выделена и освобождена ранее. Я совершенно уверен, что это ошибка где-то в системном коде.
Первым делом я бы проверил возвращаемые значения из функций waveOutX. Если какой-либо из них терпит неудачу - что не является необоснованным, учитывая сценарий, который вы описываете - и вы продолжаете в любом случае, то неудивительно, что все начинает идти не так, как надо. Я предполагаю, что waveOutWrite возвращает MMSYSERR_NOMEM в какой-то момент.
Не уверен насчет этой конкретной проблемы, но рассматривали ли вы возможность использования кроссплатформенной аудиобиблиотеки более высокого уровня? С аудио-программированием в Windows есть много странностей, и эти библиотеки избавят вас от головной боли.
Может быть полезно взглянуть на исходный код для Wine, хотя возможно, что Wine исправил любую существующую ошибку, и также возможно, что в Wine есть другие ошибки. Соответствующие файлы: dlls / winmm / winmm.c, dlls / winmm / lolvldrv.c и, возможно, другие. Удачи!
Я решил проблему, опросив воспроизведение звука и задержки:
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);
Воспроизведение аудио в Windows с использованием интерфейса waveOut
Используйте Application Verifier, чтобы выяснить, что происходит, если вы делаете что-то подозрительное, оно поймает это намного раньше.
Как насчет того, что вам не разрешено вызывать функции winmm из-за обратного вызова? MSDN не упоминает такие ограничения для оконных сообщений, но использование оконных сообщений аналогично функции обратного вызова. Возможно, внутренне это реализовано как функция обратного вызова от драйвера, и этот обратный вызов делает SendMessage. Внутренне waveout должен поддерживать связанный список заголовков, которые были написаны с использованием waveOutWrite; Итак, я думаю, что:
hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));
устанавливает предыдущий / следующий указатели связанного списка или что-то вроде этого. Если вы напишите больше буферов, то, если вы проверите указатели и если какие-либо из них будут указывать друг на друга, то мое предположение, скорее всего, верно.
В нескольких источниках в Интернете упоминается, что вам не нужно повторно готовить / подготавливать одни и те же заголовки. Если вы закомментируете заголовок Prepare / Unprepare в исходном примере, то, похоже, он работает без проблем.