Какой правильный инструмент для обнаружения 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, но это могут быть также 0, 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 в данных экземпляра, который поврежден, вот как я могу найти код, который его повреждает:
Убедитесь, что существует автоматический способ воспроизвести проблему, которая не требует ввода данных пользователем. Эта проблема может быть воспроизведена только на одной машине без перезагрузок между репродукциями из-за того, как Windows может выбрать расположение памяти.
Воспроизведите проблему и запишите адрес данных экземпляра, чья память повреждена.
Перезапустите и проверьте второе воспроизведение: убедитесь, что адрес данных экземпляра, который был поврежден во втором запуске, совпадает с адресом первого запуска.
Теперь, перейдите к третьему прогону, поместите 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 @