Почему 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 есть много странностей, и эти библиотеки избавят вас от головной боли.

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

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

Другие вопросы по тегам