Дело об остановившихся часах
Одной из наиболее заметных новых функций ОС Windows Vista наряду с эффектом прозрачности интерфейса Aero является боковая панель с набором стандартных мини-приложений, среди которых - часы, RSS-канал и программа просмотра фотографий. Такие факторы, как удобство размещения востребованной информации на рабочем столе и простота разработки, предопределили появление тысяч мини-приложений от сторонних производителей, которые распространяются через различные веб-узлы наподобие коллекции мини-приложений Windows Vista. Из любопытства я загрузил и установил несколько таких приложений, а некоторые даже включил в стандартную конфигурацию боковой панели, и до определенного момента не испытывал никаких проблем. Несколько же дней назад, установив очередную порцию мини-приложений, я обнаружил, что мини-приложение «Часы» стороннего производителя перестало обновляться. Пришлось приступить к расследованию.
Поскольку во всех остальных отношениях моя система работала совершенно нормально, в первую очередь следовало проверить конфигурацию боковой панели. Щелкнув правой кнопкой мыши в области боковой панели, я выбрал в контекстном меню пункт Properties (Свойства), но вместо диалогового окна настройки боковой панели увидел сообщение о её сбое.
Поскольку мини-приложения исполняются в рамках общих процессов боковой панели, я подумал, что остановка часов и последующий сбой могли явиться результатами повреждения содержимого памяти, отведенного процессу боковой панели. Для проверки этого предположения сбой нужно было проанализировать. На тот случай, если пользователь согласится отправить сведения об ошибке специалистам корпорации Майкрософт, служба отчетов об ошибках Windows (WER) создает файл аварийной копии памяти, содержащий состояние процесса на момент сбоя. Чтобы узнать местоположение этого файла, я открыл область View Details (Подробности).
Путь к файлу аварийной копии памяти WERD8EE.tmp.mdmp указывается в конце списка путей. Запустив служебную программу Windbg из пакета инструментов отладки Майкрософт для Windows, я открыл этот файл. При открытии файлов такого типа программа Windbg сразу указывает на инструкцию, которая в конечном счете привела к сбою. В данном случае это была операция копирования данных из памяти, вызванная средой выполнения Microsoft C (Msvcrt).
Правая часть строки, где показана упомянутая инструкция, свидетельствует о том, что копирование производилось по адресу назначения 0. Этот адрес, также называемый указателем NULL, в условиях нехватки ресурсов памяти обычно возвращают функции выделения памяти. По умолчанию в процессах Windows этот адрес считается недопустимым (приложения могут «вручную» выделять память для чтения и записи по нулевому адресу, но на практике этого не происходит). Ссылка на нулевой адрес сама по себе не могла быть однозначным свидетельством нехватки памяти как причины сбоя, однако такой сценарий казался вероятным.
Далее я приступил к анализу кода, исполнение которого предшествовало сбою. Так я намеревался узнать, что выступило источником передачи указателя NULL среде исполнения: мини-приложение или боковая панель. Для этого я открыл диалоговое окно трассировки стека в программе Windbg.
Надо сказать, что ранее я настроил в программе Windbg путь к серверу символов Майкрософт, так что имена внутренних функций в образах Windows мне были известны, а значит, мне было проще разобраться в содержимом файла аварийной копии памяти. Судя по функциям, указанным в трассировке стека, во время сбоя боковая панель запрашивала версию «пакета». Не очень понятно, что в терминологии боковой панели называется пакетом, но на основании трассировки можно было предположить, что виновником сбоя выступила именно боковая панель, а не мини-приложение.
Так что же, боковой панели не хватило памяти? Есть несколько видов нехватки системных ресурсов, потенциально приводящих к сбоям выделения памяти. Например, в системе могла закончиться память, доступная для выделения, процесс мог исчерпать запасы памяти в рамках своего адресного пространства, а внутренняя куча могла достичь максимального размера.
Я начал с проверки выделенной памяти - сделать это было проще всего. Предел выделенной виртуальной памяти (показатель «всего памяти, доступной для выделения») складывается из суммарного объема файлов подкачки и большей части объема физической памяти. При нехватке памяти, доступной для выделения, система контроля нехватки ресурсов ОС Windows Vista предупреждает пользователя об этом, выводя список процессов, потребляющих наибольший объем памяти и предлагая закрыть некоторые из них. Такого предупреждения я не увидел, что заставило меня усомниться в том, что причиной проблемы является нехватка доступной для выделения памяти. Все же на всякий случай я открыл диалоговое окно System Information (Сведения о системе) программы Process Explorer.
Памяти, доступной для выделения, оказалось предостаточно, что и требовалось доказать. Далее я обратился к потреблению боковой панелью виртуальной памяти. Утечки памяти происходят в случаях, когда процесс выделяет виртуальную память, помещает в неё данные, производит с ними операции, но не удаляет данные по завершении работы с ними. Виртуальная память, которую процессы выделяют для хранения собственных данных, учитывается счетчиком Private Bytes (Байт исключительного пользования). Соответственно, я открыл программу Process Explorer и добавил в представление столбец Private Bytes.
В 32-разрядных системах Windows в распоряжение процессов по умолчанию предоставляется адресное пространство объемом 2 ГБ. Таким образом, максимально возможное значение счетчика Private Bytes приближается к 2 ГБ. Именно такое значение показал процесс боковой панели с идентификатором 4680. Итак, причина проблемы стала известна - утечка памяти, произошедшая в боковой панели, привела к заполнению адресного пространства, которое, в свою очередь, предопределило ошибку выделения памяти. Следствием этой ситуации явились появление ссылки на указатель NULL и сбой. Мне подумалось, что часы остановились именно в результате того, что адреса в адресном пространстве закончились, и мини-приложение не смогло выделить необходимые ресурсы для обновления графики.
Теперь требовалось выяснить, какое мини-приложение стало источником утечки - для меня было совершенно не очевидно, что это именно остановившиеся часы. Боковая панель состоит из двух процессов: Sidebar.exe для встроенных мини-приложений Windows и дочернего процесса Sidebar.exe для мини-приложений сторонних производителей. На данном этапе мне было известно, что утечка памяти имеет место в мини-приложении стороннего производителя или в боковой панели из-за такого приложения, но поскольку на моей машине было установлено сразу несколько мини-приложений, определить виновника однозначно сразу не удавалось. К сожалению, боковая панель не позволяет отследить потребление памяти тем или иным мини-приложением (да и вообще каким бы то ни было ресурсом), так что для локализации утечки мне пришлось действовать вручную.
Перезапустив боковую панель, я удалил все мини-приложения сторонних производителей и начал устанавливать их по одному, запуская каждое в течение минуты-двух и отслеживая значение счетчика Private Bytes (Байт исключительного пользования) боковой панели. Чтобы упростить обнаружение повышенного потребления памяти, я добавил в представление программы Process Explorer столбец Private Bytes Delta отображающий изменение размера выделенной памяти, и после запуска очередного мини-приложения в этом столбце стали постоянно появляться положительные значения, а значит, источник утечки был найден.
Установив виновника, его можно было банально удалить и считать дело закрытым, но мне было интересно узнать, каким образом мини-приложению удалось создать в боковой панели такую утечку, которая продолжала давать о себе знать даже после его удаления.
Я перешел в каталог установки мини-приложения и открыл его HTML-файл. Как выяснилось, мини-приложение состоит из тридцати с лишним строк довольно простого кода на языке JavaScript, в котором ничего подозрительного не наблюдалось. Чтобы найти код, являющийся источником проблемы, я начал заключать отдельные его фрагменты в комментарии и наблюдать за поведением боковой панели до тех пор, пока утечка не прекратилась. Оказалось, утечка проистекает из функции, с помощью которой мини-приложение каждые 10 секунд обновляет графику. Функция эта вызывает метод RemoveObjects объекта фона боковой панели, а затем при помощи метода AddImageObject фона вновь добавляет графику и текст. Вот как выглядит упрощенная версия кода этой функции:
Претензий к способу обращения к интерфейсам API не возникло, а значит, утечка связана с кодом самой боковой панели - правда, поиск в Интернете не дал никаких результатов касаемо утечек в объекте фона. Если утечка памяти в интерфейсах API действительно существует, почему о ней ничего не известно? Я просмотрел исходный код других установленных в системе мини-приложений и обнаружил, что ни одно из них не обращается к этим интерфейсам - вот почему такая утечка не распространена. Впрочем, судя по комментариям в коллекции мини-приложений Windows Vista, другие пользователи также заметили утечку, непредумышленно спровоцированную авторами мини-приложения.
Итак, установив, что проблема связана не с мини-приложением, а с интерфейсом API, который и создает утечку памяти, я создал новую запись в базе данных ошибок Windows и закрыл дело.