Как найти ошибку в delphi

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

Частой ошибкой начинающих является пропуск конструкции begin – end в цикле. При
этом имеется в виду, что в цикле должны выполняться, например, обе команды, но на самом-то деле в цикле, конечно, будет выполняться только первая, а вторая выполнится только один раз – потом, когда программа выйдет из цикла. При попытке запуска появляется сообщение. Но это не ошибка, а предупреждение. В нем обращается внимание на то, что параметр цикла после выполнения цикла может быть неопределенным (он присутствует во второй команде).

Однако, несмотря на предупреждение, программа может запуститься. Если вводятся
дкакие-либо данные, то появляется сообщение об исключительной ситуации – exception. При этом программа приостанавливается, переходя из режима исполнения в режим отладки. Чтобы перейти к обычному редактированию кода, лучше остановить программу. Это можно сделать с помощью команды Program Reset. Затем можно поправить ошибку и вновь запустить программу.

В большие программы всегда закрадываются ошибки. Их надо быстро и квалифицированно найти и исправить.

Механизм исключительных ситуаций (exception)
одно из больших достоинств Delphi. С их помощью вы можете контролировать
возникновение ошибок и создавать в результате устойчивые к ошибкам программы.

По мере знакомства с языком и средой программист проходит несколько этапов. На
первом этапе он, по незнанию, путает типы, забывает ставить знаки препинания (например, точку с запятой в конце строки), некорректно использует операторы и т.п. В результате написанный им код в принципе невозможно исполнить. И это хорошо – 15 поскольку допущенные им ошибки оказываются автоматически выявленными на этапе компиляции, более того, часто среда программирования сама подсказывает, какая ошибка допущена, и, что важно, указывает строку, которую нужно поправить. По мере изучения языка и борьбы с синтаксическими ошибками программист плавно переходит к следующему этапу. Теперь он уже не делает таких простейших ошибок, но, поскольку сложность его программ возрастает, возрастает и вероятность совершения им ошибки, при которой программа все равно запустится. Поскольку, с точки зрения компилятора, явной ошибки нет, а некоторые странности кода, по-видимому, являются замыслом программиста. Однако компилятор все-таки сообщает об этих странностях с помощью предупреждений (Warning).

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

Опасность таких скрытых ошибок состоит:

1) в том, что они таятся в той части кода, которую программист написал и уверен, что
она правильная (программа запустилась), а значит, и не очень внимательно будет
искать ошибку;

2) в том, что проявляется эта ошибка совсем в другом месте кода – не в том, в котором
допущена. А это приводит к долгим поискам ее по всей программе.

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

Когда исполняемый код доходит до точки останова, исполнение программы
приостанавливается, и надо перейти в режим отладки. В режиме отладки можно исполнять последовательно программу по шагам, контролировать и изменять значения переменных и т.п. Этот режим служит для обнаружения и ликвидации ошибок. В этом случае появляется возможность просмотреть или изменить текущие значения переменных, однако изменение кода во время отладки невозможно. Измененный текст заработает только после перезапуска программы.

Чтобы выполнить текущую строку, на которой стоит курсор отладки, нажмите F7 или
F8. Строка выполнилась, и курсор сместился. Если необходимо перейти к следующей строке, то можно нажать F8, если нужно зайти в какую-либо функцию, то нажимают клавишу F7 и продолжают трассировку.

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

Для более основательного слежения за значениями можете воспользоваться Списком Наблюдения (Watch List, Ctrl+F5).

Таким образом, при программировании среда Delphi может находиться в различных
режимах:

• Режим редактирования – режим, в котором редактируется код проекта,
модифицируется форма, добавляя на нее компоненты и настраивая их свойства. Это
основной режим.

• Режим исполнения программы – режим, в который среда переходит, как только
нажата клавиша F9 и был построен exe-файл. Фактически, в этом режиме происходиткак раз исполнение получившегося exe-файла проекта. Программа исполняется так,
как если бы ее вызвали не из Delphi, а просто из Windows.

• Режим отладки – в этот режим можно перейти из режима исполнения программы.

При этом программа будет приостановлена (но не остановлена совсем).

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

• F9 (Run) – продолжить программу, не трассируя ее.

• F8 (Step over) – выполняется текущая строка кода, и переходят к следующей строке.

• F7 (Trace Into) – то же, что и F8, с тем отличием, что если в текущей строчке
содержится вызов какой-либо функции или процедуры, то попадают внутрь этой
процедуры и трассируют ее до конца, затем из нее возвращаетс и переходят к
следующей строке (на которую перешли бы сразу, если бы нажали F8).

• F4 (Run to Cursor) – переход в режим исполнения программы до тех пор, пока не
должна будет выполнена строка, на которой стоит текстовый курсор (аналогично
тому, как если бы была установлена точка останова)

• Shift+F8 (Run Until Return) – процедура выполняется до конца.

• Ctrl+F2 (Program Reset) – остановка трассировки и переход в режим редактирования
кода. (Иногда целесообразнее, если это не грозит ошибками, продолжить исполнение
программы (F9) и выйти из нее нормальным образом, закрыв главную форму).

При работе в Delphi сообщение об ошибке фактически появляется дважды: сначала
выводится окно об исключительной ситуации и программа приостанавливается, а потом,
если нажать F9 (F8, F7 и т.п.), – возникает стандартное сообщение об ошибке Windows.

Итак:

1. Произошла ошибка.

2. Программа приостанавливается.

3. Выводится сообщение об exception. Это сообщение для программиста. Среда Delphi
сообщает, что программа не в состоянии выполнить какую-то свою команду.
Программист не предусмотрел возможность исключительной ситуации. Среда Delphi
приостанавливает программу, чтобы программист разобрался, где и в чем ошибка.
Отключить приостановку (2)–(3) можно, сняв флажок Menu => Tools => Debugger
Options => Language Exceptions => Stop on Delphi Exceptions.

4. Нажатие клавиши F9 (F8, F7 или др.).

5. Выводится сообщение об ошибке. Это сообщение для пользователя программы
(ситуация запуска приложения не из Delphi, а через exe-файл из Windows, т.е. не
существовует пунктов 2, 3, 4).

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

Механизм обработки исключительных ситуаций заключается в том, что если
произошла ошибка (1) и не надо выводить (5), предпринимаются действия, чтобы (6) исполнялось корректно.

Для этого «опасная» команда (или целый блок) помещается внутрь конструкции try..except..end или try..finally..end.

Блок try..finally..end используется аналогично try..except.., но с тем отличием, что блок
команд между finally и end выполняется в любом случае, вне зависимости от того, было исключение между try и finally или нет.

Ошибки — неизбежное зло программирования. Видимо пока трудно даже представить средство с помощью которого можно избавится от них. Человеку, которые выдумает это чудодейственное лекарство, благодарные потомки-программисты, несомненно, воздвигнут памятник. Пока же остается лишь заниматься обычным делом: ловлей багов.
«Нарушение Доступа» — фраза, которую пользователи видят, когда приложение делает попытки обратиться к памяти, которая не обозначена для их использования — и как следствие происходит сбой в работе программы:

Access violation at address

in module .
Read of address
Ситуация при которой Windows давала бы полную свободу программам — записывай данные куда хочешь, скорее всего бы привела к разноголосице программ и полной потери управления над компьютером. Но этого не происходит — Windows стоит на страже «границ памяти» и отслеживает недопустимые операции. Если сама она справиться с ними не в силах — происходит запуск утилиты Dr. Watson, которая записывает данные о возникшей ошибки, а сама программа закрывается.
Известно что, при программирование, особенно крупных программных продуктов, уследить за всеми процессами в коде невозможно, да и нет необходимости. Использование сторонних компонентов и библиотек только усложняет дело. Именно поэтому программисты Delphi, порой и сталкиваются со «своенравными» программами, которые то и дело норовят «сбросить пользователя». Итак, давайте рассмотрим некоторые вопросы, связанные с корректной среды программирования, так и непосредственно проблемы написания кода, которые ведут к возникновению ошибок типа «ошибка доступа» (AVS) и очертим наиболее известные пути их исправления.
Мы можем поделить AVS, с которыми сталкиваются при разработке в Delphi на два основных типах: ошибки при выполнения и некорректная разработка проекта, что вызывает ошибки при работе программы.
Ошибки возникают при старте и закрытии Delphi или формировании проекта. Причиной могут являться сбои в «железе» компьютера.
Эти ошибки могут быть вызваны различными источниками, включая систему BIOS, операционную систему или аппаратные подпрограммы драйверов. Некоторые видео-, звуковые или сетевые платы могут фактически вызывать подобного рода ошибки в Delphi. Для решения подобных аппаратных проблем можно предпринять последовательность неких «стандартных» ходов:
проверить, что не имеется никаких конфликтов между установленными устройствами, устранить обнаруженные конфликты;
попробовать слегка уменьшить «аппетита» видеодрайвера — поставить меньшее разрешение;
в случае если у вас двухпроцесорная система обеспечить равное изменение шага для каждого процессора;
И в конце концов просто попытаться заменить драйвера на более свежие.

Но помимо чисто железных проблем — большую головную боль могут вызвать ошибки в работе программного обеспечения. Особенно это касается непосредственно операционной системы. Зачастую Windows терпит крах спонтанно. Вот рекомендации которые помогут вам создать более устойчивую среду программирования:
Хотя Windows 9X популярная система, разработку лучше проводить в Windows NT или Windows 2000 — это более устойчивые операционные системы. Естественно при переходе на них придется отказаться от некоторых благ семейства Windows 95/98/Me — в частности не все программы адоптированы для Windows NT/2000. Зато вы получите более надежную и стабильную систему.
Не забывайте о том, как важно всегда иметь под рукой свежие версии компонентов для Delphi и дополнительных библиотек. В отличие от Windows создатели данных пакетов стараются от версии к версии уменьшать количество ошибок.
Следите за тем, что бы устанавливаемые компоненты были предназначены непосредственно для вашей версии Delphi. Попробуйте деинсталлировать чужеродные компоненты один за другим (или пакет за пакетом) пока проблема не будет устранена.
Контролируйте все программные продукты установленные на вашей машине и деинсталлируйте те из них, которые сбоят. Фаворитами AV среди них являются шароварные утилиты и программы и бета версии программных продуктов.
Все вышеперечисленное в основном не касалось самого процесса программирования и в малой степени зависит от разработчика. Теперь же обратимся к теме, как не допустить при разработки программного продукта ситуации при которой, он сам будет являться причиной ошибки.
Вы могли бы рассмотреть компилирование вашего приложения с директивой {$D}, данная директива компилятора может создавать файлы карты (файлы с расширением map, которые можно найти в том же каталоге, что и файлы проекта), которые могут послужить большой справкой в локализации источника подобных ошибок. Для лучшего «контроля» за своим приложением, компилируйте его с директивой {$D}. Таким образом, вы заставите Delphi генерировать информацию для отладки, которая может послужить подспорьем при выявление возникающих ошибок.
Следующая позиция в Project Options — Linker & Compiler позволяет вам, определить все для последующей отладки. Лучше всего, если помимо самого выполняемого кода будет доступна и отладочная информация — это поможет при поиске ошибок. Отладочная информация увеличивает размер файла и занимает дополнительную память при компилировании программ, но непосредственно на размер или быстродействие выполняемой программы не влияет. Включение опций отладочной информации и файла карты дают детальную информацию только, если вы компилируете программу с директивой {$D+}.
Эта информация состоит из таблицы номеров строк для каждой процедуры, которая отображает адреса объектных кодов в номера строк исходного текста. Директива $D обычно используется совместно с другой директивой — $L, что позволяет или запрещает генерацию информации о локальных символах для отладки.
Таким образом вы без труда сможете найти точный адрес той подпрограммы, которая была ответственна за ошибку. Одна из наиболее общих причин ошибок выполнения — использование объекта, который еще не был создан. Если второй адрес при выдачи ошибки — FFFFFFF (или 0000000) Вы можете почти утверждать, что было обращение к объекту, который еще не был создан. Например, вызов метода формы, которая не была создана.

procedure TfrMain.OnCreate(Sender: TObject);
 var BadForm: TBadForm;
 begin
   BadForm.Refresh; // причина  ошибки
 end;
 

Попытаемся разобратся в этой ситуации. Предположим, что BadForm есть в списке «Available forms » в окне Project Options|Forms. В этом списке находятся формы, которые должны быть созданы и уничтожены вручную. В коде выше происходит вызов метода Refresh формы BadForm, что вызывает нарушение доступа, так как форма еще не была создана, т.е. для объекта формы не было выделено памяти.
Если вы установите «Stop on Delphi Exceptions » в Language Exceptions tab в окне Debugger Options, возможно возникновения сообщение об ошибке, которое покажет, что произошло ошибка типа EACCESSVIOLATION. EACCESSVIOLATION — класс исключение для недопустимых ошибок доступа к памяти. Вы будете видеть это сообщение при разработке вашего приложения, т.е. при работе приложения, которое было запущено из среды Delphi.
Следующее окно сообщения будет видеть пользователь — и программа будет закрыта при совершение недопустимой операции:

 Access violation at address 0043F193
 in module 'Project1.exe'
 Read of address 000000.
 

Первое шестнадцатиричное число (‘0043F193’) — адрес ошибки во время выполнения программы в программе. Выберите, опцию меню ‘Search|Find Error’, введите адрес, в котором произошла ошибка (‘0043F193’) в диалоге и нажмите OK. Теперь Delphi перетранслирует ваш проект и покажет вам, строку исходного текста, где произошла ошибка во время выполнения программы, то есть BadForm.Refresh.
Естественно, что списка наиболее общих причин ошибок, вызывающих аварийное завершение работы программы, написанной в Delphi в чистом виде нет. Есть несколько общих «узких мест» в коде и структуре программы, когда подобная ошибка может произойти. Перечислим наиболее распространенные.

Недопустимый параметр API

Если вы пытаетесь передать недопустимый параметр в процедуру Win API, может произойти ошибка. Необходимо отслеживать все нововведения в API при выходе новых версий операционных систем и их обновлений.

Уничтожение исключения

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

 Zero:=0;
 try
    dummy:= 10 / Zero;
 except on E: EZeroDivide do
    MessageDlg('Can not divide by zero!', mtError, [mbOK], 0);
    E.free. // причина ошибки
 end;
 

Индексация пустой строки
Пустая строка не имеет никаких достоверных данных. Следовательно, попытка индексировать пустую строку — подобно попытке обратиться к нулю, что приведет также к ошибке:

 var s: string;
 begin
    s:='';
    s[1]:='a'; // причина ошибки
 end;
 

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

 procedure TForm1.Button1Click(Sender: TObject);
 var
   p1 : pointer;
   p2 : pointer;
 begin
   GetMem(p1, 128);
   GetMem(p2, 128);
  {эта строка может быть причиной ошибки}
   Move(p1, p2, 128);
  {данная строка корректна }
   Move(p1^, p2^, 128);
   FreeMem(p1, 128);
   FreeMem(p2, 128);
 end;
 

Вот и всё, Удачи!

0 / 0 / 0

Регистрация: 24.10.2009

Сообщений: 6

1

09.10.2013, 18:51. Показов 8458. Ответов 4


Студворк — интернет-сервис помощи студентам

Привет всем! Есть код на Delphi 7, программа парсит в инете выбирает информацию. Код довольно большой. Вопрос в следующем как сопоставить адрес ошибки Access violation at address 0048AEC4 in module … read of address 00000000, с конкретной строкой в программе? Теоретически понятно, что где то программа обращается например к элементу массива, за его пределами, но при этом программа не ломается а продолжает выполняться. Так как запросы в инет на получение данных, она шлет по таймеру. Отловить ошибку не представляется возможным, т.к. модуль может работать часами и не сбоить, а может вывалиться, через пару минут. Отсюда вопрос как сопоставить адрес 0048AEC4 с конкретной строкой кода. Если кто в теме откликнетесь, перечитал кучу советов, бьюсь уже кучу времени и отдачи нет. Заранее спасибо!!!



0



пофигист широкого профиля

4662 / 3096 / 855

Регистрация: 15.07.2013

Сообщений: 17,855

09.10.2013, 20:57

2

Запустить программу под отладчиком, поставив точку останова там, куда процесс обязательно попадет (например на нажатие какой-нибудь кнопки, если в программе есть хотя бы одна кнопка). Когда процесс остановится, открыть меню Делфи -> Search -> Goto Address. В окошко ввести адрес указанный в сообщении об ошибке ($0048AEC4). Ну а затем попытаться развернуть стек вызовов глядя в окошко Call Stack (Меню Делфи -> View -> Debug Windows -> Call Stack.

Если это для вас слишком сложно, то есть готовые средства. Например Эврика



2



Ivanjulai

0 / 0 / 0

Регистрация: 24.10.2009

Сообщений: 6

09.10.2013, 21:20

 [ТС]

3

Спасибо! Да чего я и боялся случилось… Предложенным методом нашел, что ошибка возникает в строке:
Temp := Dispatch.GetIDsOfNames(GUID_NULL, NameRefs, NameCount,
GetThreadLocale, DispIDs);
Которая находится в следующей процедуре:

Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
procedure GetIDsOfNames(const Dispatch: IDispatch; Names: PChar;
  NameCount: Integer; DispIDs: PDispIDList);
 
  procedure RaiseNameException;
  begin
    raise EOleError.CreateResFmt(@SNoMethod, [Names]);
  end;
 
type
  PNamesArray = ^TNamesArray;
  TNamesArray = array[0..0] of PWideChar;
var
  N, SrcLen, DestLen: Integer;
  Src: PChar;
  Dest: PWideChar;
  NameRefs: PNamesArray;
  StackTop: Pointer;
  Temp: Integer;
begin
  Src := Names;
  N := 0;
  asm
    MOV  StackTop, ESP
    MOV  EAX, NameCount
    INC  EAX
    SHL  EAX, 2  // sizeof pointer = 4
    SUB  ESP, EAX
    LEA  EAX, NameRefs
    MOV  [EAX], ESP
  end;
  repeat
    SrcLen := StrLen(Src);
    DestLen := MultiByteToWideChar(0, 0, Src, SrcLen, nil, 0) + 1;
    asm
      MOV  EAX, DestLen
      ADD  EAX, EAX
      ADD  EAX, 3      // round up to 4 byte boundary
      AND  EAX, not 3
      SUB  ESP, EAX
      LEA  EAX, Dest
      MOV  [EAX], ESP
    end;
    if N = 0 then NameRefs[0] := Dest else NameRefs[NameCount - N] := Dest;
    MultiByteToWideChar(0, 0, Src, SrcLen, Dest, DestLen);
    Dest[DestLen-1] := #0;
    Inc(Src, SrcLen+1);
    Inc(N);
  until N = NameCount;
  Temp := Dispatch.GetIDsOfNames(GUID_NULL, NameRefs, NameCount,
    GetThreadLocale, DispIDs);
  if Temp = Integer(DISP_E_UNKNOWNNAME) then RaiseNameException else OleCheck(Temp);
  asm
    MOV  ESP, StackTop
  end;
end;

Которая в свою очередь находится в ComObj.pas из soursertlcommon вот и приехали… Я так понимаю исправить это сложно… Самое странное, что ни к каким видимым ошибкам она не приводит, кроме того, что программа встает колом и если сказать ОК и стартануть программу снова, она продолжает работать…



0



пофигист широкого профиля

4662 / 3096 / 855

Регистрация: 15.07.2013

Сообщений: 17,855

09.10.2013, 21:58

4

Цитата
Сообщение от Ivanjulai
Посмотреть сообщение

Так как запросы в инет на получение данных, она шлет по таймеру

Вот кстати узкое место. Периодический процесс по таймеру с непредсказуемым временем окончания. Проверьте внимательно не может ли где что-то не успевать создаваться или уничтожаться? Или наоборот что-то уже уничтожено а к нему поступает задержавшееся обращение.



1



0 / 0 / 0

Регистрация: 24.10.2009

Сообщений: 6

10.10.2013, 18:42

 [ТС]

5

Спасибо, ясно где собака порылась… Хотя вроде логика там верная, таймер запускается, через определенное время, по завершению события DocumentComplit… Ладно главное теперь знаю как найти место ошибки, чего и добивался, еще раз спасибо!!!



0



Примечание для людей, заходящих сюда из поисковика: эта статья написана для разработчиков программ. Если вы не программист и не пытаетесь исправить ошибку в СВОЕЙ программе, эта статья — не для вас. До свидания. Извините, что потратил ваше время.

Примечание для студентов/новичков, пишущих на Delphi/C++ Builder: эта статья написана для диагностики исключений в вашей программе. Если вместо этого вы получаете ошибки от самой IDE (а не от вашей программы), например, access violation в пакете dclite60.bpl, то эта статья — не для вас. Чтобы решить проблемы с IDE — идите сюда. Краткий ответ: не надо использовать динозавров (Delphi 5/6/7), используйте современные IDE (Delphi XE и выше). Если всё же хочется динозавров, то часто причиной является DEP. Т.е. нужно добавить Delphi/Builder в исключения DEP. Ну или на крайний случай — отключить/удалить конфликтующий пакет.

Итак, для всех прочих (а именно: разработчиков Delphi/C++ Builder, пытающихся решить проблему возникновения исключения Access Violation в своей программе) — приступим!

Исключение класса EAccessViolation — это самое частое исключение в Delphi-программах. Я хотел бы рассмотреть, что это такое, когда возникает, и как с ним бороться. Этот пост скорее для начинающих, поэтому данные могут излагаться с упрощением.

Примечания:

  • если вы совсем начинающий или студент/студентка и получили Access Violation — первым делом включите опцию Range Check Errors (Project/Options, вкладка Compiler) и сделайте Project/Build.
  • если вы плохо или совсем не понимаете, что такое указатели и/или объекты — рекомендую сначала прочитать эту статью.
  • если вы плохо или совсем не умеете работать с отладчиком IDE (или даже не знаете, что это такое) — прочитайте сначала эту статью.

Каждая программа использует при работе память (*). Память занимает любая переменная в вашей программе. Будь это форма, компонент, массив, запись, строка или же простой Integer. Под некоторые переменные память выделяется автоматически (например, под переменные типа Integer и статические массивы), под другие — вы должны выделять её сами, явно (например, динамические массивы). Собственно, с точки зрения операционной системы каждая переменная характеризуется адресом в памяти (местоположением) и размером. Понятно, что обычно данные разных переменных не пересекаются — за исключением случаев обращением к одной области памяти через разные имена с помощью указателей.

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

Память для глобальных переменных выделяется загрузчиком ОС при загрузке исполняемого модуля программы в память и освобождается при выгрузке модуля (выходе из программы). Глобальные переменные — это любые переменные, объявление которых располагается вне класса или процедуры. Стек используется для размещения локальных переменных (объявленных в процедуре/функции) и служебных данных (типа адресов возврата и адресов обработчиков исключений). Куча же используется для размещения динамических данных.

Подробнее.

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

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

Иногда из-за ошибок в коде программы происходит ситуация, когда программа при выполнении пытается получить доступ к памяти, которая не была выделена или уже была освобождена. Когда такое происходит, процессор возбуждает исключение класса EAccessViolation. Обычный текст ошибки в приложении Delphi — «Access violation at address XXX in module ‘YYY’. Write/read of address ZZZ» («Нарушение доступа по адресу XXX в модуле ‘YYY’. Попытка записи/чтения в ZZZ»). Хотя причина этого исключения всего одна (попытка обращения к недействительной памяти), но эта ошибка может проявлять себя в весьма разном виде и коде.

Более подробно об указателях и памяти говорится в уже упоминавшейся выше статье.

Ищем место возникновения Access Violation

Как, собственно, бороться с этими ошибками? Ну, если вы получили EAccessViolation под отладчиком:


То нужно просто нажать на «Break» («Ok» в старых версиях Delphi) и отладчик сразу же ткнёт вас на строчку с ошибкой. Также можно посмотреть стек вызовов (в меню Delphi — View/Debug windows/Call Stack):


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

Иными словами, отладчик сразу же тыркает вас в строку с ошибкой.

Если же вы используете средства автоматической диагностики типа EurekaLog/madExcept, то вместо обычного сообщения об ошибке вы получите баг-отчёт, в котором будет виден тот же самый Call Stack (вид стека вызова может отличаться из-за различных методов его получения):


Не имеет значения, столкнулись ли вы с проблемой во время отладки или получили баг-отчёт от EurekaLog для уже распространяемой программы — хорошо бы подготовиться к этой ситуации заранее и включить опции проекта, упрощающие отладку. Как правило, это опции «Use Debug DCUs» и «Stack frames».

Окей, найти место ошибки — это только пол-дела. Определить почему же в этой строке возникла ошибка — это вторые пол-дела.

Ищем причину возникновения Access Violation анализом кода

Если ситуация возникла у вас в отладчике, то тут всё относительно просто: вам нужно установить точку останова на проблемную строчку и проверить значения всех переменных и выражений, участвующих в ней — вот вам и причина ошибки, находится сразу же. Я не буду подробно останавливаться на теме отладки здесь, более подробно об этом написано в моей статье, часть 2 (осторожно: большой размер).

В случае, если у вас на руках есть только баг-репорт, а не ситуация под отладчиком, то вам придётся использовать свои телепатические способности, которые обычно развиваются с опытом. Дабы помочь вам в этом, здесь я как-раз и хочу рассмотреть типичные причины возникновения ошибки Access Violation.

1. Во-первых, это всевозможные ошибки выхода за границы массивов. Например, типичная ошибка новичка может выглядеть так:

var
  X: Integer;
...
  for X := 1 to Length(List) do // ошибка! Должно быть: for X := 0 to Length(List) - 1 do
  begin
    // ... делаем что-то с List[X]
  end;

Если в вашей проблемной строке есть скобочки типа [], то у вас есть хороший довод к проверке допустимости выражения в [].

Обычно такие ошибки нужно отлавливать на стадии отладки, включая опцию Range Check Errors. Дело в том, что подобные ошибки весьма опасны тем, что могут пройти незамеченными (и потом редко ловятся при эксплуатации программы), даже более того — они могут разрушить стек, так что нельзя будет получить место возникновения ошибки. Но об этом позже.

2. Различного рода неверные передачи параметров. Обычно эти ошибки отлавливаются во время разработки и тестирования, нежели во время эксплуатации программы. Чаще всего они возникают при использовании процедур с нетипизированными параметрами. Сюда же относятся различные варианты ошибок переполнения буфера, например:

var
  S1: array of Integer;
  S2: String;
...
  // Неверно:
  Stream.ReadBuffer(S1, 256);     // портит указатель S1
  // Правильно:
  Stream.ReadBuffer(S1[0], 256);  // читает данные из потока в массив

  // Неверно:
  FillChar(S2, Length(S2), 0);            // портит указатель S2
  // Правильно:
  FillChar(Pointer(S2)^, Length(S2), 0);  // очищает строку, забивая её данные нулями

Для избавления от таких ошибок нужно просто внимательно изучать документацию на функции: что они ожидают увидеть и что вы действительно им подсовываете.

3. Передачи данных между двумя менеджерами памяти. Обычно ошибки такого плана возникают при передаче данных из DLL в приложение или наоборот. а также между двумя DLL. Чаще всего новички любят передавать из/в DLL строки типа String.

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

4. Неверное объявление функций, импортируемых из DLL. Наиболее часто путают модель вызова. Если у вас получается EAccessViolation при вызове функции из DLL — просто внимательно посмотрите на её объявление и убедитесь, что её сигнатура верна — чаще всего пропускают модель вызова, stdcall или cdecl.

Хотя обычно ошибки такого плана отлавливаются на этапе разработки, тем не менее могут быть ситуации, когда ошибка проползает в готовую программу. Вот увлекательная история Реймонда Чена о том, как программа может работать с неверно объявленным прототипом функции (довольно интересны и посты в серии до и после этого).

5. Отсутствие синхронизации при работе с потоками. Если вы делаете программу с использованием нескольких потоков, то у вас могут быть проблемы, если вы не обеспечили необходимой синхронизации. Например, любые обращения к VCL запрещены из вторичных потоков — вам нужно использовать Synchronize. Собственно, проблемы тут возникают, когда один поток меняет данные с которыми работает второй поток — что для последнего становится полной неожиданностью.

К сожалению, ошибки с синхронизацией потоков наиболее тяжело диагностировать. Лучшее, что вы можете сделать — прогарантировать, что такие проблемы никогда не возникнут: используйте Synchronize и/или заключайте код в критические секции при работе с разделяемыми потоками переменными. Иногда проблемы возникают из-за использования CreateThread вместо BeginThread или TThread (из-за отсутствия установки IsMultiThreaded).

6. Вызовы функций или процедур по процедурной переменной, когда она содержит неверное значение. Например:

var
  Lib1, Lib2: HMODULE;
  Proc: procedure;
...
  Lib1 := LoadLibrary('MyDll.dll');         // один код загрузил библиотеку. Быть может - другой поток
  ...
  Lib2 := GetModuleHandle('MyDll.dll');    
  Proc := GetProcAddress(Lib2, 'MyProc');   // нет проверки на ошибку. Функции может не быть - тогда Proc будет равна nil
  Proc;                                     // Proc может быть равна nil - будет Access Violation
  ...
  FreeLibrary(Lib1);                        // ещё какой-то код выгрузил библиотеку
  ...
  Proc;                                     // хотя Proc <> nil, код, на который она указывает,
                                            // больше не загружен - здесь будет AV.

Ситуация очень сильно напоминает следующий пункт и бороться с нею нужно такими же методами.

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

Проблема в том, что при освобождении компонента, его ссылка-переменная не меняется, продолжая указывать на уже удалённую память. Кроме того, локальные переменные не инициализируются автоматически при входе в процедуру и содержат мусор. Вот пример подобного рода ошибок:

var
  Str: TStringList;
...
  Str.Add('S'); // Ошибка! Мы забыли создать объект вызовом Str := TStringList.Create;
  ...
  Str := TStringList.Create;
  Str.Add('S');
  ...
  Str.Free; // Здесь мы удалили объект, но ссылка Str по-прежнему указывает на ту же область памяти
  ...
  if Str.Count > 0 then // Ошибка! Обращение к уже удалённому объекту

Как мы уже говорили ранее, в приложениях Delphi есть служебный код, называемый «менеджером памяти», который отвечает за выделение и освобождение памяти в вашей программе и служит прослойкой между низкоуровневыми функциями операционной системы и вашим кодом. При всей своей пользе менеджер памяти, однако, добавляет в программу одну проблему: из-за него в программе находится куски памяти, которые выделены с точки зрения операционной системы, но свободны с точки зрения программы. Например, удалили вы компонент, но менеджер памяти не отдаёт память системе немедленно, придерживая её для дальнейшего использования.

Поэтому все ошибки доступа к памяти опасны в первую очередь тем, что могут пройти незамеченными. Например, мы обращаемся к уже удалённому объекту, но поскольку менеджер памяти ещё не отдал эту память системе, то обращение может пройти успешно. Чуть ранее мы говорили, что для предотвращения таких ситуаций вам нужно использовать FreeAndNil и другие механизмы. Ситуация ещё хуже с локальными массивами: дело в том, что локальные массивы размещаются в стеке, в котором обычно есть довольно большие участки размещённой памяти по краям массива. Что ещё хуже, эта память обычно реально используется программой (в отличие от памяти, которую мы освободили при удалении объекта), так что вы можете, спокойно промахнувшись, записать что-то не туда, и в итоге, ошибка всплывёт в совершенно другом месте из-за испорченных данных. Чтобы сделать ситуацию ещё хуже: в стеке хранятся и служебные данные программы, необходимые для её выполнения — это адреса возврата и обработчики исключений.

Например:

procedure TForm13.Button1Click(Sender: TObject);
var
  S: array [0..1] of Integer;
  I: Integer;
begin
  I := 2;           // предположим, что это значение как-то вычисляется и
                    // из-за ошибки в программе получает неверное значение
  S[I] := 0;        // эта строка затрёт адрес возврата из Button1Click в стеке
end;                // в этой строке произойдёт Access Violation, т.к. мы испортили адрес возврата

procedure TForm13.Button2Click(Sender: TObject);
var
  S: array [0..1] of Integer;
  I: Integer;
begin
  I := -6;          // пусть мы снова ошиблись в I
  try
    S[I]     := 1;  // вместо массива мы стираем обработчик исключений, установленный try
    S[I + 1] := 2;
    S[I + 2] := 3;
    Abort;          // полный вылет программы, т.к. менеджер исключений обнаружил испорченный стек
  except
    ShowMessage('Aborted');
  end;
end;

procedure TForm13.Button3Click(Sender: TObject);
var
  S: array [0..1] of Integer;
  I: Integer;
begin
  I := -1;          // пусть мы снова ошиблись в I
  S[I] := 1;        // хотя мы снова портим стек, но нам это сходит с рук
                    // никакого EAccessViolation не будет вовсе!
end;

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

Вот почему чрезвычайно важно использовать опцию Range Check Errors во время разработки и тестирования.
Ну, вы можете также включить её и для release-версии кода, если не уверены в качестве своей стадии тестирования.

Итак, что, собственно, нужно сделать, когда мы получили Access Violation? Ну, с помощью предыдущего пункта мы находим строку с ошибкой, а дальше пытаемся по пунктам подставить возможные причины:
— Есть в строке []? — подумаем, а не может ли у нас быть неверный индекс?
— Есть работа с объектами? Проследим, какова логика работы — не удаляется ли объект раньше времени?
— Используем DLL? А правильно ли объявлена функция? А уж не обмениваемся ли мы динамическими данными (строками, там, массивами)?
и т.д.

Существенную помощь в таком анализе нам поможет следующий пункт.

Ищем причину возникновения Access Violation анализом данных

Во-первых, мы можем попытаться вытащить информацию из самого сообщения об ошибке. Напомним его вид:

Access violation at address XXX in module ‘YYY’. Write/read of address ZZZ.

Во-первых, адрес XXX указывает на точное место в программе, где произошла ошибка. Именно по этому адресу отладчик Delphi и EurekaLog ищут строчку для показа её вам. Также модуль, которому она принадлежит, показывается в сообщении как YYY. Обычно это ваша программа, DLL или системная DLL. Однако, иногда это может быть и совершенно левое значение. Например, если в сообщении не указан модуль или значение XXX выглядит подозрительно (меньше $400000 или больше $7FFFFFFF), то у вас либо проблемы с перезаписью стека (пункт «в» в конце предыдущего раздела), либо вызов неверной функции (пункт 6 или, иногда, 4 из предыдущего раздела).

Следующий полезный кусок информации — это слово «write» или «read». Первое означает, что возникла проблема при записи информации, второе — что проблема была при чтении. Соответственно, вам нужно проверять в строке кода либо операции записи, либо операции чтения. Например, если проблемная строка была «P := W;«, то вам нужно обратить внимание на P, если в сообщении стоит «write». Если же там стоит «read», то нужно проверять, что же у нас с W.

И последний кусок информации, который можно извлечь из сообщения — это ZZZ. Собственно, точное значение нас обычно не волнует. Важен только факт — велико оно или мало. Мало — это что-то типа $00000000, $0000000A, $00000010 и т.п. Большие значения — это, например, $00563F6A, $705D7800 и др. Если ZZZ мало, то у вас идёт обращение по ссылке равной nil. Если оно велико, то у вас идёт обращение по ненулевой, но мусорной ссылке. В первом случае вам нужно искать, зачем же вы полезли по ссылке равной nil (или кто же освободил переменную раньше времени), во втором случае вам нужно понять, кто же это такой освободил объект, а ссылку не занулил. Короче говоря, это значение (так же, как и с «write»/»read») помогает сузить область поиска.

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



На первой вкладке вы можете видеть ассемблерный листинг своей программы. Приводится он здесь только для удобства — чтобы не надо было лезть ещё куда-то, чтобы подсмотреть его. Никакой информации он не несёт. А вот на второй вкладке вы можете видеть состояние регистров, (части) стека и (части) памяти в момент исключения. В данном случае мы смотрим на ассемблерный листинг и видим, что в проблемной команде участвуют регистры eax и edx. По вкладке CPU мы находим, что eax равен 0, что означает, что мы пытаемся присвоить значение по указателю, равному nil. Взглянув на строчку исходника, которую мы узнали из стека вызовов, мы узнаем имя переменной. Вот вам и причина: переменная оказалась равна nil.

Конечно, эта работа с такой информацией требует минимального знания ассемблера, но зато и является довольно мощным инструментом.

В следующий раз мы поговорим о ситуациях, когда у вас в коде есть ошибка, но никакого исключения не возбуждается. Частично мы уже говорили об этом здесь (например, пункт «1» и пункты «а»-«б» в конце второго раздела). Но в следующий раз мы пойдём чуть дальше и посмотрим, что ещё можно сделать для отлова таких ситуаций. И, в любом случае, у вас всегда есть возможность переписать код ;)

Читать дальше.

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

Примечания:
(*) Очень подробно о памяти для приложений рассказывает Марк Руссинович.
(**) Вот ещё один пример, как один и тот же код может демонстировать широкий диапазон поведений.

i’m getting an I/o 998 error, my task is to rewrite numbers from file to array, and find max and min values. What i’m doing wrong ?

implementation

var
  f2: file of Real;
  m: array of Real;

procedure TForm1.Button1Click(Sender: TObject);
var 
  f: Real;
  max, min: Real;
  i, j: Integer;
begin
  AssignFile(F2, 'test3.dat');
  Rewrite(f2);

  for i := 1 to 50 do
  begin
    f := RandomRange(-100, 100);
    Randomize;
    Write(f2, f);
  end;

  CloseFile(f2);

  i := 0;

  Reset(f2);

  while not Eof(f2) do
  begin
    SetLength(m, i);
    Read(f2, m[i]);
    Inc(i);
  end;

  CloseFile(f2);

  max := m[1];
  min := m[1];

  for j := 1 to i do
    if m[j] > max then
      max := m[j]
    else 
    if m[j] < min then
      min := m[i];

TLama's user avatar

TLama

74.9k17 gold badges213 silver badges387 bronze badges

asked Dec 18, 2013 at 17:42

user3116222's user avatar

5

Many errors, see comments in code.

  • Randomize should be called once at program start.
  • Dynamic arrays has start index 0.
  • CloseFile releases file handle
  • Define length of the dynamic array before the loop, otherwise you will get i/O error.
  • High(m) will get the max index of the dynamic array.
  • Index variable for assigning the min value is j.

implementation

var
  f2: file of Real;
  m: array of Real;

procedure TForm1.Button1Click(Sender: TObject);
var 
  f: Real;
  max, min: Real;
  i, j: Integer;
begin
  AssignFile(F2, 'test3.dat');
  Rewrite(f2);

  for i := 1 to 50 do
  begin
    f := RandomRange(-100, 100);
    //Randomize;  <-- Call this once at program start
    Write(f2, f);
  end;

  //CloseFile(f2); <-- Don't close yet.

  Reset(f2);
  SetLength(m, 50);  // <-- Define length of dynamic array
  i := 0;
  while not Eof(f2) do
  begin
    // SetLength(m, i); // <-- Moved to before while loop, or use SetLength(m,i+1);
    Read(f2, m[i]);
    Inc(i);
  end;

  CloseFile(f2);

  max := m[0];  // <-- Dynamic arrays start with index 0
  min := m[0];  // <-- Dynamic arrays start with index 0

  for j := 1 to High(m) do // <- Max index
    if m[j] > max then
      max := m[j]
    else 
    if m[j] < min then
      min := m[j]; // <-- j is correct index variable

answered Dec 18, 2013 at 18:01

LU RD's user avatar

LU RDLU RD

34.4k5 gold badges88 silver badges295 bronze badges

9

  i := 0;

  Reset(f2);

  while not Eof(f2) do
  begin
    SetLength(m, i);
    Read(f2, m[i]);
    Inc(i);
  end;

The above code sets the length of a dynamic array to 0 (i) and tries to read into its non-existing element. This causes the RTL to pass an invalid buffer to ReadFile api. The OS returns ‘0’ indicating the function failed and sets the last error to ‘998’ — that’s ERROR_NOACCESS. RTL sets the in/out error code and raises it.

As for the answer, use the debugger. Break when the debugger raises an exception. On the next run, put a breakpoint on the faulting statement then trace into code (RTL in this case). Additionally, should you have ‘range checking’ on in compiler options, you’d get a range check error instead of an I/O error, in which case you would probably see the mistake quickly.

answered Dec 18, 2013 at 18:10

Sertac Akyuz's user avatar

Sertac AkyuzSertac Akyuz

54.1k4 gold badges100 silver badges168 bronze badges

5

Понравилась статья? Поделить с друзьями:
  • Как найти ошибку в css
  • Как найти ошибки в тексте ворд
  • Как найти ошибки в тексте в эксель
  • Как найти ошибки в тексте в документе
  • Как найти ошибки в тексте 2 класс