Хабраюзер 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 для компоновщика) и отключенной оптимизацией. Командная строка для сборки из консоли будет выглядеть примерно так:
После того, как мы запустим программу, она упадёт с Access Violation.
Смотрим стек — View Call Stack (или Alt+6 или kp в приглашении) и видим, что упало в функции f, на втором уровне вложенности. Чтобы в окошке Call Stack было видны аргументы функций жмём кнопку Source args. Чтобы было видно ссылки на строки кода жмём кнопку Source. Команда kp выведет эту информацию в окошко Command отладчика. Также должно открыться окошко с исходным текстом и в нём подсветиться текущая строка.
Ок, мы видим, что проблема в строке
Отладчик вывалит нам простыню текста, из которой нам интересны следующие вещи:
DEFAULT_BUCKET_ID: INVALID_POINTER_READ — попытка прочитать по невалидному указателю
READ_ADDRESS: 060a0ff4 — собственно сам адрес, по которому мы пытались прочитать.
Также будет распечатан коллстек, который мы уже видели и даже кусок исходника с помеченной строкой, где случилось исключение.
Это всё конечно очень интересно, но хотелось бы узнать, а почему эту память нельзя читать? Благодаря настройкам, которые мы сделали в AppVerifier, система при каждом выделении и освобождении памяти собирала коллстеки и бережно сохраняла, чтобы потом по нашей просьбе любезно предоставить.
Вводим в приглашение отладчика
Таким образом мы узнали, что на третьем уровне вложенности рекурсии, при очередном vector::push_back вектор решил изменить свой размер (vector::reserve), что привело к переаллокации этого самого вектора (std::allocator::deallocate и дальше по стеку) и последующему доступу к освобождённой памяти при возврате на второй уровень.
С написанием красивых заключений и подытоживаний у меня всегда были проблемы, поэтмоу их не будет. Люди умные, сами сделают себе нужные выводы :)
Спасибо за внимание. :)
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
- Запускаем AppVerifier с привилегиями администратора.
- Выбираем File->Add Application (или Ctrl+A), находим наш misused_vector.exe, жмём Open.
- Снимаем все галочки с узла Basic.
- Устанавливаем галочку на узел Basic->Heaps. На всякий случай зайдём в свойства этого узла (правый клик на нём->Properties) и убедимся, что галочки напротив Full (в самом верху) и напротив Traces (примерно посередине диалога) включены. Если не включена — включаем и жмём OK.
- Жмем кнопку Save.
Настраиваем отладчик
- Идем в File->Symbol File Path… и вписываем туда строку
srv*c:\mysymbols*http://msdl.microsoft.com/download/symbols
. Это означает, что отладчик будет сначала искать символы в каталоге c:\mysymbols, а если не найдет — скачает из интернета из Microsoft Symbol Store. Публичные символы нужны, чтобы видеть красивые коллстеки. Можно использовать команду.symfix+ c:\mysymbols
, но уже после того, как приложение будет загружено в отладчик. - В File->Open Executable… (Ctrl+E) выбираем наш misused_vector.exe. Соглашаемся с предложением сохранить workspace. Отладчик загрузит образ в память, но не запустит исполнение.
- Запускаем пример на исполнение — Debug->Go (или F5, или g в приглашении отладчика).
Находим причину падения
После того, как мы запустим программу, она упадёт с 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 и дальше по стеку) и последующему доступу к освобождённой памяти при возврате на второй уровень.
Итого
С написанием красивых заключений и подытоживаний у меня всегда были проблемы, поэтмоу их не будет. Люди умные, сами сделают себе нужные выводы :)
Спасибо за внимание. :)