Pull to refresh

Отлаживаем ошибки доступа к памяти с помощью Application Verifier

Reading time 4 min
Views 13K
Хабраюзер burdakovd задал в Q&A задачку про C++, vector и запись в чужую память. Задачка, кроме всего прочего, хороша тем, что на ней можно удобно продемонстрировать, как пользоваться инструментом Application Verifier и находить, кто же портит память.

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

Инструменты


Кроме Application Verifier нам понадобится WinDBG — бесплатный отладчик, входящий в Microsoft Debugging Tools for Windows. Debugging Tools раньше можно было скачать отдельно, а сейчас почему-то только в составе Windows SDK или Windows Driver Kit. Но всё ещё можно скачать отдельно Previous Version, которая для наших задач отлично подойдёт. Ну или вот я выложил свежие версии (6.12.2.633), чтобы не качать весь SDK: dbg_x86.msi, dbg_amd64.msi.

Ещё понадобится Visual C++ (любой версии, новее, пожалуй, VS2003, можно Express) либо компилятор C++ из Windows SDK. Нужен именно компилятор от Microsoft, а не MinGW, потому что нам понадобится отладочная информация в формате PDB, которую понимает WinDBG.

Собираем пример


Исходник берем в упомянутой выше задачке (копия на pastie). Собираем обязательно с отладочной информацией (ключи /Zi или /ZI для компилятора и /DEBUG для компоновщика) и отключенной оптимизацией. Командная строка для сборки из консоли будет выглядеть примерно так:
cl /D_DEBUG /Zi /Od /EHsc /DEBUG /MDd vector_misuse.cpp

Настраиваем Application Verifier

  1. Запускаем AppVerifier с привилегиями администратора.
  2. Выбираем File->Add Application (или Ctrl+A), находим наш misused_vector.exe, жмём Open.
  3. Снимаем все галочки с узла Basic.
  4. Устанавливаем галочку на узел Basic->Heaps. На всякий случай зайдём в свойства этого узла (правый клик на нём->Properties) и убедимся, что галочки напротив Full (в самом верху) и напротив Traces (примерно посередине диалога) включены. Если не включена — включаем и жмём OK.
  5. Жмем кнопку Save.

Настраиваем отладчик

  1. Идем в File->Symbol File Path… и вписываем туда строку srv*c:\mysymbols*http://msdl.microsoft.com/download/symbols. Это означает, что отладчик будет сначала искать символы в каталоге c:\mysymbols, а если не найдет — скачает из интернета из Microsoft Symbol Store. Публичные символы нужны, чтобы видеть красивые коллстеки. Можно использовать команду .symfix+ c:\mysymbols, но уже после того, как приложение будет загружено в отладчик.
  2. В File->Open Executable… (Ctrl+E) выбираем наш misused_vector.exe. Соглашаемся с предложением сохранить workspace. Отладчик загрузит образ в память, но не запустит исполнение.
  3. Запускаем пример на исполнение — Debug->Go (или F5, или g в приглашении отладчика).
Если вы ранее не работали с WinDBG имеет смысл заглянуть в меню View->Font и настроить шрифт. Тот, что установлен по-умолчанию, может показаться вам совершенно невменяемым (а может и не показаться).

Находим причину падения


После того, как мы запустим программу, она упадёт с Access Violation.

Смотрим стек — View Call Stack (или Alt+6 или kp в приглашении) и видим, что упало в функции f, на втором уровне вложенности. Чтобы в окошке Call Stack было видны аргументы функций жмём кнопку Source args. Чтобы было видно ссылки на строки кода жмём кнопку Source. Команда kp выведет эту информацию в окошко Command отладчика. Также должно открыться окошко с исходным текстом и в нём подсветиться текущая строка.

Ок, мы видим, что проблема в строке
v[i] += f(x / 2);
но что именно с ней не так? На этот вопрос нам ответит отладчик, если его правильно спросить. Пишем в приглашение !analyze -v и нажимаем Enter.

Отладчик вывалит нам простыню текста, из которой нам интересны следующие вещи:
DEFAULT_BUCKET_ID: INVALID_POINTER_READ — попытка прочитать по невалидному указателю
READ_ADDRESS: 060a0ff4 — собственно сам адрес, по которому мы пытались прочитать.

Также будет распечатан коллстек, который мы уже видели и даже кусок исходника с помеченной строкой, где случилось исключение.

Это всё конечно очень интересно, но хотелось бы узнать, а почему эту память нельзя читать? Благодаря настройкам, которые мы сделали в AppVerifier, система при каждом выделении и освобождении памяти собирала коллстеки и бережно сохраняла, чтобы потом по нашей просьбе любезно предоставить.

Вводим в приглашение отладчика !heap -p -a 060a0ff4 (тут вам нужно будет подставить тот адрес, который будет у вас в READ_ADDRESS, он, скорее всего, будет отличаться.На это отладчик нам ответит, что адрес этот принадлежит такому-то хипу, такого-то размера, который был освобожден (in free-ed allocation) вот таким колл-стеком:
    5da190b2 verifier!AVrfDebugPageHeapFree+0x000000c2
    77cd1464 ntdll!RtlDebugFreeHeap+0x0000002f
    77c8ab3a ntdll!RtlpFreeHeap+0x0000005d
    77c33472 ntdll!RtlFreeHeap+0x00000142
    75cc14dd kernel32!HeapFree+0x00000014
    5c677f59 MSVCR100D!_free_base+0x00000029
    5c687a4e MSVCR100D!_free_dbg_nolock+0x000004ae
    5c687560 MSVCR100D!_free_dbg+0x00000050
    5c686629 MSVCR100D!operator delete+0x000000b9
    00f71af0 vector_misuse!std::allocator<int>::deallocate+0x00000010
    00f7193b vector_misuse!std::vector<int,std::allocator<int> >::reserve+0x0000010b
    00f716db vector_misuse!std::vector<int,std::allocator<int> >::_Reserve+0x0000005b
    00f714c4 vector_misuse!std::vector<int,std::allocator<int> >::push_back+0x000000c4
    00f712dc vector_misuse!f+0x0000002c
    00f7130b vector_misuse!f+0x0000005b
    00f7130b vector_misuse!f+0x0000005b
    00f7134b vector_misuse!main+0x0000000b
    00f7323f vector_misuse!__tmainCRTStartup+0x000001bf
    00f7306f vector_misuse!mainCRTStartup+0x0000000f
    75cc33ca kernel32!BaseThreadInitThunk+0x0000000e
    77c39ed2 ntdll!__RtlUserThreadStart+0x00000070
    77c39ea5 ntdll!_RtlUserThreadStart+0x0000001b

Таким образом мы узнали, что на третьем уровне вложенности рекурсии, при очередном vector::push_back вектор решил изменить свой размер (vector::reserve), что привело к переаллокации этого самого вектора (std::allocator::deallocate и дальше по стеку) и последующему доступу к освобождённой памяти при возврате на второй уровень.

Итого


С написанием красивых заключений и подытоживаний у меня всегда были проблемы, поэтмоу их не будет. Люди умные, сами сделают себе нужные выводы :)

Спасибо за внимание. :)
Tags:
Hubs:
+30
Comments 11
Comments Comments 11

Articles