Почему waveOutWrite () вызывает исключение в куче отладки? - PullRequest
13 голосов
/ 12 октября 2008

Во время исследования этой проблемы я обнаружил несколько упоминаний о следующем сценарии в Интернете, неизменно как оставшиеся без ответа вопросы на форумах по программированию. Я надеюсь, что размещение этого здесь, по крайней мере, послужит документированием моих выводов.

Во-первых, симптом: во время работы довольно стандартного кода, который использует 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 ]

3 голосов
/ 05 февраля 2009

Теперь причина того, что GlobalHandle () бомбы не из-за кучи коррупции, как я и подозревал сначала - это потому что waveOutWrite () возвращается без установка первого указателя в внутренняя структура для правильного указателя. Я подозреваю, что это освобождает память указывает на этот указатель перед возвращаюсь, но я не разобрал это еще.

Я могу воспроизвести это с вашим кодом в моей системе. Я вижу нечто похожее на то, что сообщил Йоханнес. После вызова WaveOutWrite hdr-> reserved обычно содержит указатель на выделенную память (который, помимо прочего, содержит имя устройства для вывода сигнала в юникоде).

Но иногда, после возврата из WaveOutWrite (), байт, на который указывает hdr->reserved, устанавливается в 0. Обычно это наименее значимый байт этого указателя. Остальные байты в hdr->reserved в порядке, и блок памяти, на который он обычно указывает, все еще выделен и не поврежден.

Вероятно, это засоряется другим потоком - я могу поймать изменение с условной точкой останова сразу после вызова WaveOutWrite (). И точка останова системной отладки происходит в другом потоке, а не в обработчике сообщений.

Однако я не могу заставить точку останова отладки системы возникнуть, если я использую функцию обратного вызова вместо насоса сообщений Windows. (fdwOpen = CALLBACK_FUNCTION в WaveOutOpen ()) Когда я делаю это таким образом, мой обработчик OnWOMDone вызывается другим потоком - возможно, тем, который в противном случае ответственен за повреждение.

Так что я думаю, что есть ошибка, либо в Windows, либо в драйвере, но я думаю, что вы можете обойтись, обрабатывая WOM_DONE с функцией обратного вызова вместо насоса сообщений Windows.

2 голосов
/ 31 января 2009

Вы не одиноки с этой проблемой: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=100589

1 голос
/ 16 января 2009

Я вижу ту же проблему и сам провел некоторый анализ:

waveOutWrite () выделяет (т.е. GlobalAlloc) указатель на область кучи размером 354 байта и правильно сохраняет его в области данных, на которую указывает header.reserved.

Но когда эту область кучи нужно снова освободить (в waveCompleteHeader (), согласно вашему анализу; у меня нет символов для wdmaud.drv), младший байт указателя был установлен в ноль , таким образом, лишает законной силы указатель (пока куча еще не повреждена). Другими словами, происходит что-то вроде:

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

Так что я не согласен с вашими утверждениями в одной точке: waveOutWrite () сначала сохраняет действительный указатель; указатель будет позже поврежден только из другого потока. Возможно, это тот же поток (mxdmessage), который позже пытается освободить эту область кучи, но я пока не нашел точку, где хранится нулевой байт.

Это происходит не очень часто, и одна и та же область кучи (тот же адрес) была успешно выделена и освобождена ранее. Я совершенно уверен, что это ошибка где-то в системном коде.

0 голосов
/ 18 марта 2016

Я решил проблему путем опроса воспроизведения звука и задержек:

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

0 голосов
/ 20 августа 2009

А как насчет того, что вам не разрешено вызывать функции winmm из-за обратного вызова? MSDN не упоминает такие ограничения для оконных сообщений, но использование оконных сообщений аналогично функции обратного вызова. Возможно, внутренне это реализовано как функция обратного вызова от драйвера, и этот обратный вызов делает SendMessage. Внутренне waveout должен поддерживать связанный список заголовков, которые были написаны с использованием waveOutWrite; Итак, я думаю, что:

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

устанавливает предыдущие / следующие указатели в связанном списке или что-то в этом роде. Если вы напишите больше буферов, то, если вы проверите указатели и если один из них будет указывать друг на друга, то, скорее всего, мое предположение будет правильным.

В нескольких источниках в Интернете упоминается, что вам не нужно повторно готовить / подготавливать одни и те же заголовки. Если вы закомментируете заголовок Prepare / Unprepare в исходном примере, то он, похоже, работает без проблем.

0 голосов
/ 31 января 2009

Может быть полезно взглянуть на исходный код для Wine , хотя вполне возможно, что Wine исправил любую существующую ошибку, а также возможно, что в Wine есть другие ошибки. Соответствующие файлы: dlls / winmm / winmm.c, dlls / winmm / lolvldrv.c и, возможно, другие. Удачи!

0 голосов
/ 30 января 2009

Используйте Application Verifier, чтобы выяснить, что происходит, если вы делаете что-то подозрительное, оно поймает это намного раньше.

0 голосов
/ 17 октября 2008

Первое, что я хотел бы сделать, это проверить возвращаемые значения из функций waveOutX. Если какой-либо из них терпит неудачу - что не является необоснованным, учитывая сценарий, который вы описываете - и вы продолжаете в любом случае, то неудивительно, что все начинает идти не так. Я думаю, что waveOutWrite возвращает MMSYSERR_NOMEM в какой-то момент.

0 голосов
/ 14 октября 2008

Не уверен насчет этой конкретной проблемы, но рассматривали ли вы возможность использования кроссплатформенной аудиобиблиотеки более высокого уровня? С аудио-программированием в Windows есть много странностей, и эти библиотеки избавят вас от головной боли.

Примеры включают PortAudio , RtAudio и SDL .

...