Pull to refresh

Comments 21

Плакать из-за отсутствия conditional breakpoints.

До 2015 — right click on breakpoint, после 2015 — hover on breakpoint and click gear icon. Не?

Я имею ввиду, что их нету в api monitor

А вы читайте внимательнее, а потом советы давайте. Автор не поо VS.
Прочитал по диагонали, так что не знаю, была ли возможность логировать стек, но в таких ситуациях стнадартный алгоритм такой:
1. Хукаем API функции создания объектов, логируем стек и хендл.
2. Хукаем удаление объектов и собираем утекшие хендлы.
3. Смотрим стек первого утекшего хендла, правим код и повторяем сначала.
Трехминутное гугление выдало и библиотеку для работы со StackFrame, и библиотеку для хуков WinApi.
Реально все просто, непонятно зачем было огород городить.
У меня была мысль написать программу, которая бы подменяла адреса нативных функций на мои, не был уверен, как именно это сделать, но мне подвернулась эта тула и необходимость писать что-то свое отпала.
И еще одна проблема: не уверен, что получилось бы вытянуть корректный и полный стек в смешанной(управляемой и неуправляемой) среде.

Еще часто просто стека может оказаться мало, потому может оказаться полезным получить возможность отладки именно с момента создания утекающего объекта.

Был бы рад состряпай Вы статью на эту тему, потому как похоже эта одна из проблем о которых не принято говорить. А найти что-то дельное по ней так и вообще невозможно. С удовольствием прочту Ваш вариант.

Как-то ковырялся с чем-то таким по другому поводу (под линуксом). Там лажа была что всякие валгринды не понимали что есть утечка, потому что всё потом освобождалось. Тоже перехватывал api захвата-освобождения и разбирал многогигабайтные csv-трассы malloc/calloc/realloc/free, хоть и питоном. При этом вся память перед смертью приложения освобождалась, а просто при ничего не делании (скажем "перейти во вкладку, перейти обратно в исходное положение" (1)) неограниченно росла, поэтому всякие валгринды не осиливали.


То есть да, неосвобождённая память в любой момент до смерти программы разделяется на ту что "просто ещё не освободилась но нужна", и на ту что мы хотим найти, которая тоже ещё не освободилась, которая тоже потом освободится, но которая "не нужна". Поэтому можно проскипать любой кусок истории вначале, когда программа насоздавала объектов один раз чтобы с ними жить. И, разумеется, кусок истории "в конце", когда "ненужная" память будет тоже удалена и мы всё потеряем.


А потом я просто тыкал сценарий (1) и глазами смотрел на трассу утечек и находил повторяющиеся аллокации. Получить по ним бэктрейс — уже кто как умеет, мне повезло что там аллок с красивым числом байтов был и я кондишнл-брейк просто поставил. А так либо инструмент сразу даст все бэктрейсы, либо ставить больше брейков, на разные библиотеки, и тоже поиск логарифмически сложный выходит.


А поделитесь секретом — насколько локализован был баг на небольшой площади кода? В смысле ну статический анализ смог бы найти?

ДЛя таких случаев делаются слепки кучи в те моменты, когда все созданные объекты должны быть удалены (например, перед созданием и после закрытия вкладки). Затем разница между этими слепками и составляет утечку.
Да, спасибо, другими словами, если удастся любым способом разметить эти моменты в трассе, то задача становится практически нетворческой.

Наверное даже можно сделать инструмент, который будет её хорошо решать, чтобы ему надо было только кормить запущенное приложение и моменты.

P.S. Извиняюсь за самоповторы в комменте выше, старческий маразм/оверредактиринг.
Кстати, была идея написать подобную тулу, но быстро испугался объема работы.
Хотя с радостью увидел бы такую фичу в каком-нибудь dotMemory и ему подобных.
Я правильно понял, у Вас была утечка памяти? Почему пришлось идти именно таким путем, а не использовать профилировщик?

А поделитесь секретом — насколько локализован был баг на небольшой площади кода? В смысле ну статический анализ смог бы найти?

А вот здесь интересно вышло. Утечек было 3:
1. Самая интенсивная. Была донельзя банальна: не вызывался Dispose объекту, который не отписывал от событий дюжину других объектов, которые оставались висеть в памяти и держать объекты, которые держали GDI-wrapping-objects.
2. Наименее интенсивная. Была в нашем коде. И думаю могла пойматься бы статическим анализатором, но боюсь кроме дельных срабатываний анализатор выдал бы еще очень много ложных срабатываний, где мог бы просто потеряться.
3. Средней интенсивности. Оказалась в коде third-party библиотеки, доступа к коду которой не оказалось. И здесь было бы не на что натравить анализатор.
Работа непосредственно с winapi долгие годы побудила к выработке особого подхода:
* Пишешь CreateXXX, тут же пишешь DeleteXXX. Между ними код работы с объектом;
* Пишешь new, тут же пишешь delete/delete[]. Между ними код работы с памятью Перешел на векторы;
* Если объект «долгоиграющий» (неважно, HANDLE или HGDIOBJ), обязательно имей методы в классе вида AllocResource/FreeResource, последний из которых обязан быть в деструкторе, а в конструкторе не забудь написать hObject( NULL ). В AllocResource тоже всегда вызывай FreeResource, который внутри имеет код вида «if ( hObject != NULL ) FreeObject( hObject );». Причем оформление стоит делать сразу: написать оба метода и поместить последний в деструктор, потом уже делать основную работу.
* Где можно не выделять новый объект — не выделяй. Имеешь много пиктограмм для вывода в меню, списки, дерево, свой собственный элемент управления? Собери в один длинный битмап, и используй BitBlt/StretchBlt/TransparentBlt. Этот способ в одном проекте увеличил скорость работы приложения в тысячи раз, а использование GDI объектов уменьшил в разы. Если лень, используй родные виндовые ImageList.

Можно много еще много чего написать, но уже эти три подхода уменьшили головную боль при работе с winapi в разы.

Для отладки можно использовать метод вида #undef OrigFunc, #define OrigFunc MyAuditFunc (точно подходит для парных функций OrigFuncA/OrigFuncW, для одиночных тоже подходит, но надо поколдовать с хедерами). С new / delete([]) вообще все просто: переопределяем на свои, где записываем выделенные объекты в vector/map (по вкусу), удаляем оттуда при delete.

Ну это c++. В шарпе, думаю, возможен подобный подход, и думать над тем, чтобы лезть на более низкий уровень, не нужно.
Это хоршо для нового кода, у автора 9 млн строк чужого :)
Так я о том и пишу: переопределить основные функции на свои функции аудита, а затем уже анализировать их вывод. Конечно, всегда будут особые случаи, но это уже частности.
Вектор удобно, но для большого количества объектов может памяти кушать много. Особенно если и сама программа её жрёт как не в себя. Была похожая задача — так же дефайнами прикрыл все подозрительные функции и писал в лог вместе с файлом/строкой вызова. Логи были огромные, но короткий perl-скрипт их разгребал влёт оставляя на выходе всех подозрительных.
Пора идти за книгой по perl. Часто внутренний/внешний голос говорит, что задача хорошо решается перлом.
Да в шарпе есть очень четкий набор правил, как использовать unmanaged ресурсы.
1. Никогда не используйте unmanaged ресурсы.
2. Если никогда наступило, то оберните ресурс в класс, задачей которого будет следить за этим ресурсом. В классе должен присутствовать Dispose и финализатор/деструктор, оба освобождающие ресурс.
Обойтись одним деструктором, увы, не получится, так как в c# уж очень недерменирована его работа.

По этому принципу написаны все объекты из System.Drawing.

Про трюк с #define. В таком виде в шарпе его не провернуть. Но мне кажется он не будет корректно работать и на плюсах или си, поправьте, если ошибаюсь.
#define на плюсах это директива препроцессора, тоесть «выполняется» на этапе компиляции. А, что если вызов интересующего Вас метода происходит из подключенной dll библиотеки? Мне кажется трюк с #define не сможет поймать такой трюк, хотя мог где-то и ошибиться в размышлениях.
Есть трюк который точно сработает, он был моим последним вариантом, на случай, если вообще ничего не будет помогать:
Подменить в памяти вызовы нативной функции на свои. Когда-то читал, что так можно делать, но полного представления что-именно нужно для этого написать нету, потому рад что не пришлось спуститься на последний круг ада. Хотя если бы пришлось, то изменилось бы не так много: собрать лог, найти что не удаляется, остановить отладку в момент создания текущего объекта. Разве что лог пришлось бы собирать самому.

Вместо ApiMonitor можно попробовать запустить отладку в Mixed Mode и поставить брекпоинты (с трассировкой) на нативные функции. И тут уже могут работать условные брекпоинты.

Спасибо за комментарий. Сейчас проверил, breakpoint сработал отлично, но установить condition никак не получается.
В доке написано, что аргумент называется crColor.
А без аргументов становится очень тяжело трассировать что-то внятное… Возможно есть способ достать стек, но мне кажется аргументы более точно идетифицируют утекающий объект.
Чтобы сделать условный брекпоинт с использованием имени параметра нужно загрузить символы для библиотеки. Microsoft Symbol Servers содержит символы для gdi32.dll, но, к сожалению, не содержит имен параметров. Поэтому нужно опуститься на уровень ассемблера, так как все параметры, в конечном счёте, передаются либо через стек, либо через регистры.

Если запускать программу в 32-битном режиме, то первый параметр будет находиться по адресу [ebp+8], так как winapi использует stdcall. см. x86 Function-call Conventions
Соответсвенно значение по этому адресу можно получить так: (*(int*)(ebp+8))


Если программа 64-битная, то первые 4 целочисленные параметры функции находятся в регистрах RCX, RDX, R8, R9. см. Microsoft x64 calling convention.
Если мы хотим проверять только младшие 4 байта параметра, то вместо регистра rcx нужно использовать ecx: ecx == 0x12345678.
Ничего себе. На этот круг ада благо еще не приходилось спускаться. Спасибо за ссылки и интересный трюк.
Буду иметь ввиду в моменты глубокого отчаяния)
Sign up to leave a comment.

Articles