EAccessViolation
при выполнении кода определённого события.Беглое знакомство с проектом показало, что приложение возбуждало исключение
EAccessViolation
с текстом "Access violation at 0x0108164d: read of address 0xdeadc30f
" и таким стеком вызовов:
ContosoInplaceContainer.TContosoCustomViewInfoItem.Destroy ContosoInplaceContainer.TContosoEditCellViewInfo.Destroy System.TObject.Free ContosoClasses.TContosoObjectList.FreeItem ContosoClasses.TContosoObjectList.Clear ContosoGrid.TContosoCustomRowViewInfo.ClearValuesInfo ContosoGrid.TContosoCustomMultiEditorRow.EditorsChanged ContosoGrid.TContosoEditorPropertiesCollection.Update System.Classes.TCollection.Changed System.Classes.TCollection.RemoveItem System.Classes.TCollectionItem.SetCollection System.Classes.TCollectionItem.Release System.Classes.TCollectionItem.Destroy ContosoGrid.TContosoCustomEditorRowProperties.Destroy System.TObject.Free System.Classes.TCollection.Delete Unit2.TForm2.DeleteEditor Unit2.TForm2.PropertiesEditValueChanged ContosoEdit.TContosoCustomEditingController.EditValueChanged ContosoInplaceContainer.TContosoEditingController.EditValueChanged ...Проблемный код выглядел так:
destructor TContosoCustomViewInfoItem.Destroy; begin // Вылетает в строке ниже: if (Control <> nil) and (Control.Controller.HotTrackController.HintElement = Self) then Control.Controller.HotTrackController.CancelHint; inherited Destroy; end;Здесь слово "Contoso" ссылается на Contoso Ltd. - фиктивную (выдуманную) компанию, используемую Microsoft в качестве примеров. В данном случае это слово скрывает настоящего разработчика библиотеки, поскольку смысл этой заметки в том, чтобы показать, как EurekaLog помогает отыскивать проблемы в коде, а не в том, чтобы тыкать в кого-то пальцем.
В отличие от большинства подобных историй, этот случай было достаточно просто расследовать - не в последнюю очередь благодаря тому, что у нас был надёжно воспроизводимый пример.
Во-первых, обратите внимение, что сообщение исключения выглядит так: "...read of address 0xdeadc30f". Заметьте, что какой-то код пытается что-то прочитать по адресу DEADC30F, который лежит очень близко к адресу DEADBEEF. Адрес DEADBEEF - это специальный отладочный маркер (см. описание опции "When memory is released"), которым заполняется освобождённая память. Иными словами, этот код пытается читать из уже удалённого объекта.
Вышесказанное означает, что в коде клиента (или сторонней библиотеке Contoso) есть баг типа "use after free" (доступ к удалённой памяти). Этот вывод 100% точен, эта ситуация НЕ может быть ложно-положительной. Другими словами, если клиент уверен, что его код корректен - то он только что нашёл баг в сторонней библиотеке Contoso (наши поздравления!). И наоборот: если разработчики Contoso утверждают, что их код корректен, то баг сидит в коде клиента (то, как он работает с библиотекой Contoso).
Вооружённые этим знанием, нам теперь осталось лишь просто пройтись по коду клиента, уделяя внимание удаляемым объектам. Вот как проходит выполнение:
- Метод
TContosoCustomEditingController.EditValueChanged
вызывает обработчик события, который назначен на код клиента. Обработчик события клиента (PropertiesEditValueChanged
) вызываетRow1.Properties.Editors.Delete(1);
. - Метод
Delete
здесь - это обычный методTCollection
из RTL. Он удаляет элемент из коллекции:
procedure TCollection.Delete(Index: Integer); begin Notify(TCollectionItem(FItems[Index]), cnDeleting); TCollectionItem(FItems[Index]).DisposeOf; // здесь end; destructor TContosoCustomEditorRowProperties.Destroy; begin FreeAndNil(FEditContainer); // ВАЖНО inherited Destroy; // здесь end;
Обратите внимение, что методTContosoCustomEditorRowProperties.Destroy
удаляет (своё) полеFEditContainer
. Однако на этот объект всё ещё есть ссылка в другом месте - как мы увидем ниже. - Теперь деструктор
TCollectionItem
отсоединяет себя от владельца-коллекции:
destructor TCollectionItem.Destroy; begin if FCollection <> nil then Release; // здесь inherited Destroy; end; procedure TCollectionItem.Release; begin SetCollection(nil); // здесь end; procedure TCollectionItem.SetCollection(Value: TCollection); begin if FCollection <> Value then begin if FCollection <> nil then FCollection.RemoveItem(Self); // здесь if Value <> nil then Value.InsertItem(Self); end; end; procedure TCollection.RemoveItem(Item: TCollectionItem); begin Notify(Item, cnExtracting); if Item = FItems.Last then FItems.Delete(FItems.Count - 1) else FItems.Remove(Item); Item.FCollection := nil; NotifyDesigner(Self, Item, opRemove); Changed; // здесь end;
- Как часть этого процесса: будут запущены обработчики уведомлений, в частности, включая
TContosoEditorPropertiesCollection.Update
:
procedure TCollection.Changed; begin if FUpdateCount = 0 then Update(nil); // здесь end; procedure TContosoEditorPropertiesCollection.Update(Item: TCollectionItem); var I: Integer; begin for I := 0 to Count - 1 do GetItem(I).EditContainer.FCellIndex := I; Row.EditorsChanged; // здесь end;
- Далее библиотека Contoso попытается обновить редакторы:
procedure TContosoCustomMultiEditorRow.EditorsChanged; begin if Properties.Locked or VerticalGrid.IsLoading then Exit; ViewInfo.ClearValuesInfo; // здесь Changed; end; procedure TContosoCustomRowViewInfo.ClearValuesInfo; begin FIsRightToLeftConverted := False; FInitialized := False; ValuesInfo.Clear; // здесь ValuesLinesInfo.Clear; end; procedure TContosoObjectList.Clear; var I: Integer; begin if OwnObjects then begin for I := 0 to Count - 1 do // = 2 FreeItem(I); // здесь end; inherited Clear; end;
- В списке
ValuesInfo
находится два элемента. Первый (с индексом 0) удаляется успешно, а вот второй (с индексом 1) и вызывает проблему:
destructor TContosoEditCellViewInfo.Destroy; begin if (EditContainer <> nil) and not EditContainer.IsDestroying then // здесь // ... end;
Вот и проблема:EditContainer
являетсяOwner
-ом и он указывает на уже удалённый объект - то самое поле, удалённое на шаге 2 внутриTContosoCustomEditorRowProperties.Destroy
.
- Код клиента удаляет элемент из коллекции;
- Элемент удаляет свои поля;
- Коллекция уведомляет об удалении элемента;
- Код уведомления пытается очистить ассоциированную информацию и в процессе пытается читать из уже удалённого поля/объекта.
Вы можете спросить: а как же код работал "безупречно" до добавления EurekaLog в проект? Ну это просто: удалённый объект оставался в памяти без изменений, поэтому
TContosoEditCellViewInfo.Destroy
мог успешно получить доступ к уже удалённому объекту и прочитать из него неизменённые данные. Добавление EurekaLog в проект с настройками по умолчанию (включенные проверки памяти) изменяет это поведение, приводя к настоящему стиранию удаленных объектов/памяти.Итак, является ли это ошибкой в библиотеке Contoso? Хотя это, безусловно, выглядит именно так, мы не можем быть уверены в этом на 100%, так как не являемся экспертами в этой библиотеке. Так что вполне возможно, что это ошибка в коде клиента.
Клиент сообщил, что эта проблема будет передана в службу поддержки библиотеки Contoso.
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.