Какой правильный инструмент для обнаружения VMT или повреждения кучи в Delphi?

Я являюсь членом команды, которая использует Delphi 2007 для более крупного приложения, и мы подозреваем, что кучи повреждены, потому что иногда встречаются странные ошибки, которые не имеют другого объяснения. Я считаю, что опция Rangechecking для компилятора предназначена только для массивов. Я хочу инструмент, который дает исключение или журнал, когда есть запись в адрес памяти, который не выделяется приложением.

С уважением

РЕДАКТИРОВАТЬ: ошибка имеет тип:

Ошибка: нарушение прав доступа по адресу 00404E78 в модуле "BoatLogisticsAMCAttracsServer.exe". Чтение адреса FFFFFFDD

EDIT2: спасибо за все предложения. К сожалению, я думаю, что решение глубже, чем это. Мы используем исправленную версию Bold для Delphi, поскольку у нас есть источник. Вероятно, есть некоторые ошибки, внесенные в рамку Bold. Да, у нас есть журнал со стеками вызовов, которые обрабатываются JCL, а также трассируют сообщения. Таким образом, стек вызовов с исключением может блокироваться следующим образом:

20091210 16:02:29 (2356) [EXCEPTION] Raised EBold: Failed to derive ServerSession.mayDropSession: Boolean
OCL expression: not active and not idle and timeout and (ApplicationKernel.allinstances->first.CurrentSession <> self)
Error: Access violation at address 00404E78 in module 'BoatLogisticsAMCAttracsServer.exe'. Read of address FFFFFFDD. At Location BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)

Inner Exception Raised EBold: Failed to derive ServerSession.mayDropSession: Boolean
OCL expression: not active and not idle and timeout and (ApplicationKernel.allinstances->first.CurrentSession <> self)
Error: Access violation at address 00404E78 in module 'BoatLogisticsAMCAttracsServer.exe'. Read of address FFFFFFDD. At Location BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)
Inner Exception Call Stack:
 [00] System.TObject.InheritsFrom (sys\system.pas:9237)

Call Stack:
 [00] BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas:4016)
 [01] BoldSystem.TBoldMember.DeriveMember (BoldSystem.pas:3846)
 [02] BoldSystem.TBoldMemberDeriver.DoDeriveAndSubscribe (BoldSystem.pas:7491)
 [03] BoldDeriver.TBoldAbstractDeriver.DeriveAndSubscribe (BoldDeriver.pas:180)
 [04] BoldDeriver.TBoldAbstractDeriver.SetDeriverState (BoldDeriver.pas:262)
 [05] BoldDeriver.TBoldAbstractDeriver.Derive (BoldDeriver.pas:117)
 [06] BoldDeriver.TBoldAbstractDeriver.EnsureCurrent (BoldDeriver.pas:196)
 [07] BoldSystem.TBoldMember.EnsureContentsCurrent (BoldSystem.pas:4245)
 [08] BoldSystem.TBoldAttribute.EnsureNotNull (BoldSystem.pas:4813)
 [09] BoldAttributes.TBABoolean.GetAsBoolean (BoldAttributes.pas:3069)
 [10] BusinessClasses.TLogonSession._GetMayDropSession (code\BusinessClasses.pas:31854)
 [11] DMAttracsTimers.TAttracsTimerDataModule.RemoveDanglingLogonSessions (code\DMAttracsTimers.pas:237)
 [12] DMAttracsTimers.TAttracsTimerDataModule.UpdateServerTimeOnTimerTrig (code\DMAttracsTimers.pas:482)
 [13] DMAttracsTimers.TAttracsTimerDataModule.TimerKernelWork (code\DMAttracsTimers.pas:551)
 [14] DMAttracsTimers.TAttracsTimerDataModule.AttracsTimerTimer (code\DMAttracsTimers.pas:600)
 [15] ExtCtrls.TTimer.Timer (ExtCtrls.pas:2281)
 [16] Classes.StdWndProc (common\Classes.pas:11583)

Внутренняя часть исключения - это стек вызовов в момент, когда исключение вызывается.

РЕДАКТИРОВАТЬ 3: Теория прямо сейчас такова, что таблица виртуальной памяти (VMT) как-то сломан. Когда это происходит, нет никаких признаков этого. Только когда вызывается метод, возникает исключение (ВСЕГДА по адресу FFFFFFDD, -35 десятичное число), но тогда уже слишком поздно. Вы не знаете истинную причину ошибки. Любой намек на то, как поймать такую ​​ошибку, действительно ценится!!! Мы пробовали использовать SafeMM, но проблема в том, что потребление памяти слишком велико, даже если используется флаг 3 ГБ. Так что теперь я пытаюсь дать награду SO сообществу:)

РЕДАКТИРОВАТЬ 4: Один намек заключается в том, что согласно журналу часто (или даже всегда) другое исключение до этого. Это может быть, например, оптимистическая блокировка в базе данных. Мы попытались вызвать исключения силой, но в тестовой среде это работает нормально.

РЕДАКТИРОВАТЬ5: История продолжается... Я провел поиск в журналах за последние 30 дней. Результат:

  • "Чтение адреса FFFFFFDB" 0
  • "Чтение адреса FFFFFFDC" 24
  • "Чтение адреса FFFFFFDD" 270
  • "Чтение адреса FFFFFFDE" 22
  • "Чтение адреса FFFFFFDF" 7
  • "Чтение адреса FFFFFFE0" 20
  • "Чтение адреса FFFFFFE1" 0

Таким образом, текущая теория заключается в том, что перечисление (в Bold есть много) перезаписывает указатель. Я получил 5 хитов с другим адресом выше. Это может означать, что перечисление содержит 5 значений, где второе используется наиболее часто. Если есть исключение, для базы данных должен произойти откат, и объекты Boldobjects должны быть уничтожены. Возможно, есть вероятность, что не все будет уничтожено, и перечисление все еще может записать адрес в адрес. Если это так, может быть, можно найти код с помощью регулярного выражения для перечисления с 5 значениями?

РЕДАКТИРОВАТЬ6: Подводя итог, нет, пока нет решения проблемы. Я понимаю, что могу немного ввести вас в заблуждение с помощью стека вызовов. Да, в этом есть таймер, но есть и другие стеки вызовов без таймера. Простите за это. Но есть 2 общих фактора.

  • Исключение с чтением адреса FFFFFFxx.
  • Вершина стека вызовов - System.TObject.InheritsFrom (sys\system.pas:9237)

Это убедило меня, что Ville Krumlinde лучше всего описывает проблему. Я также убежден, что проблема где-то в рамках Bold. Но БОЛЬШОЙ вопрос в том, как можно решить подобные проблемы? Недостаточно иметь Assert, как предлагает Ville Krumlinde, так как повреждение уже произошло, и в этот момент колл-стэк исчез. Итак, чтобы описать мой взгляд на то, что может вызвать ошибку:

  1. Где-то указателю присваивается неверное значение 1, но это могут быть также 0, 2, 3 и т. Д.
  2. Объект назначен этому указателю.
  3. В базовом классе объектов есть вызов метода. Это вызывает метод TObject.InheritsForm, который вызывается, и по адресу FFFFFFDD появляется исключение.

Эти 3 события могут быть вместе в коде, но они также могут быть использованы гораздо позже. Я думаю, что это верно для последнего вызова метода.

EDIT7: Мы тесно сотрудничаем с автором Bold Яном Норденом, и он недавно обнаружил ошибку в OCL-оценщике в среде Bold. Когда это было исправлено, такого рода исключения значительно уменьшились, но они все еще иногда случаются. Но это большое облегчение, что это почти решено.

10 ответов

Решение

У меня нет решения, но есть некоторые подсказки об этом конкретном сообщении об ошибке.

System.TObject.InheritsFrom вычитает константу vmtParent из Self-указателя (класса), чтобы получить указатель на адрес родительского класса.

В Delphi 2007 vmtParent определен:

vmtParent = -36;

Таким образом, ошибка $FFFFFFDD (-35) звучит так, как будто указатель класса равен 1 в этом случае.

Вот контрольный пример для его воспроизведения:

procedure TForm1.FormCreate(Sender: TObject);
var
  I : integer;
  O : tobject;
begin
  I := 1;
  O := @I;
  O.InheritsFrom(TObject);
end;

Я попробовал это в Delphi 2010 и получил "Чтение адреса FFFFFFD1", потому что vmtParent отличается в разных версиях Delphi.

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

Вы можете попробовать это на ваших объектах, которые используются в коде DMAttracsTimers (который, я полагаю, является кодом вашего приложения):

Assert(Integer(Obj.ClassType)<>1,'Corrupt vmt');

Вы пишете, что хотите, чтобы там было исключение, если

есть запись в адрес памяти, который не выделяется приложением

но в любом случае это происходит, и аппаратное обеспечение, и операционная система в этом уверены.

Если вы имеете в виду, что хотите проверить наличие недействительных записей в памяти в выделенном диапазоне адресов вашего приложения, то вы можете сделать только очень много. Вам следует использовать FastMM4 и использовать его с самыми подробными и параноидальными настройками в режиме отладки вашего приложения. Это отлавливает много недействительных записей, обращений к уже освобожденной памяти и тому подобному, но не может отловить все. Рассмотрим висячий указатель, который указывает на другое место в памяти для записи (например, на середину большой строки или массива значений с плавающей запятой) - запись в него будет выполнена успешно, и он будет уничтожать другие данные, но у менеджера памяти нет возможности отловить такое доступ.

Похоже, у вас есть повреждение памяти данных экземпляра объекта.

Сам VMT не повреждается, FWIW: VMT (обычно) хранится в исполняемом файле, а отображаемые на нем страницы доступны только для чтения. Скорее, как говорит VilleK, похоже, что первое поле данных экземпляра в вашем случае было перезаписано 32-разрядным целым числом со значением 1. Это достаточно легко проверить: проверьте данные экземпляра объекта, вызов метода которого не удался, и убедитесь, что первым мечом является 00000001.

Если это действительно указатель VMT в данных экземпляра, который поврежден, вот как я могу найти код, который его повреждает:

  1. Убедитесь, что существует автоматический способ воспроизвести проблему, которая не требует ввода данных пользователем. Эта проблема может быть воспроизведена только на одной машине без перезагрузок между репродукциями из-за того, как Windows может выбрать расположение памяти.

  2. Воспроизведите проблему и запишите адрес данных экземпляра, чья память повреждена.

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

  4. Теперь, перейдите к третьему прогону, поместите 4-байтовую точку останова данных в раздел памяти, указанный в предыдущих двух прогонах. Дело в том, чтобы сломать каждую модификацию этой памяти. По крайней мере, один разрыв должен быть вызовом TObject.InitInstance, который заполняет указатель VMT; могут быть другие, связанные с конструкцией экземпляра, например, в распределителе памяти; и в худшем случае данные соответствующих экземпляров могли быть переработаны из памяти предыдущих экземпляров. Чтобы сократить количество необходимых шагов, сделайте точку останова данных записывать в стек вызовов, но не прерывать ее. Проверяя стеки вызовов после сбоя виртуального вызова, вы сможете найти неправильную запись.

Первое, что я хотел бы сделать, это добавить MadExcept в ваше приложение и получить трассировку стека, которая распечатывает точное дерево вызовов, что даст вам некоторое представление о том, что здесь происходит. Вместо случайного исключения и двоичного / шестнадцатеричного адреса памяти, вам нужно увидеть дерево вызовов со значениями всех параметров и локальных переменных из стека.

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

Например, в структурах памяти (классы или типы записей) можно расположить Magic1:Word в начале и Magic2:Word в конце каждой записи в памяти. Функция проверки целостности может проверить целостность этих структур, посмотрев, чтобы увидеть, что для каждой записи Magic1 и Magic2 не были изменены по сравнению с тем, что было установлено в конструкторе. Деструктор изменит Magic1 и Magic2 на другие значения, такие как $FFFF.

Я также хотел бы рассмотреть возможность добавления трассировки в мое приложение. Ведение журнала трассировки в приложениях delphi часто начинается с того, что я объявляю форму TraceForm с включенным там TMemo, а функция TraceForm.Trace(msg:String) запускается как "Memo1.Lines.Add(msg)". По мере развития моего приложения средства ведения журнала трассировки - это способ, с помощью которого я наблюдаю за запущенными приложениями на предмет общих шаблонов их поведения и неправильного поведения. Затем, когда происходит "случайный" сбой или повреждение памяти без "объяснения", у меня есть журнал трассировки, чтобы вернуться назад и посмотреть, что привело к этому конкретному случаю.

Иногда дело не в повреждении памяти, а в простых базовых ошибках (я забыл проверить, назначен ли X, затем я разыменую его: X.DoSomething(...), который предполагает, что X назначен, но это не так.

Мги прав, конечно. (fastmm4 вызывает флаг fulldebugmode или что-то в этом роде).

Обратите внимание, что это обычно работает с барьерами непосредственно перед и после выделения кучи, которые регулярно проверяются (при каждом доступе к heapmgr?).

Это имеет два последствия:

  • место, где fastmm обнаруживает ошибку, может отличаться от места, где она произошла
  • полная случайная запись (не переполнение существующего выделения) может быть не обнаружена.

Итак, вот некоторые другие вещи для размышления:

  • включить проверку во время выполнения
  • Просмотрите все предупреждения вашего компилятора.
  • Попробуйте скомпилировать с другой версией Delphi или FPC. Другие компиляторы /rtls/heapmanager имеют разную компоновку, и это может привести к более легкому обнаружению ошибки.

Если все это ничего не дает, попробуйте упростить приложение, пока оно не исчезнет. Затем исследуйте самые последние прокомментированные /ifdefed части.

Я заметил, что таймер находится в трассировке стека.
Я видел много странных ошибок, когда причиной было событие таймера после формы, которую я освободил.
Причина в том, что таймер событий может быть помещен в очередь сообщений, а noge обработан для уничтожения других компонентов.
Одним из способов решения этой проблемы является отключение таймера в качестве первой записи в уничтожении формы. После отключения времени вызовите Application.processMessages, чтобы любые события таймера обрабатывались до уничтожения компонентов.
Другой способ - проверить, уничтожается ли форма за время. (csDestroying в компонентах состояния).

Я думаю, что есть еще одна возможность: таймер срабатывает, чтобы проверить, есть ли "висячие сеансы входа в систему". Затем выполняется вызов объекта TLogonSession, чтобы проверить, может ли он быть отброшен (_GetMayDropSession), верно? Но что, если объект уже разрушен? Может быть, из-за проблем безопасности потока или просто вызова.Free, а не вызова FreeAndNil (поэтому переменная по-прежнему <> nil) и т. Д. И т. Д. В то же время создаются другие объекты, поэтому память используется повторно. Если вы попытаетесь получить доступ к переменной через некоторое время, вы можете / будете получать случайные ошибки...

Пример:

procedure TForm11.Button1Click(Sender: TObject);
var
  c: TComponent;
  i: Integer;
  p: pointer;
begin
  //create
  c := TComponent.Create(nil);
  //get size and memory
  i := c.InstanceSize;
  p := Pointer(c);
  //destroy component
  c.Free;
  //this call will succeed, object is gone, but memory still "valid"
  c.InheritsFrom(TObject);
  //overwrite memory
  FillChar(p, i, 1);
  //CRASH!
  c.InheritsFrom(TObject);
end;

Нарушение прав доступа по адресу 004619D9 в модуле "Project10.exe". Прочитать адрес 01010101.

Не проблема ли в том, что _GetMayDropSession ссылается на свободную переменную сеанса?

Я видел подобные ошибки раньше, в TMS, где объекты освобождались и на них ссылались в onchange и т. Д. (Только в некоторых ситуациях это приводило к ошибкам, которые очень трудно / невозможно воспроизвести, теперь исправляется TMS:-)). Также с сеансами RemObjects я получил нечто подобное (из-за плохой ошибки программирования самостоятельно).

Я бы попытался добавить фиктивную переменную в класс сеанса и проверить ее значение:

  • открытая переменная iMagicNumber: целое число;
  • конструктор create: iMagicNumber:= 1234567;
  • деструктор уничтожить: iMagicNumber: = -1;
  • "другие процедуры": assert(iMagicNumber = 1234567)

Можете ли вы опубликовать исходный код этой процедуры?

BoldSystem.TBoldMember.CalculateDerivedMemberWithExpression (BoldSystem.pas: 4016)

Итак, мы можем видеть, что происходит на линии 4016.

А также процессор вид этой функции?
(просто установите точку останова в строке 4016 этой процедуры и запустите. И скопируйте + вставьте содержимое представления CPU, если вы достигли точки останова).
Таким образом, мы можем видеть, какая инструкция процессора находится по адресу 00404E78.

Может ли быть проблема с повторным вводом кода?

Попробуйте поместить некоторый защитный код вокруг кода обработчика событий TTimer:

procedure TAttracsTimerDataModule.AttracsTimerTimer(ASender: TObject);
begin
  if FInTimer then
  begin
    // Let us know there is a problem or log it to a file, or something. 
    // Even throw an exception
    OutputDebugString('Timer called re-entrantly!'); 
    Exit; //======> 
  end;

  FInTimer := True;
  try

    // method contents

  finally
    FInTimer := False;
  end;
end;

N @

Другие вопросы по тегам