Итак, провели мы сеанс коллективной отладки с
ger_kar'ом. Я расставил брекпоинты в отладчике, затем он играл и пытался спровоцировать сбой, затем я подключился и оценил улов в отладчике.
Если коротко, то есть два важных вывода:
- Ошибка выбрасывается методом SetCurrentPosition, хотя из всех методов он один в документации не имеет такой ошибки (какую имеем) среди списка возможных ошибок.
- Фактически я оказался прав в своём предположении, которое делал ранее:
Хакер писал(а):Но вскоре понял, что ни в каких if-блоках проверок юзермодной части DirectSound такой код ошибки не фигурирует, а все обращения так или иначе стекаются к вызову DeviceIoControl и обращению к ядерной части аудиоподсистемы (KS — Kernel Streaming). И я почти уверен, что ноги у сбоя растут оттуда.
Теперь подробнее:
Во-первых, я метод
SetCurrentPosition вообще до этого не изучал и реверсить не пытался, свято веря в то, что он такую ошибку выкидывать не может. Как только мы выяснили, что дело в нём — я залез его изучать и оказалось, что его «начинка» вообще существенно проще, чем у остальных методов, которые я перед этим начинал реверсить.
Но опять же, сохраняется тот же принцип: какие-то проверочные if-блоки, которые я вижу, если и есть, то их деятельность направлена на формирование совершенно других ошибок (другие коды), и все внутренние проверки, которые там есть, не могли бы дать нам
E_FAIL — значит надо было рыть глубже и смотреть вызываемые («вложенные») функции и методы, потому что ошибка может всплыть только оттуда.
Так я в скором времени, даже ничего не реверся, а беглым взглядом дошёл до уже знакомой функции
PostDevIoctl, которая, как понятно из названия, является DirectSound-овской обёрткой над
DeviceIoControl. На выходе из неё был поставлен условный брекпоинт, чтобы словить момент возврата при сбойном вызове, но эта тактика ничего дала — ошибка упорно не хотела возникать.
Тогда я сделал предположение, что наличие присоединённого отладчика как такового, а в большей степени — наличие условного брекпоинта портит всю картину и оказывает влияние на протекающие процессы, поэтому мы и не имеем ошибки. Тут надо понимать, что «conditional breakpoint» — это изнутри самый обычный брекпоинт, просто отладчик постоянно делает проверку условия. Это значит, что много раз в секунду процесс напарывается на этот брекпоинт и между отлаживаемым процессом и отладчиком постоянно шлётся поток отладочных событий (
debug events), то есть имеет место межпроцессное взаимодействие, которое видимо и портит всю картину. И было решено отказаться от условных брекпоинтов в пользу безусловных — безусловный поставить на тот участок кода, который выполняется уже после того, как
DeviceIoControl вернёт сбой. Тогда никаких лишних IPC между отладчиком и процессом-жертвой не будет до тех пор, пока ошибка не будет поймана. Результат не заставил себя ждать: ошибка была поймана менее чем через минуту.
Вот обратная трассировка стека, демонстрирующая цепочку вызовов от обращения к DirectSound из VB-кода до обращения к
DeviceIoControl из нутра DirectSound:
stk_exp.png
По сути, в прямом порядке:
- Функция TNTAdd вызывает функцию SoundFire.
- SoundFire делает вызов DSBFire(FireInd).SetCurrentPosition 0
- Со стороны библиотеки DSound.dll реализацией интерфейса IDirectSoundBuffer8 является шаблонный класс CImpDirectSoundBuffer<CDirectSoundSecondaryBuffer>, у которого метод SetCurrentPosition является обёрткой над одноименным методом класса CDirectSoundSecondaryBuffer.
- CDirectSoundSecondaryBuffer::SetCurrentPosition вызывает CKsSecondaryRenderWaveBuffer::SetCursorPosition
- Он в свою очередь делает вызов к CKsRenderPin::SetCursorPosition
- CKsRenderPin::SetCursorPosition вызывает функцию KsSetProperty
- KsSetProperty вызывает PostDevIoctl
- PostDevIoctl вызывает DeviceIoControl — она то возвращает сбой, код ошибки, получаемый от GetLastError при этом равен ERROR_GEN_FAILURE (0x1F)
- По этому поводу PostDevIoctl сравнивает полученный код ошибки с некоторыми заранее известными кодами (например с ERROR_IO_PENDING, который вообще-то означает не ошибку вовсе, а тот факт, что операция будет завершена асинхронно), и если ни один код не подходит, вызывается WIN32ERRORtoHRESULT, которая и конвертирует код ERROR_GEN_FAILURE (0x1F) в E_FAIL (0x80004005) — дальше этот код просто всплывает наверх вплоть до VB, где превращается в ошибку.
Вот, собственно, код
PostDevIoctl, которая является обёрткой над
DeviceIoControl и вызывает оную:
2_.png
test eax, eax // jz dsound.73ed3d7c — это собственно прыжок на блок обработки ошибки в случае, если
DeviceIoControl сбойнула.
Вот этот блок (начиная с вызова
GetLastError):
3.png
(Вот тут-то мы ошибку впервые и поймали). Обратите внимание справа на то, LastError = ERROR_GEN_FAILURE.
MSDN даёт вот такое толкование этой ошибке:
May be used to indicate that the device has stopped responding (hung) or a general failure has occurred on the device. The device may need to be manually reset.
Ещё раз напоминаю, что эту ошибку выдаёт
DeviceIoControl, которая является мостом между пользовательской частью и ядерной частью, позволяющим послать драйверу IRP типа
IRP_MJ_DEVICE_CONTROL. Кто не знает: в ядре драйверы принимают входящие IRP и отсылают IRP другим драйверам (точнее устройствам), аналогично тому, как в user-mode WindowProc-и принимают оконные сообщения и шлют сообщения другим оконным классам (точнее окнам).
На всякий случай, я пустил отладчик дальше, чтобы посмотреть, какой был индекс буфера при обращении к объекту-буферу:
obj_ind.png
Как видно отсюда,
FireInd = 1, то есть сбой произошёл при обращении к буферу-клону, а не к буферу-оригиналу. Не могу точно сказать, что все сбои касаются клонов, но интуиция подсказывает, что механизм клонирования может быть замешан в этом.
Если кто не понял,
4183B8 это адрес глобальной переменной
FireInd и по нему содержится число 1, а
4183AC — это адрес поля
pvData структуры
SAFEARRAY переменной-массива
DSBFire.
mov eax, [ecx*4+edx] — обращение к элементу массива (получение ссылки на объект-буфер).
Ну и сама ошибка после того, как отладчик отпустил отлаживаемый процесс в дальнейшее свободное выполнение:
err.png
Там везде устанавливается позиция на 0, то есть на начало. Такого ограничения быть не может.
Я хочу обратить внимание всё-таки, что все пойманные ошибки касаются буферов, подвергающихся клонированию, а в том единичном случае, что мы поймали под отладчиком, это был ещё и второй экземпляр буфера.
Дело не в том, что там везде 0, а в том, что возможно между буфером-клоном и буфером-оригиналом сохраняется какая-то связь (общая память под данные) и пока оригинал играется, нельзя у клона трогать указатель. Например потому, что перестановка указателя с конца на начало требует выгрузки из невыгружаемого пула той части буфера, которая соответствует концу звука, и загрузки туда той части буфера, которая соответствует началу, а это, к примеру, не может быть сделано, потому что у другого экземпляра конец ещё проигрывается и не может быть выгружен.
Естественно, это скорее всего баг или очень узко-специфическая недокументированная особенность, но всё-таки хочется разобраться.
У вас нет доступа для просмотра вложений в этом сообщении.