24 мая 2009 г.

Ищем утечки памяти

Прежде чем приступить к продолжению описания других способов ловли "плохих" указателей, я хотел бы поговорить об утечках памяти и о механизмах их диагностики. Как мы увидим в дальнейшем, эта тема будет очень близко связана с нашим предыдущим разговором.
Примечание: если вы плохо или совсем не понимаете, что такое указатели и/или объекты - рекомендую сначала прочитать эту статью.

Хотя любые ошибки в программе - это всегда плохо, но некоторые типы ошибок могут менее болезненно проявлять себя в определённом окружении. Например, ошибки утечки памяти или ресурсов довольно безобидны для клиентских машин (из-за сильно ограниченного времени работы) и довольно опасны на серверах.

Когда новичок загорается идеей поиска утечек памяти (или оптимизации использования памяти), он обычно открывает Диспетчер Задач и смотрит на своё приложение и колонку "Память" напротив него:

Диспетчер Задач с колонкой Память
И всё бы ничего, но потом он замечает, что колонка эта очень странно себя ведёт даже на простом приложении: память не уменьшается при закрытии форм, но почему-то пропадает при сворачивании приложения и т.п.

Хороший вопрос: а с чего новичок взял, что в этой колонке показывается именно память, выделенная его кодом? Если открыть меню "Вид"/"Выбрать столбцы", то можно увидеть ещё кучу показателей, которые тоже подходят под определение "память" - это и виртуальная память и различные виды пулов.
Так вот, открою вам секрет, что колонка "Память" в Диспетчере задач - это кол-во оперативной памяти, которую занимает ваша программа. Это вовсе не количество выделенной вами памяти (об этом можно было догадаться и самому, как только мы увидели, что у нашей программы пропадает "память" при сворачивании - ведь и ребёнку понятно, что без нашей команды память у нас отнять не могут). Во-первых, ваша программа может занимать меньше ОЗУ, чем вы выделили, т.к. часть данных может быть скинута в файл подкачки. Во-вторых, часть памяти не является эксклюзивно вашей: например, все системные библиотеки хотя и загружаются в каждый процесс, но тем не менее, присутствуют в памяти в единственном экземпляре (впрочем, это не относится к их данным). Это значение также называют "Working Set" или песочницей.

Вот снимок стандартного Диспетчера Задач и более продвинутого Process Explorer со всеми колонками "памяти":

Сравнение показателей Диспетчера Задач и Process Explorer-а
Не правда-ли, большой разброс по значениям?

Поздравляю, вы попались в ловушку очередного мифа о памяти (по ссылке - см. мифы №11 и 13).

Так что давайте выкинем на время Диспетчер Задач и вернёмся к нашему Паскалю. Как в Delphi осуществляется управление памятью? Ранее я уже говорил о наличии кода, который осуществляет централизованное управление памятью. Этот код называется менеджером памяти. Это значит, что искать утечки памяти не очень сложно, т.к. мы можем легко перехватить любые выделения и освобождения памяти, просто подставив свой "менеджер памяти", который будет просто заглушкой. Он не будет реально работать с памятью, перенаправляя запросы старому (настоящему) менеджеру памяти программы, а вместо этого он будет просто вести учёт памяти.

Что значит, что в программе есть утечка памяти? Это значит, что ваш код выделил память и забыл её освободить. Также это значит, что при выходе из программы у вас окажется блок неосвобождённой памяти. Иными словами, будет несоответствие числа вызовов на выделение и освобождение памяти.
Что нам нужно сделать для поиска утечки памяти? Нам нужно запоминать каждое выделение блока, фиксируя, в каком месте оно произошло, а в конце работы пройтись по оставшимся блокам памяти, которые никто не удосужился освободить. Каждый такой блок - это и есть утечка памяти.

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

Delphi

Окей, даже в Delphi "из коробки" есть базовая возможность диагностики, так что вы можете как минимум проверить наличие утечек памяти без использования сторонних утилит. Правда, если вы увидите утечку, то чтобы найти источник проблемы, вам всё же придётся воспользоваться чем-то ещё. Ну да ладно.

Во-первых, стоит упомянуть о том, что за всю историю Delphi в ней использовалось два менеджера памяти: один - довольно древний и примитивный (я не знаю, имел ли он какое-то название). В новые версии Delphi стали включать модификацию менеджера памяти FastMM - функционально богатого и исключительно быстрого менеджера памяти. Автономная версия FastMM и является одним из инструментов для поиска утечек памяти - его использование мы и расмотрим чуть ниже (кстати, FastMM вполне можно установить и в старые версии Delphi).

Окей, в старом менеджере памяти у нас в распоряжении есть глобальные переменные AllocMemCount и AllocMemSize (кол-во и суммарный размер выделенных блоков). Таким образом, чтобы сделать простейшую проверку на утечки памяти, вам нужно использовать такой простой модуль:
unit Unit2;

interface

implementation

uses
  Windows;

initialization

finalization

  if AllocMemCount <> 0 then
    MessageBox(0, 'An unexpected memory leak has occurred.', 'Unexpected Memory Leak', MB_OK or MB_ICONERROR or MB_TASKMODAL);

end.
Не забудьте, что подключать его нужно самым первым в dpr-файле (оба пункта важны принципиально). Если при выполнении программы у вас будет утечка памяти, то при закрытии программы вам будет показано сообщение.

В новых версиях Delphi эти переменные больше не работают. Зато их итоговая функциональность доступна переключением флага ReportMemoryLeaksOnShutdown. Всё, что вам нужно сделать в этом случае - установить глобальную переменную ReportMemoryLeaksOnShutdown в True при старте программы, например, так:
unit Unit1;

interface

... { ваша форма }

implementation

... { ваш код формы }

initialization
  ReportMemoryLeaksOnShutdown := True;

end.
Однако, все эти способы просто покажут вам, есть ли у вас утечка памяти или нет. Для того, чтобы диагностировать, кто конкретно тут виноват - вам придётся использовать один из инструментов, перечисленных ниже.

EurekaLog

У EurekaLog тоже есть функциональность по поиску утечек памяти. По-умолчанию она выключена, т.к. её включение не обходится за бесплатно - при её включении слегка замедляется выполнение программы и увеличивается потребляемая память, поскольку теперь надо вести учёт каждому выделению памяти. Кроме того, эта функциональность ограничена: она не доступна в C++ Builder и для приложений, собранных с пакетами, кроме того, она может поймать не более 1024 утечки.

В любом случае, для её включения нужно поставить флажок "Catch memory leaks" на вкладке "Advanced options":

Включение контроля утечек памяти в EurekaLog
Если вы включаете отлов утечек, то при выходе из программы вам сообщат о наличии утечек памяти, если таковые будут:

Диалог об утечке в стиле MS Classic
Стиль MS Classic

Диалог об утечке в стиле EurekaLog
Стиль EurekaLog

Подробный вид отчёта об утечках
Подробности отчёта

Как видите, все утечки памяти в программе будут представлены в одном отчёте, который ничем не отличается от обычных отчётов EurekaLog - пользователь точно так же может отправить его вам, как он делает это с обычными отчётами. Единственные два отличия это: отсутствие вкладок CPU и Assembler и пропуск вызовов обработчиков событий. Второе делается по той простой причине, что проверка на утечки делается последним действием в программе, когда никакой служебный код уже не работает. Вызов стандартных обработчиков в этот момент привело бы к плохим вещам.

Несколько под-опций на всё той же вкладке "Advanced options" контролируют поведение проверок утечек памяти.

Опция "Group son leaks with its father" помогает улучшить читабельность отчёта при наличии большого числа утечек. Она скрывает "дочерние" утечки памяти. Например, вы забыли удалить объект. Но в этом объекте есть ссылки на другие объекты. С выключенной опцией "Group son leaks with its father" у вас будет упомянут каждый объект, с включенной - только один: тот самый, корневой, который ссылается на другие объекты. Т.к. при удалении этого объекта пропадут и другие утечки. Вам следует выключать эту опцию, если вы хотите получать более детальный отчёт о проблемах.

Опция "Hide Borland leaks" пытается опознать и скрывать утечки памяти, вызванные недочётами в коде RTL/VCL Delphi. При выключенной опции вы можете получать отчёт об утечке, даже если в вашем коде её нет. Например, код может создать глобальный объект, но никогда не удалять его, в расчёте на то, что память будет освобождена при выходе из приложения. Хотя, строго говоря, это не является программистской ошибкой, это, тем не менее, ошибка проектирования, т.к. она затрудняет анализ приложения на настоящие утечки памяти.

Опция "Free All Memory" говорит сама за себя: надо ли освобождать память для этих самых утечек. В большинстве случаев нет никакой разницы, т.к. при выходе из программы вся память всё равно будет освобождена. В основном эта опция используется для решения проблем совместимости со сторонним кодом.

Об опции "Catch leaks exceptions" мы поговорим в другой раз - это и есть та побочная функциональность диагностики утечек памяти, которая может быть использована для поиска проблем с удалением объектов.

FastMM

FastMM также имеет неплохие возможности по диагностике утечек памяти. К сожалению, встроенная в Delphi версия не обладает полным функционалом, поэтому вам всё равно придётся скачать и установить полную версию FastMM.

Собственно, устанавливать ничего и не нужно: достаточно скачать и распаковать архив, после чего добавить модуль FastMM4.pas в свой проект (возможно, вам также придётся добавить папку с FastMM в Search Paths проекта). Добавлять модуль, разумеется, нужно самым первым. Если вы используете EurekaLog, то модуль ExceptionLog должен быть перечислен сразу же за модулем FastMM4. Сразу после подключения FastMM работает в штатном режиме: как отличный менеджер памяти. Различные функции диагностики включаются правкой файла опций. Для этого вы должны открыть файл FastMM4Options.inc, поменять там опции и сделать Build проекту. Для тех, кто не очень понимает в том, что такое директивы условной компиляции:
{.$define AssumeMultiThreaded} - опция выключена
{$define AssumeMultiThreaded} - опция включена
Или же вы можете использовать вот эту программку для визуального редактирования опций.

Итак, во-первых, нам понадобится включить опцию FullDebugMode, т.к. только с ней становятся доступными возможности диагностики утечек. Включение этой опции означает, что вашей программе теперь будет нужна библиотека FastMM_FullDebugMode.dll. Скомпилированный вариант можно найти в папке \FullDebugMode DLL\Precompiled\. Вы также можете скомпилировать свой вариант, если вам нужны другие опции для неё (я скажу об этом чуть ниже). В простейшем случае вам нужно просто скинуть эту библиотеку в папку с программой или в C:\Windows\System32, чтобы сделать доступной её для всех программ. Последнее удобно сделать на машине для разработки. Замечу, что функциональность по отлову утечек памяти доступна и без включения опции FullDebugMode, но в этом случае FastMM будет работать так же, как и в варианте с Delphi выше: сообщая только о наличии/отсутствии утечки, но ничего не сообщая о её местонахождении.

Из-за этого FastMM в режиме отладки слабо подходит для использования вне окружения для разработки: всё-таки вам придётся таскать за собой отдельную DLL, да плюс ещё замедление работы. Поэтому оптимальным вариантом будет тестирование программы с полным режимом отладки FastMM при разработке, а поставлять программу стоит с выключенной опцией поиска утечек памяти, либо же использовать функциональность EurekaLog. Она выглядит более скромно, чем аналогичная функциональность в FastMM, зато и более пригодна к использованию на машина конечных пользователей.

Итак, кроме включения режима отладки, нам нужно включить ещё функциональность поиска утечек памяти. Это делается опцией EnableMemoryLeakReporting. Заметьте, что рядом с FullDebugMode и EnableMemoryLeakReporting перечисленны ещё несколько опций, которые контролируют поведение этой функциональности (заметьте, что каждая опция снабжена комментарием в самом файле, так что я приведу только краткое описание):
  • RawStackTraces - включает RAW метод трассировки стека. По умолчанию используется стековый метод. Вы можете переключать эту опцию, если стек вызовов вызывает у вас затруднения при чтении. Подробнее об этом я говорю в статье про чтение лог-файлов.
  • LogErrorsToFile - записывать ошибки (не только утечки) в файл отчёта в папке с программой. Если вы выключаете эту опцию, то вам нужно ловить вывод от FastMM другими способами.
  • LogMemoryLeakDetailToFile - записывать утечки памяти в файл. Не работает, если выключена опция LogErrorsToFile.
  • ClearLogFileOnStartup - удалять файл-лог при запуске. Если опция выключена, то в один файл будут суммироваться отчёты от нескольких запусков. Может иногда ввести вас в заблуждение: вы можете подумать, что в программе есть утечка, когда файл-отчёт содержит старые записи от предыдущего запуска.
  • DisableLoggingOfMemoryDumps - отключает запись подробной информации об учетках (дампы памяти). Уменьшает размер отчёта.
  • HideExpectedLeaksRegisteredByPointer - скрывать утечки памяти, которые были зарегистрированы вызовом RegisterExpectedMemoryLeak. FastMM не добавляет автоматически утечки памяти в коде самой Delphi, так что если у вас такая ситуация, то вы можете при старте программы вызвать RegisterExpectedMemoryLeak, передав ей адрес объекта, который утекает (если у вас есть возможность его получить, конечно же).
  • RequireIDEPresenceForLeakReporting - включение этой опции заставляет программу включать диагностику утечек памяти только при запущенной IDE Delphi.
  • RequireDebuggerPresenceForLeakReporting - очень похожа на предыдущую опцию, только требует, чтобы программа была запущена именно из IDE Delphi (предыдущая опция требует наличия IDE, программа же может быть запущена и в свободном режиме).
  • RequireDebugInfoForLeakReporting - а эта опция включает диагностику только, если в программе есть отладочная информация (я об этом скажу ещё чуть ниже).
  • ManualLeakReportingControl - включение опции позволяет вам использовать глобальную переменную ReportMemoryLeaksOnShutdown для ручного включения/выключения контроля утечек памяти.
Кроме того, в этом же файле перечисленны и другие интересные для диагностики опции:
  • NoMessageBoxes - выключает показ сообщений об ошибках.
  • UseOutputDebugString - включает использование OutputDebugString для вывода сообщений об ошибках. Вывод от OutputDebugString ловится отладчиком (в Delphi это View/Debug Windows/Events) или программой DebugView при свободном прогоне.
Опции CheckHeapForCorruption, CatchUseOfFreedInterfaces и DetectMMOperationsAfterUninstall мы обсудим в следующий раз, т.к. они напрямую связаны с нашей предыдущей темой.

Прежде, чем вы сможете использовать FastMM в своей программе, вам осталось сделать ещё одну вещь: добавить в программу один из вариантов отладочной информации. Если в случае с EurekaLog отладочная информация генерировалась и добавлялась в программу автоматически, а в случае с Delphi она была просто не нужна (т.к. там были простые проверки, не было стеков вызовов), то для FastMM её нужно добавлять вручную. Отладочная информация используется библиотекой FastMM_FullDebugMode.dll при построении отчёта для получения информации об адресах памяти в вашей программе. Если отладочная информация будет недоступна, то в отчётах об ошибках вы увидите только безликие адреса, а никаких Button1Click там не будет.

Окей, по-умолчанию, FastMM_FullDebugMode.dll скомпилирована с поддержкой JCL, что означает, что DLL может читать отладочную информацию в формате map, TD32 и JDBG. Вы также можете перекомпилировать библиотеку для включения поддержки EurekaLog - в этом случае к уже перечисленным форматам добавится собственный формат EurekaLog. Чтобы сделать это, вам нужно включить опцию EurekaLog и выключить JCLDebug в файле FastMM_FullDebugMode.dpr, после чего перекомпилировать DLL. Тогда для правильной работы FastMM вам нужно просто включить EurekaLog для вашей программы.

Если же вы не хотите или не можете перекомпилировать DLL, то вы можете добавить в вашу программу отладочную информацию в виде map-файла, TD32 или JDBG. Первые два варианта поддерживаются Delphi "из коробки": первый включается установкой опции "Map file" в "Detailed" на вкладке "Linker", второй вариант - включением опции "Include TD32 debug info" ("Debug information" в D2009 и выше) на вкладке "Linker" в опциях проекта.

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

К сожалению, оба эти варианта плохо подходят для распространения приложения, т.к. они значительно увеличивают размер программы. Наиболее компактный вариант - это отладочная информация в формате EurekaLog или JCL. Если с первым я уже всё сказал, то второй вариант предполагает скачивание и установку библиотеки JCL (там есть автоматический установщик) и включение генерации/внедрения отладочной информации:

Включение внедрения отладочной информации формата JDBG
Скорее всего, вы захотите включить все три опции.

Вот как выглядит одна полная запись об утечке памяти в лог-файле:
--------------------------------2009/5/26 21:52:35--------------------------------
A memory block has been leaked. The size is: 12

This block was allocated by thread 0x4F8C, and the stack trace (return addresses) at the time was:
403206 [System][System.@GetMem]
4CC5B7 [Unit15.pas][Unit15][Unit15.B][35]
4CC5C4 [Unit15.pas][Unit15][Unit15.A][39]
4CC5DC [Unit15.pas][Unit15][Unit15.TForm15.FormCreate][43]
4C0CCB [Forms][Forms.TCustomForm.DoCreate]
4C0913 [Forms][Forms.TCustomForm.AfterConstruction]
4C08E8 [Forms][Forms.TCustomForm.Create]
4CA539 [Forms][Forms.TApplication.CreateForm]
4CD986 [Project14][Project14.Project14][14]
75B94911 [BaseThreadInitThunk]
770FE4B6 [Unknown function at RtlInitializeExceptionChain]

The block is currently used for an object of class: TTest

The allocation number is: 3814

Current memory dump of 256 bytes starting at pointer address 7FF75E50:
A0 C5 4C 00 E8 5E F7 7F 00 00 00 00 F4 20 51 2A 00 00 00 00 E0 4B F7 7F 00 00 00 00 00 00 00 00
FF FF FF FF 00 00 00 00 E7 0E 00 00 06 32 40 00 B7 C5 4C 00 C4 C5 4C 00 DC C5 4C 00 CB 0C 4C 00
13 09 4C 00 E8 08 4C 00 39 A5 4C 00 86 D9 4C 00 11 49 B9 75 B6 E4 0F 77 8C 4F 00 00 22 32 40 00
EF 70 42 00 26 6B 42 00 D9 6A 42 00 5A 9B 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 8C 4F 00 00 08 00 00 00 00 00 00 00 81 6F AF 70 0C 11 40 00 00 00 00 00
7E 90 50 8F 80 80 80 80 00 00 00 00 E0 4B F7 7F 00 00 00 00 00 00 00 00 FF FF FF FF 00 00 00 00
E8 0E 00 00 06 32 40 00 DC C5 4C 00 CB 0C 4C 00 13 09 4C 00 E8 08 4C 00 39 A5 4C 00 86 D9 4C 00
11 49 B9 75 B6 E4 0F 77 89 E4 0F 77 00 00 00 00 8C 4F 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  Е L . и ^ ч  . . . . ф Q * . . . . а K ч  . . . . . . . .
я я я я . . . . з . . . . 2 @ . · Е L . Д Е L . Ь Е L . Л . L .
. . L . и . L . 9 Ґ L . † Щ L . . I № u ¶ д . w Њ O . . " 2 @ .
п p B . & k B . Щ j B . Z › B . . . . . . . . . . . . . . . . .
. . . . . . . . Њ O . . . . . . . . . . Ѓ o Ї p . . @ . . . . .
~ ђ P Џ Ђ Ђ Ђ Ђ . . . . а K ч  . . . . . . . . я я я я . . . .
и . . . . 2 @ . Ь Е L . Л . L . . . L . и . L . 9 Ґ L . † Щ L .
. I № u ¶ д . w ‰ д . w . . . . Њ O . . . . . . . . . . . . . .

MemProof/AQTime

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

Я не буду рассматривать использование этих инструментов здесь, т.к. это отдельная большая тема. Интересующихся, отправляю к этой статье.

Замечу, что они позволяют справляться даже с такими проблемами, как неявные утечки памяти. Например, объекты могут не освобождаться вовремя, скапливаясь в каком-либо глобальном списке. Они реально не используются в программе и их число со временем растёт, но, тем не менее, они не считаются за утечку памяти, т.к. список вместе со всеми объектами корректно удаляется при выходе из программы.

Не только утечки...

Как я не раз упоминал тут - утилиты по диагностике учетек памяти зачастую имеют побочную функциональность, которая помогает отловить и другие типы "плохого использования" памяти, в частности, различные проблемы с объектами, которые могут приводить к возникновению Access Violation. Напомню, что Access Violation - это весьма хитрая штука, которая может и не возникать в вашем коде, даже если в нём есть баг. Простейший способ убедиться в этом:
var
  Str: TStringList;
begin
  Str := TStringList.Create;
  try
    Str.Add('Test 1');
  finally
    Str.Free;
  end;
  Str.Add('Test 2');      // ошибка! Обращение к уже удалённому объекту
  ShowMessage(Str.Text);
end;
Хотя этот код содержит глюк, в подавляющем большинстве случае он отлично отработает у вас без единого исключения и даже покажет какой-то результат.

Но об этом мы поговорим в следующий раз.

В любом случае, у вас всегда есть вариант просто переписать код ;)

См. также: как читать отчёты об ошибках.

Читать дальше: ищем утечки памяти, часть 2.

P.S. Ах, да, ещё одно: не важно, какой инструмент вы решили использовать, вы всегда можете улучшить его работу, выбором подходящих опций проекта ;)

57 комментариев :

  1. Вот тут кажеться ошибка:
    Хороший вопрос:а с чего новичек взял, что в этой колонке [b]показыватся[/b] именно память
    Наверное далжно быть [b]показывается[/b]

    ОтветитьУдалить
  2. Вот еще нашел :)
    FastMM так же имеет неплохие возможности по >диагнотике<

    ОтветитьУдалить
  3. Раз уш пошла такая пьянка - вот еще пара опечаток :)
    Сообщая только о наличии/отсутствии >учетки<

    Итак кроме >включение< режима

    Функциональности (>земетьте<, что

    Скапливаясь в >како-либо< глобальном

    В целом статья отличная :)

    ОтветитьУдалить
  4. За ссылку на блог спасибо - только не понял, как VMMap может помочь при диагностике утечек памяти.

    ОтветитьУдалить
  5. Я имел в виду не столько поиск утечек, сколько анализ того, на что память расходуется.

    ОтветитьУдалить
  6. ой статья то какая хорошая
    сколько ссылок полезных...
    5 баллов
    по мемчеку ещё пройтись можно было

    ОтветитьУдалить
  7. Кстати, для тех, кто предпочитает "увидеть и пощупать", вместо "скучного" чтения: здесь есть повторы с CodeRage 2. В частности - презентация "Fighting Memory Leaks for Dummies" от Francois Gaillard (ссылка на скачивание - примерно 25 Мб).

    Да, на английском, но рекомендую к просмотру ;)

    ОтветитьУдалить
  8. Есть хорошая утилита для поиска утечек памяти в C++ - Deleaker - deleaker.ru Мне лично очень нравится.

    ОтветитьУдалить
  9. Непонятное какое-то поведение у FastMM. Установил я его в D6, и опробовал на проекте из одной пустой формы. Так он мне заругался на утечки, которых вроде в принципе и быть не может, на всякие THelpManager, Unknown, которых у меня в объекте и нет совсем...

    ОтветитьУдалить
  10. чекбокс "Build with runtime packages".. вот где собака порылась %0
    Оказывается, сборка без рантаймовских пакаджей приводит к сообщениям об утечке памяти, причем, неважно, пусть в списке только штатные vcl, rtl пакеты - утечка уже диагностируется.. Что за ерунда..??

    ОтветитьУдалить
  11. мдя... как только убрал галку "Build with runtime packages", избавился в том числе и от сообщений о настоящих утечках памяти... Что за своеобразное поведение у FastMM...

    ОтветитьУдалить
  12. спасибо за статью. Руки дойдут надо прикрутить это всё.

    ОтветитьУдалить
  13. MemCheck.pas (http://v.mahon.free.fr/pro/freeware/memcheck/) очень симпатичный ловец утечек. Один модуль, представляет собой обёртку вокруг менеджера памяти. По завершении программы выдаёт исключения по каждой утечке, позиционируя отладчик на месте выделения памяти. Заодно пишет лог.

    ОтветитьУдалить
  14. Гм, второе упоминание о MemCheck :)

    Говорят, вещь хорошая, но я сам не смотрел, потому что проект уже не развивается - не обновляется с 2007-го года. Т.е. никаких тебе новых Delphi без допиливания напильником.

    Не стесняйтесь сделать обзор сами, если хотите, и прилинковать сюда.

    ОтветитьУдалить
  15. Благодарствую за статью, мне помогла :)

    ОтветитьУдалить
  16. А вот и неправда ваша по поводу MemCheck. Я тоже долгое время думал, что проект загнулся и больше не развивается. Однако недавно наткнулся вот на это. Так-что проект продолжают развивать и адаптирован он уже вплоть до Delphi2010.

    ОтветитьУдалить
  17. Парни,я не программист и не соображаю ничего тут как вы,но у меня вопрос,после закрытия одного приложения у меня выдаёт ошибку о каком то FastMM,на работу это вроде никак не влияет,но хочу спросить что это и как исправить?

    ОтветитьУдалить
  18. отличная статья, спасибо!
    Только я воспользовался MemCheck-ом :)

    ОтветитьУдалить
  19. Пробовал в Builder C++ 2009 использовать данну переменную (ReportMemoryLeaksOnShutdown), незаработала:( Отключил RTL и Runtime packages, тот же эффект. Может что в настройках еще надо изменить?
    Интересно еще, насколько сильно менеджер памяти билдера отличаеться от дельфи?
    Вообще свелся к мысли что использование CodeGuard + memprof достаточно, чтобы отловить утечку ресурсов.

    ОтветитьУдалить
    Ответы
    1. RTFM: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.ReportMemoryLeaksOnShutdown

      Warning: ReportMemoryLeaksOnShutdown only works on Delphi applications, it has no effect in C++ applications and in packages.

      Удалить
  20. Кстати madCollection теперь обзавелась шикарным инструментом по контролю за утечкой ресурсов (в том числе и памяти).

    ОтветитьУдалить
  21. Не совсем понятно, что указано в квадратных скобках.
    Например, Line 35[1], т.е. что означает 1?

    ОтветитьУдалить
  22. Здравствуйте!
    Собранная для использования с EurekaLog dll-ка FastMM_FullDebugMode.dll выдаёт сообщение: You must activate EurekaLog in the "FastMM_FullDebugMode.dll" module. В коде в ExceptionLog инициализация отменяется вот в этом месте процедуры Init:
    ...
    if not IntoIDE then
    begin
    {$IFNDEF CBuilder}
    Hook_InitializationAndFinalization;
    {$ENDIF}
    if (not IsEurekaLogModule(HInstance)) then
    begin
    Initialized := False;
    Exit;
    end;
    ...
    Может подскажете как решить эту проблему? Ах, да - Delphi 7, EurekaLog 6.0.15.
    Спасибо.

    ОтветитьУдалить
  23. А как собирали-то? Сообщение говорит о том, что код EL в DLL есть (читай: добавлены модули), но вот DLL с EL скомпилирована не была (читай: нет данных EL - опций, отладочной информации).

    Если в IDE стоит эксперт EL, то в свойствах проекта EL нужно её включить. Если эксперта не стоит, то нужно руками включить генерацию .map файла, создать .eof файл с настройками EL и пост-процесснуть DLL после компиляции вызовом ecc32.

    ОтветитьУдалить
  24. В IDE всё включаю (EL активна) и для DLL-ки и для тестовой проги, но в DLL-ку в ресурсы почему-то не записывает ресурс ELDATA из которого считывается MagicNumber по которому и определяется что собрано с EL. В экзешник в ресурсы вписывает. Короче похоже это какой-то старый баг EL

    ОтветитьУдалить
  25. Похоже дело в этом:
    TUnhandledModules: set of TModuleType =
    {$IFDEF PROFESSIONAL}
    [mtUnknown];
    {$ELSE}
    [mtUnknown, mtPackage, mtLibrary];
    {$ENDIF}

    В списке необслуживаемых - mtLibrary - поэтому map-файл dll-ки не обрабатывается EL.

    ОтветитьУдалить
  26. Что-то я совсем ничего не понял. Вы ломаный Trial, что-ли, используете?

    Во-первых, зачем вы пересобираете ecc32 (или IDE-эксперт)?
    Во-вторых, директива PROFESSIONAL включена в любых версиях EL, кроме Trial.

    ОтветитьУдалить
  27. [Fatal Error] Alpha1.dpr(8): File not found: 'FastMM4Messages.dcu'
    Только начинаю изучать Делфи.В чем может быть проблема?Сделал все как написано.Не могу разобраться что к чему

    ОтветитьУдалить
    Ответы
    1. А вы где FastMM-то брали? Просто если брать где надо, то все необходимые файлы там прямо в корне лежат.

      Удалить
  28. Александр, подскажите, может, я что-то не так делаю, но после локализации приложения встроенными средствами Delphi (Seattle) по его закрытии выводится сообщение "FastMM has detected a FreeMem call after FastMM was uninstalled" и после нажатия OK еще одно "Runtime error 204". Столкнулся с этим на своем проекте и повторил ситуацию на простеньком тестовом с одной формой и одним TLabel. Причем если удалить запись в HKEY_CURRENT_USER\Software\Embarcadero\Locales, относящуюся к моему приложению, то сообщение не появляется.
    Не понятно совсем, почему это происходит (догадываюсь, что связано с попыткой повторной выгрузки ресурсов) и существует ли какой workaround для этого?

    ОтветитьУдалить
    Ответы
    1. Известные грабли. Не сказать, что совсем баг, скорее "особенности дизайна". Решается использованием опции типа NeverUninstall, но при этом будут регистрироваться ложные утечки памяти.

      Удалить
  29. Александр, подскажите как подключить jcl debug expert. Вроде устанавливаю, а в меню не вижу

    ОтветитьУдалить
    Ответы
    1. Как-то неправильно ставите, значит.

      Само собой, JCL - библиотека кода и поэтому не нуждается в установке, можно просто указать её папку в путях поиска. Но чтобы в IDE появились примочки - тут установка нужна. Проще всего это делается запуском Install.bat в корне - он скомпилирует установщик (\bin\JediInstaller.exe) и запустит его. Сделать это .bat-ник сможет только при условии, что у вас рабочий dcc32.exe. Например, в редакции Starter он отключен, поэтому JCL автоматически установить не удастся - только вручную.

      В установщике JediInstaller будет набор вкладок - по одной на каждую установленную у вас версию Delphi. Если вкладки нет, то либо установщик вашу IDE не поддерживает (скачайте последнюю версию JCL), либо пора делать Repair самой IDE (отсутствует регистрация IDE в реестре).

      На вкладке указываются пути, куда складывать BPL и DCP, а также опции установки. Убедитесь, что вы ставите галку JEDI Code Library / Packages / IDE Experts / Debug Extension - эта опция скомпилирует JclDebugExpert. (Лично я включаю все галки, кроме Demos.) Жмёте Install - и установщик скомпилирует и установит пакеты. Следите за логом, не будет ли там ошибок.

      Готовые пакеты (BPL/DCP) попадут в папки, указанные на вкладке. Путь может выглядеть как-то так: C:\Users\Public\Documents\RAD Studio\8.0\BPL или так: C:\jcl\jcl\lib\d17\win32\

      Пакеты можно скомпилировать и вручную - откройте подходящий \packages\JclPackagesDxxx.groupproj и сделайте Build All. Затем на каждом пакете правой щёлкаете и - Install. А если у вас уже есть скомпилированные BPL, то в самой среде - Components / Install Packages и Add - выбираете BPL. Ну и убедитесь, что галка в списке пакетов напротив JCL экспертов не сброшена.

      Да, и всегда скачивайте свежую/последнюю версию JCL.

      Удалить
    2. Спасибо за развернутый и оперативный ответ!В общем вчера качал видимо что-то не то! Скачал отсюда http://wiki.delphi-jedi.org/wiki/JEDI_Code_Library и произвел установку(manual) как описано здесь: http://wiki.delphi-jedi.org/wiki/JCL_Installation#Manual_Installation. И вуаля!

      Удалить
  30. Здравствуйте, Александр!
    Подскажите пожалуйста почему в лог файле события дублируются? Я вызываю событие единожды, но в лог файле записей больше сотни!
    Line 819372: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819410: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819448: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819486: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819524: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819562: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819600: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819635: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819676: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 819714: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]
    Line 838447: CF1C8F [frxClass.pas][frxClass][frxClass.TfrxReport.Create][11611]

    ОтветитьУдалить
    Ответы
    1. Я не телепат. Я понятия не имею, какой инструмент вы используете для поиска утечек, какое событие вы вызываете (и зачем).

      Могу предположить, что у вас куча однотипных утечек. Т.е. утечка в одном месте, и это место вызывается множество раз. В этом случае можно расследовать и починить только один (первый) отчёт, все остальные пропадут автоматически.

      Удалить
  31. Подскажите можно ли в исходном коде (через дерективы) включить/отключить генерацию TD32 или Detail map? Это команды для linker, и в документации такой штуки не нашел, но возможно есть какие-то способы?
    Возможно такое можно сделать с JDBG?

    ОтветитьУдалить
    Ответы
    1. Поясню зачем, есть код, который будет компилироваться под разные версии Delphi, хотелось бы управлять через *.inc файл.
      Например: для Debug принудительно включать генерацию отладочной информации, а для Release отключать.

      Удалить
    2. Конечно же нельзя, ведь это не опции для компилятора (compiler), это опции для компоновщика (linker). Компоновщик с исходным кодом не работает. С исходным кодом работает компилятор.

      Не очень понятно как вы делаете компиляцию. Обычно делают так: настраивают два профиля (Debug и Release). Дают им разные опции. И включают в профиле Debug символ условной компиляции DEBUG. Ну а в исходном коде уже проверяют символ: {$IFDEF DEBUG}. Т.е. идут от профиля к коду, а не наоборот.

      В старых Delphi профилей нет. Вместо этого можно использовать два проекта.

      Наконец, можно генерировать map/tds всегда. И из релиза их просто удалять. Они не влияют на машинный код, это только сопутствующая информация. (для TD32 в опциях проекта можно указать, что инфу внедрять в .exe не надо, а надо создавать отдельный .tds файл; в старых Delphi такой опции нет, вместо неё можно использовать утилиты вроде StripTDS).

      Удалить
    3. Все это я понимаю. Использую старую версию. Уточнил что директив для linker нет в доке, но возможно есть "хаки".
      А команды компоновщику через директивы в исходном коде в с++ например через #pragma comment(linker,.. вполне передают, может думаю что-то для Delphi есть, чего не знаю.
      debug/release задаю руками в inc файле:
      {$IFNDEF RELEASE}{$IFNDEF DEBUG}
      {$DEFINE BUILD_TYPE_IDE_NOT_SUPPORT}
      {.$DEFINE RELEASE}
      {$IFNDEF RELEASE}{$DEFINE DEBUG}{$ENDIF}
      {$ENDIF}{$ENDIF}
      Потом опции.
      За StripTDS спасибо. Вспомнил еще про DDevExtensions.

      Удалить
    4. А еще по поводу "компоновщик/компилятор". Забавная деталь: опция «Использовать Debug DCU» в настройках проекта почему то находиться на вкладке компилятора.

      Удалить
    5. Технически, это опция компоновщика, конечно. Ибо это замамкированная search paths (посмотрите как они меняются при переключении этой опции).

      Но логически это аналог $D+/-. Видимо, поэтому её рядом и поместили.

      Иными словами, это всё равно опция компоновщика и передаётся она компоновщику (search paths).

      Удалить
    6. > debug/release задаю руками в inc файле

      Ну и зачем вы это делаете?

      Удалить
    7. Уже не делаю, в старой версии Delphi установил DDevExtensions.

      Удалить
  32. Александр, а как бы вы посоветовали делать? Посоветуйте ваш способ организации разных build профилей на старых версиях Delphi.

    ОтветитьУдалить
    Ответы
    1. Лично я использую три варианта:
      1). Не использовать старые версии Delphi (серьёзно, 15 лет назад всё же).
      2). Использую два проекта с одинаковым содержимым, но разными настройками (например, Project_Release.dpr и Project_Debug.dpr). При этом, само собой, DPR не содержит кода, а лишь вызывает основную процедуру из главного модуля (это для ручного управления).
      3). Использую один проект и конфигурирую его в DEBUG. Проект для RELEASE собирается как часть сборки релизного билда чем-нибудь вроде FinalBuilder (в самом деле, зачем RELEASE компилировать из IDE?).

      Удалить
    2. Что-то мне подсказывает, что на полную вы используете только первый вариант :)
      1) Не всегда есть возможность купить новую версию.
      2) Проходил это, сильно не удобно. Несколько примеров: Есть идея, нужен прототип, запускаем Делфи, (оп, нужно же второй проект создать, иногда лень), через несколько часов разработки оказывается что "релиз" проект сильно устарел и запустить его быстро не получиться, нужно например прописать пути (второй оп), а бывает так, переключился на проект "релиз": а где этот файл? а мы его не добавили в "релиз" проект (3-ий оп). Может еще что забыл, помню что отказался от такого подхода. Не смертельно, но неудобно.
      3) Не понятно тогда, зачем все IDE дают возможность создавать build профили. Было бы Debug и команда Deploy сразу на стол заказчику. Ну а если серьезно то этот сценарий хорош например для крупного проекта долгожителя, где один раз настроил конфигурацию сборки и разрабатываешь. Причем предметная область задачи явно не завязана на различие генерируемого debug и release кода. Но сценарии бывают разные. Тот же второй пункт сюда (быстрое прототипирование). А если задача написать например stack tracer. Все же профили сборки в IDE это даже не плюс, а стандарт.

      Для старых версий, на мой взгляд, нет ничего удобней DDevExtensions, который дает возможность создавать даже более двух профилей сборки.

      По поводу куска кода с defines, который я указал выше. Мне удобно при разработке библиотек под разные версии компилятора или с дополнительными опциями которые задаются через defines. И так как там уже есть свои defines, без них никак, то сюда же удобно перенести переключение Debug/Release. Все в одном месте, удобно. Ну это для старых версий среды, в коде видно что работает он если не заданы ни RELEASE ни DEBUG. Опять же DDevExtensions проблему решает, если он установлен.

      Удалить
  33. Блог еще живой?
    Я не знаю, баг это FastMM или он так задуман - он не дает работать с SendMessage и Thread.
    Суть.
    Unit1: Form с двумя Button и двумя Edit.
    const
    MESS_1 = WM_USER+1;
    MESS_2 = WM_USER+2;
    ....
    procedure GetText(var Msg: TMessage); message MESS_1;
    procedure SetText(var Msg: TMessage); message MESS_2;
    ....
    procedure TForm1.GetText(var Msg: TMessage);
    begin
    Msg.Result:=integer(PChar(TAccessControl(Msg.WParam).Text));
    end;
    procedure TForm1.SetText(var Msg: TMessage);
    begin
    TAccessControl(Msg.WParam).Text := PChar(Msg.LParam);
    end;
    //--------------------
    Unit2: Thread, который через SendMessage отправляет и получает данные формы.

    function TNew.GetText: string;
    var
    i: integer;
    begin
    i:=SendMessage(Form1.Handle, MESS_1, WPARAM(Form1.Edit1), 0);
    result := PChar(i);
    end;
    procedure TNew.SetText(s: string);
    begin
    SendMessage(Form1.Handle, MESS_2, WPARAM(Form1.Edit2), LPARAM(PChar(source)));
    end;

    Без FastMM всё работает как надо: берет текст из одного Edit и ставит в другой.
    Подключаю FastMM, берем текст. А там какой-то мусор в итоге. Значение PChar не меняется в результате передачи, а вот по адресу какая-то шляпа.
    Я сперва подумал, что возможно с Юникодом что-то, перевел строку в набор hex. Даже близко не похожы.
    В чем может быть дело? Я уже весь гугл облазил.

    ОтветитьУдалить
    Ответы
    1. Сам задал, сам отвечу. Т.к. SendMessage возвращает Integer, а не string, то я помещал в integer адрес начала строки. Но! "В отладочном режиме (т.е. если включена опция условной компиляции FullDebugMode) FastMM при удалении объекта "портит" память, занимаемую ранее объектом, таким образом, что при следующей попытке вызывать виртуальный метод удаленного объекта FastMM возбуждает исключение." Т.е. строка была ЛОКАЛЬНАЯ для ф-ии, при выходе из ф-ии строка как бы всё. FastMM ее "портит" специально, поэтому всякая туфта была на выходе.
      Выход: задать строковую переменную глобальную в форме и через нее получать значение едита.

      Удалить
    2. Edit не хранит сам строку текста. Соответственно, при вызове свойства .Text он вынужден создавать новую строку. Которая и удаляется из памяти при выходе из подпрограммы. Если бы он хранил текст самостоятельно (в поле объекта) и возвращал бы значение поля без изменений, то код бы работал - благодаря ссылкам для строк.

      Удалить
    3. Если честно, я до этого вообще из потока брал просто Form1.Edit1.Text :) Т.к. считал, что обращаюсь к свойству VCL объекта, не изменяю его... просто читаю. А вот изменял через SendMessage и Synchronize. И так делал, пока не связался с Frame. Вот что было: если создать динамически фрейм, где лежит VCL объект, но фрейм я еще не увидел - он в другой вкладке PageControl, то при попытке прочитать св-во объекта из потока, вся форма висла намертво. Но удивительно, что если этот фрейм посмотреть (простой перейти в другую вкладку PageControl) и обратиться напрямую к св-ву объекта из потока, то все работает :)

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.