Comments 21
Плакать из-за отсутствия conditional breakpoints.
До 2015 — right click on breakpoint, после 2015 — hover on breakpoint and click gear icon. Не?
1. Хукаем API функции создания объектов, логируем стек и хендл.
2. Хукаем удаление объектов и собираем утекшие хендлы.
3. Смотрим стек первого утекшего хендла, правим код и повторяем сначала.
Реально все просто, непонятно зачем было огород городить.
И еще одна проблема: не уверен, что получилось бы вытянуть корректный и полный стек в смешанной(управляемой и неуправляемой) среде.
Еще часто просто стека может оказаться мало, потому может оказаться полезным получить возможность отладки именно с момента создания утекающего объекта.
Был бы рад состряпай Вы статью на эту тему, потому как похоже эта одна из проблем о которых не принято говорить. А найти что-то дельное по ней так и вообще невозможно. С удовольствием прочту Ваш вариант.
Как-то ковырялся с чем-то таким по другому поводу (под линуксом). Там лажа была что всякие валгринды не понимали что есть утечка, потому что всё потом освобождалось. Тоже перехватывал api захвата-освобождения и разбирал многогигабайтные csv-трассы malloc/calloc/realloc/free, хоть и питоном. При этом вся память перед смертью приложения освобождалась, а просто при ничего не делании (скажем "перейти во вкладку, перейти обратно в исходное положение" (1)) неограниченно росла, поэтому всякие валгринды не осиливали.
То есть да, неосвобождённая память в любой момент до смерти программы разделяется на ту что "просто ещё не освободилась но нужна", и на ту что мы хотим найти, которая тоже ещё не освободилась, которая тоже потом освободится, но которая "не нужна". Поэтому можно проскипать любой кусок истории вначале, когда программа насоздавала объектов один раз чтобы с ними жить. И, разумеется, кусок истории "в конце", когда "ненужная" память будет тоже удалена и мы всё потеряем.
А потом я просто тыкал сценарий (1) и глазами смотрел на трассу утечек и находил повторяющиеся аллокации. Получить по ним бэктрейс — уже кто как умеет, мне повезло что там аллок с красивым числом байтов был и я кондишнл-брейк просто поставил. А так либо инструмент сразу даст все бэктрейсы, либо ставить больше брейков, на разные библиотеки, и тоже поиск логарифмически сложный выходит.
А поделитесь секретом — насколько локализован был баг на небольшой площади кода? В смысле ну статический анализ смог бы найти?
Наверное даже можно сделать инструмент, который будет её хорошо решать, чтобы ему надо было только кормить запущенное приложение и моменты.
P.S. Извиняюсь за самоповторы в комменте выше, старческий маразм/оверредактиринг.
А поделитесь секретом — насколько локализован был баг на небольшой площади кода? В смысле ну статический анализ смог бы найти?
А вот здесь интересно вышло. Утечек было 3:
1. Самая интенсивная. Была донельзя банальна: не вызывался Dispose объекту, который не отписывал от событий дюжину других объектов, которые оставались висеть в памяти и держать объекты, которые держали GDI-wrapping-objects.
2. Наименее интенсивная. Была в нашем коде. И думаю могла пойматься бы статическим анализатором, но боюсь кроме дельных срабатываний анализатор выдал бы еще очень много ложных срабатываний, где мог бы просто потеряться.
3. Средней интенсивности. Оказалась в коде third-party библиотеки, доступа к коду которой не оказалось. И здесь было бы не на что натравить анализатор.
* Пишешь CreateXXX, тут же пишешь DeleteXXX. Между ними код работы с объектом;
*
* Если объект «долгоиграющий» (неважно, 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++. В шарпе, думаю, возможен подобный подход, и думать над тем, чтобы лезть на более низкий уровень, не нужно.
1. Никогда не используйте unmanaged ресурсы.
2. Если никогда наступило, то оберните ресурс в класс, задачей которого будет следить за этим ресурсом. В классе должен присутствовать Dispose и финализатор/деструктор, оба освобождающие ресурс.
Обойтись одним деструктором, увы, не получится, так как в c# уж очень недерменирована его работа.
По этому принципу написаны все объекты из System.Drawing.
Про трюк с #define. В таком виде в шарпе его не провернуть. Но мне кажется он не будет корректно работать и на плюсах или си, поправьте, если ошибаюсь.
#define на плюсах это директива препроцессора, тоесть «выполняется» на этапе компиляции. А, что если вызов интересующего Вас метода происходит из подключенной dll библиотеки? Мне кажется трюк с #define не сможет поймать такой трюк, хотя мог где-то и ошибиться в размышлениях.
Есть трюк который точно сработает, он был моим последним вариантом, на случай, если вообще ничего не будет помогать:
Подменить в памяти вызовы нативной функции на свои. Когда-то читал, что так можно делать, но полного представления что-именно нужно для этого написать нету, потому рад что не пришлось спуститься на последний круг ада. Хотя если бы пришлось, то изменилось бы не так много: собрать лог, найти что не удаляется, остановить отладку в момент создания текущего объекта. Разве что лог пришлось бы собирать самому.
Вместо ApiMonitor можно попробовать запустить отладку в Mixed Mode и поставить брекпоинты (с трассировкой) на нативные функции. И тут уже могут работать условные брекпоинты.
В доке написано, что аргумент называется crColor.
А без аргументов становится очень тяжело трассировать что-то внятное… Возможно есть способ достать стек, но мне кажется аргументы более точно идетифицируют утекающий объект.
Если запускать программу в 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.
Поиск утечки GDI объектов: Как загнать мастодонта