Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4439.html
 

В поисках ошибки (runtime 216)

Опубликовано 13.09.2010 г. 23:35

Эту небольшую заметку я посвящу рассказу о поиске ошибки (runtime error 216) в проекте при его закрытии. Как оказалось ошибка была тривиальна в своей сути, но необычна своими последствиями.

Предыстория такова: необходимо к заданному времени "реанимировать" немаленькую расчетную программу. Обновить некоторые модули, подправить алгоритмы и т.п. Перед началом работы я решил обновить наборы компонентов TMS, ибо с каждым разом там исправляются старые ошибки, правда появляются и новые. В ходе работы по обновлению кода мое внимание привлекло возникновение ошибки при закрытии программы (runtime error 216 - ошибки обращения к памяти). Решение данной проблемы не было приоритетным, и сразу решать ее я не стал, поскольку решил, что возникнуть она могла в следствии повторного освобождения какого-нибудь объекта при закрытии. Появилось свободное время, и изрядно утомившись закрывать диалоговое окно сообщения об ошибке каждый раз при завершении программы, я отправился на поиски. Сразу скажем, что проект был не маленький, и насчитывал порядка 30-40 юнитов (MDI приложение). Переписанных деструкторов классов в проекте не было, были проверены все обработчики событий закрытия форм и т.п. Ошибка возникала после того как завершался обработчик onClose главной формы. После него никаких других событий не вызывалось. Установив опцию use debug DCUs в значение true отладка продолжилась. Стандартный вид кода приложения примерно таков:
begin
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

строка application.run() реализует цикл обработки сообщений, и пока наше приложение запущено, то этот цикл продолжается. При закрытии приложения данный цикл прерывается, и если мы установим точку останова на ключевом слове end то попадем в процедуру system._halt0. Существует также процедура halt() которая используется в консольных приложениях для закрытия приложения с ошибкой, а код 0 используется обычно для обозначения корректного завершения приложения. Поэтому рассматриваем суффикс 0 в конце имени процедуры как знак того, что пока что все правильно (: Если упростить то смысл работы данной процедуры в том чтобы освободить все загруженные библиотеки и модули, и вызвать обработать секции finalization для всех юнитов (секции initialization, напротив, выполняются до запуска приложения (т.е создания объекта application)). Дак вот опытным путем было установлено что ошибка возникала при вызове метода FinalizeUnits(). В данном методе проводится последовательный циклический вызов метода финализации для юнитов. Причем метод рекурсивный, рекурсия запускается в случае возникновения исключительной ситуации, чтобы отработать finalization для оставшихся модулей. Суть беды была в том, что в моих юнитах секции finalization отсутствовали как класс в принципе. Однако в подключаемых юнитах TMS их было не мало. В очередной раз обновили TMS. Ошибка не исчезла. Печально (: После того как была определена итерация цикла на которой возникала ошибка, я углубился еще дальше, перейдя в метод System._FinalizeArray. Судя по названию метода, мы очищали память какого то массива. Если предыдущий шаг мне был не совсем понятен, то на данном этапе код функции представлял ассемблерный код, что посеяло еще большие сомнение в успешности поисков. Далее переход по цепочке вызовов _WStrArrayClr() и _LStrArrayClr(), что видимо описывало очистку массива строк. Функция _LStrArrayClr() была последней стадией отладки. В данном методе реализован цикл очистки, каждая итерация которого завершается вызовом _freeMem. Основываясь на комментариях к коду можно было сделать заключение что в регистр EDX передает адрес буфера строки. Добавив pChar(EDX) в WatchList при первой итерации цикла я наблюдал имя исполняемого файла, при второй полный путь с именем исполняемого файла. После чего наблюдалось появление ошибки в секции

@@doneEntry:
        ADD     EBX,4
        DEC     ESI
        JNE     @@loop

на строке dec ESI если мне память не изменяет, что показалось странным. Тут я понял, что зашел в тупик, и мои знания программирования не способны решить данную задачу стандартными методами отладки. Когда ошибка не очевидна, то первым, что можно сделать это сократить объем кода, чтобы попытаться таким образом ее как либо локализовать. Так что я создал копию проекта и начал удалять юниты один за другим, проверяя возникновение ошибки. В конечном счете осталось 3 юнита: главная форма приложения, модуль данных, модуль описания глобальных переменных и типов. Третий модуль не выполнял никаких действий, так что влиять теоретически не мог. Модуль данных и главная форма создавались в приложении автоматически. Помня о том что секции finalization есть только в юнитах TMS, а ошибка возникает где то в FinalizeUnits() начал удалять компоненты с главной формы, на этом правда не остановился. главная форма в итоге осталась пустая, и на ней был расположен лишь ADOConnection (не спрашивайте почему объект подключения к БД на главной форме а не в модуле данных (: ) Вот тут фокус, в секции uses не осталось ни одного юнита от TMS. Откуда же тогда ошибка в FinalizeUnits() ? Следующим шагом была очистка модуля данных (вобще то они выполнялись одновременно). В конечном счете все приложение состояло из 4х функций которые были максимально упрощены. Главная форма содержала обработчик события onCreate() в к котором был вызывался метод Connect() [см. код] модуля данных.

procedure TDBModule.Connect();
begin
    loadOkvedNames();
    loadRegionName();
end;

procedure TDBModule.LoadOkvedNames();
begin
    with adoQuery1 do begin
        sql.text := '....';
        open();
        while not eof do begin
            next();
            inc(i);
        end;
        close();
    end;
end;

procedure TDBModule.LoadRegionName();
var id:integer;
begin
    regionExists[id] := id;
end;

Итак, глобальный массив regionExists был определен как

regionExists : array[0..1] of integer;

переменная id имела значение явно не из интервала 0..1. (28 было). Следовательно происходила запись в чужую память. Отметим так же, что после объявления массива далее была определена переменная region - структура вида:

type TRegion = record 
        name : string[100];
    end;
var reg : TRegion

Память при этом выделяется последовательно т.е адрес элемента regionExists[28] ссылался куда то на начало строки name переменной reg. В принципе чего страшного? Записали данные в чужой буфер, но он ведь статичен, и был пуст на тот момент. Установка требуемого значения id решила проблему. Однако, оставим все без изменений. В чем же необычность?.

  1. В силу каких то магических обстоятельств, если мы закомментируем вызов loadOkvedNames() то программа закрывается без ошибок.
  2. Если мы в коде функции LoadOkvedNames закомментируем, например, строку inc(i) то программа вылетает при запуске. То же самое касается всего содержимого между with adoQuery1 do begin и end;

Обычно ли такое поведение? Нет, вряд ли. Какие можно сделать выводы? -

  1. Лучше лишний раз написать код инициализации переменной начальным значением, чем потом долго искать ошибку. Это правда не касается членов классов, которые инициализируются нулевыми значениями.
  2. Стоит использовать Range check в настройках компиляции, хотя бы в DEBUG варианте сборки.
Метки:  debug  |  error 

Комментарии

JayDi
14.09.2010 в 01:32
Была такая же ошибка (runtime error 216) при выходе из программы -- причина оказалась в том, что при некоторых условиях одна из форм не уничтожалась и сбивала работу всего приложения.

А вообще, подобное "магическое поведение кода" встречается регулярно -- ошибки, на появление которых влияют совершенно левые строчки из другой части проекта, либо странное поведение отладчика, и перескок выполнения текущей команды в неожиданное место.

Если вижу подобные признаки -- значит надо где-то искать неинициализированную либо неправильно уничтоженную переменную.
GunSmoker
14.09.2010 в 03:48
Самое обычное повреждение памяти. Повреждение памяти - причина больше половины случаев для run-time error 216.

И про опцию range-check многие забывают (или не знают?). Печально...
Bonart
14.09.2010 в 09:10
Универсальный рецепт:
1. Никогда не использовать глобальные переменные.
2. Никогда не отключать Range check за исключением релизных версий критичных по быстродействию участков кода.
ter
14.09.2010 в 11:11
почему кстати RangeCheck отключен по дефолту?
GunSmoker
14.09.2010 в 21:53
Вероятнее всего, наследие старых времён, когда у вас не было профиля Debug и Release.
ter
14.09.2010 в 22:43
дак даже создавая новый проект по дефолту отключена проверка.
ASLR
28.02.2014 в 21:49
Спасибо. Очень помогло при подобном баге.
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно