11 мая 2009 г.

Access Violation в деталях

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

Примечание для студентов/новичков, пишущих на 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 (или даже не знаете, что это такое) - прочитайте сначала эту статью.


Что такое Access Violation

Каждая программа использует при работе память (*). Память занимает любая переменная в вашей программе. Будь это форма, компонент, массив, запись, строка или же простой 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" и пункты "а"-"б" в конце второго раздела). Но в следующий раз мы пойдём чуть дальше и посмотрим, что ещё можно сделать для отлова таких ситуаций. И, в любом случае, у вас всегда есть возможность переписать код ;)

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

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

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

52 комментария :

  1. Отличная статья! только мелкие опечатки ,s поправить)
    как то: Ищем причину... п.6, коментарий к коду "нет про_ерки на ошибку..."
    и т.п.

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

    ОтветитьУдалить
  3. Спасибо за информацию. Доступно!.

    Было бы здорово, если бы ты описал ещё так же доступно про утечки памяти (memory leaks). В разрабатываемом проекте они часто приключаются, а хорошего описания (что и к чему) не нашёл (возможно плохо искал).

    Буду ждать ;)

    ОтветитьУдалить
  4. >>> Было бы здорово, если бы ты описал ещё так же доступно про утечки памяти (memory leaks)
    Я как раз готовлю такой материал. А вообще это было ещё вот здесь (во второй части).

    ОтветитьУдалить
  5. скажите, а если два потока одновременно читают тот же участок памяти, Access Violation не случитсо?

    ОтветитьУдалить
  6. Если память доступна - то никаких проблем.

    Только убедитесь, что вы не используете эти данные для синхронизации и что у вас нет чередования запись/чтение.
    Если есть сомнения - используйте критическую секцию.

    P.S. По потокам вообще в Delphi можно почитать вот это.

    ОтветитьУдалить
  7. Спасибо, почитаем

    ОтветитьУдалить
  8. вспомнилось, из фильма вроде:
    "пить, курить и всовывать он начал одноврёмённо..."
    :)

    ОтветитьУдалить
  9. Спасибо за статью! )
    Я пишу сервер на Делфи7, и тут такая есть бяка с менеджером памяти после 2-5 дней работы сервера: (Допустим речь идет о вызове функции которая должна возвращать объект класса).Менеджер иногда не обнуляет нормально указатель на объект в самом начале, и указатель имеет вид что-то типа ($0000010), и соответственно код
    If Assigned(Класс) then
    Класс.Create
    просто пропускается так как Assigned($00000010) = True;
    Ну потом отсос виалейшен фискульт привет при любом обращении к объекту....
    Или это Делфи7 такой глючный или помогите советом!
    (борюсь - везде сначала обнуляю указатели) но это не всегда уследить можно..
    Спасибо!

    ОтветитьУдалить
  10. опечатался в коде в предыдущем комменте, тут правильно
    If NOT Assigned(Класс) then
    Класс.Create

    ОтветитьУдалить
  11. Yaroslav, можно прикрутить костыль к вашему коду)))
    if (integer(Класс)<100) then
    Класс := Класс.Create;

    ОтветитьУдалить
  12. Есть еще один случай получения AV, очень неприятный.
    Признаки: программа работала нормально, развивалась, но некоторое время назад стали появляться AV, причем в неожиданных местах. Какой билд дал такой эффект установить крайне трудно. Под отладчиком AV не появляется.
    Краткое описание: программа однопоточная, dll не используется, активная работа с формами, в том числе модальными и вспомогательными плавающими, StayOnTop. Формы создаются динамически, dll и пакетов не употребляется, кое-где используется СОМ, как с ранним, так и с поздним связыванием. Везде используется FreeAndNil.
    Пользователи клянутся, что не могут найти последовательность действий, приводящую к AV.

    ОтветитьУдалить
  13. А о себе написал что техно-гик)))
    хотя и верно...
    Но наверняка ошибка вызывается сломанным блоком RAM,так что и ее нужно проверить.

    ОтветитьУдалить
  14. спасибо за статью, очень познавательно. вот только так и не могу понять, почему "идёт обращение по ссылке равной nil" в деструкторе формы...

    ОтветитьУдалить
  15. У меня access violation at ardress 00000000. read of ardress 00000000 вылетает при попытке создать новый проект. у меня стоит delphi 2010.

    ОтветитьУдалить
  16. Наверное надо отличать проблемы ваших приложений от проблем среды Delphi?

    Эта статья говорит о решении проблем в ваших программах.

    Для решения проблем с самой Delphi - идите куда-нибудь ещё. Предпочтительно - в тех-поддержку Embarcadero (я искренне не понимаю людей, спрашивающих подобные вопросы где угодно, но только не у разработчиков Delphi - кому как не им знать, что может быть не так с их программой?).

    ОтветитьУдалить
  17. А не подскажете тогда ссылочку на страницу техподдержки, где можно задать такой вопрос?

    ОтветитьУдалить
  18. Ээээ.... ну вы даёте.

    Официальный сайт, в меню выбираем Services/Customer Support - переходит на сайт суппорта - там есть большая кнопка "Create case".

    Не знаю, как там с русским - но, по-идее, в худшем случае нет никаких проблем вам или им воспользоваться авто-переводчиком.

    Альтернативно, можно попробовать писать на адрес российского представительства (нашёл в разделе контакты).

    ОтветитьУдалить
  19. Спасибо большое - очень помогло

    ОтветитьУдалить
  20. Уважаемый Александр! Вы как профессионал в этом деле наверно сможете мне помочь! Вот тут я описала вкратце мою проблему: http://www.adminplanet.ru/t4789.html#post15703
    Если вы знаете как её решить, напишите мне, пожалуйста, на moorlena@mail.com
    Возможно я тоже буду вам чем-то полезна.

    ОтветитьУдалить
  21. у меня тоже access violation at address 00000000. read of address 00000000 я почитал разделы,но толком так и не понял где искать,и ещё,дэлфи обязательна? у меня ошибка выходит когда в онлайн игру захожу.пожалуйста подскажи.

    ОтветитьУдалить
  22. Неужели сложно сообразить, что эта статья написана для программистов (разработчиков программ)? Вы программист? Вы писали эту онлайн-игру? Нет? Вот и топайте отсюда.

    ОтветитьУдалить
  23. Здравствуйте, Александр! У меня проект компилируется без ошибок, всё чётко. Экзешник работает без проблем, а когда запускаю его на другом компе, сразу вылетают 2 ошибки: Access Violation at address 005560E1 in Module MyProg.exe Read of address 0 и No MCI Device Open, хотя запускается. Но это не красиво. Подскажите, что сделать? Заранее благодарю.

    ОтветитьУдалить
  24. Есть два варианта.

    Вариант первый - гадать. Например, из сообщения "No MCI Device Open" видно, что ваша программа как-то работает с мультимедийными функциями (по идее, это вы нам должны говорить, а не мы угадывать). Ещё это сообщение говорит, что ваша программа пытается что-то сделать, не открыв (инициализировав) устройство. Откуда можно получить гипотезу: вы открываете устройство, но не проверяете возвращаемое значение. Функция же на второй машине по какой-то причине завершается с ошибкой, возвращая nil (что подтверждается "Read of address 0"). Поскольку вы не анализируете результат вызова, а вслепую продолжаете работать дальше, то вы получаете свой законный щелчок по лбу (Access Violation).

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

    Вариант второй - не гадать, а использовать инструменты.

    Инструмент номер 1 - локальный отладчик. Запустите программу, нажмите Run/Pause, а затем - Search/Go to address, введите $5560E1, нажмите OK. Отладчик переместит вас в точности то место, где возникла проблема (разумеется при условии, что у себя вы запустили в точности тот вариант программы, который запускали на другой машине). Смотрите, куда вас ткнул отладчик, анализируйте код в этой строке.

    Инструмент номер 2 - удалённый отладчик. На ту машину, где происходит AV ставите клиента отладчик (как это делать - написано в справке Delphi или интернете), подключаетесь к той машине со своей (где у вас Delphi стоит), запускаете программу, там возникает AV, а отладчик это видит и сигнализирует обычным образом - всё, останавливаетесь, исследуете, смотрите текущее место, стек вызовов, значения переменных - всё как обычно.

    Инструмент номер 3 - трейсер исключений. EurekaLog, madExcept или JclHookExcept.pas из JCL - это основные варианты. Подключаете к программе, переносите программу на ту проблемную машину, запускаете, получаете AV - трейсер исключений сгенерирует вам отчёт. Изучаете отчёт, находите проблему.

    ОтветитьУдалить
  25. Спасибо за подсказки, получилось! Отпишусь: все заработало с помощью обычного try..except :)
    Вот в конкретном месте:
    try
    CoCreateInstance(CLSID_VCmd, nil, CLSCTX_ALL, IID_IVoiceCmd, fIVoiceCmd);
    fIVoiceCmd.QueryInterface(IID_IVCmdAttributes, fIVCmdAttrs);
    OleCheck(fIVoiceCmd.Register(nil, TVCmdNotifySink.Create(Self),
    IVCmdNotifySink, VCMDRF_ALLMESSAGES, nil));
    CreateCommandMenu;
    fIVCmdAttrs.EnabledGet(Enabled);
    fIVCmdAttrs.AwakeStateGet(Awake);
    except
    ShowMessage('Cannot do it my darling!');
    end;

    ОтветитьУдалить
  26. Неправильно.

    У вас в коде отсутствует обработка ошибок. CoCreateInstance и QueryInterface - функции. Они возвращают значение, которое вы игнорируете, а должны были бы анализировать.

    Например так:

    OleCheck(CoCreateInstance(CLSID_VCmd, nil, CLSCTX_ALL, IID_IVoiceCmd, fIVoiceCmd));
    OleCheck(fIVoiceCmd.QueryInterface(IID_IVCmdAttributes, fIVCmdAttrs));
    OleCheck(fIVoiceCmd.Register(nil, TVCmdNotifySink.Create(Self),
    IVCmdNotifySink, VCMDRF_ALLMESSAGES, nil));
    ...

    При необходимости - завернуть в:

    try
    ...
    except
    on E: EOleError do
    ShowMessage('Sorry, error: ' + E.Message);
    end;

    Ну или хотя бы как-то так:

    CoCreateInstance(CLSID_VCmd, nil, CLSCTX_ALL, IID_IVoiceCmd, fIVoiceCmd);
    if not Assigned(fIVoiceCmd) then
    Exit;
    fIVoiceCmd.QueryInterface(IID_IVCmdAttributes, fIVCmdAttrs);
    if not Assigned(fIVCmdAttrs) then
    Exit;
    OleCheck(fIVoiceCmd.Register(nil, TVCmdNotifySink.Create(Self),
    IVCmdNotifySink, VCMDRF_ALLMESSAGES, nil));
    ...

    ОтветитьУдалить
  27. Уважаемый автор! Респект тебе и уважуха! Без тебя накрылась бы дипломная, а так, вычеслил ошибки и пофиксил, Спасибо!

    ОтветитьУдалить
  28. Здравствуйте! Спасибо автору статья хороша! Хотел бы заметить что у меня вылетала AV только потому что я вместо Объект:=Тип_Объекта.create; писал Объект.create; хотя казалось бы ;)

    ОтветитьУдалить
  29. Я НИФИГА не поняла...

    ОтветитьУдалить
  30. Огромное вам спасибо! С помощью самого начала вашей статьи за минуту нашёл ошибку, которую не мог найти 3 часа (не приходилось раньше пользоваться отладчиком)!
    В очередной раз убедился - всегда виноват сам.

    ОтветитьУдалить
  31. что делать если access violation at address in module rtl60.bpl вылезает при закрытии программы и даже при просто закрытии делфи.

    ОтветитьУдалить
  32. Добрый день! У меня возникла проблема с WebServices
    Итак...сделан клиент и сервер...с помощью C++ Builder на Windows 7
    Клиент работает отлично на Windows 7, XP, Windows Server 2003, а вот на Windows Server 2008 R2 пишет ошибку access violation at address....(при этом на сервере в логах вообще ничего нет...то есть он просто не может дать запрос на сервер)
    Помогите советом как решить эту проблему...пожалуйста

    ОтветитьУдалить
  33. Какое слово непонятно в статье?

    ОтветитьУдалить
  34. в моем случае , без открывания или закрывания программ , само произвольно вылезает примерно 70 окон с ошибкой :
    access violation at address 037b84db Read of address 00000180
    Что делать в моем случае ?

    ОтветитьУдалить
  35. у меня такая проблема, написал програмку у меня все работает и у многих других тоже, а у некоторых вылазит AV, с чем это может быть связано?

    ОтветитьУдалить
  36. Это риторический вопрос? Любой Access Violation обусловлен багом в коде. В 99% случае - в вашем коде.

    Или вы не понимаете, как такое может быть что иногда работает, а иногда - нет? Ну вот есть пример в п. 7 к разделу "Ищем причину возникновения Access Violation анализом кода".

    Или вы не знаете, как искать причину? Ну вот же целая статья. А для сторонней машины даже и комментарий.

    ОтветитьУдалить
  37. Здравствуйте, хорошая статья. Вот только возник вопрос. Я не пишу сам код, я лишь эксплуатирую поставленное для нашей компании ПО, т.е. доступа к кодам у меня нет.
    ПО работало на протяжении 5-6ти лет без проблем, пока не вышла из строя материнская плата ПК. Заменили плату на аналогичную, все драйвера подхватились, кроме звука (установили вручную отдельно), ну и установили новый антивирус macafe. В итоге при запуске ПО (программа взвешивания ж/д транспорта) появляется ошибка "Acsess violation at adress 004A3C58 in module "WinVesy.exe" read of adress 00000034" можно ли устранить проблему не без исходников ПО?? Теоретически может ли ПО конфликтовать с антивирусом? ПО ошибку выдаёт не всегда, например если перезагрузить копм и сразу запустить ПО оно запускается.

    ОтветитьУдалить
  38. > можно ли устранить проблему без исходников ПО?

    Нельзя.

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

    А если смотреть на это шире: где гарантии, что программа работает правильно? Если в ней есть одна ошибка (обращение к неинициализированной памяти), то почему бы не быть и другой?

    ОтветитьУдалить
  39. Александр, можете посоветовать способ (в академических целях) гарантированно получить AV без обращения по nil?
    Я полагаю, это вот например не гарантия вылета?

    procedure P(Sender: TObject);
    var
    S: array [0..0] of Integer;
    I: Integer;
    begin
    I := 1;
    S[I] := 0;
    end;

    или

    procedure P2;
    var
    S: array [0..0] of Integer;
    I: Integer;
    begin
    I := -6;
    try
    S[I] := 1;
    Abort;
    except
    end;
    end;

    ОтветитьУдалить
    Ответы
    1. Вопрос не понятен, поскольку nil ничем не отличается от любого другого адреса.

      Например: P := Pointer(1); I := PInteger(P)^; - даст A/V, поскольку $00000001 < 4 Кб, и, следовательно, принадлежит защитной странице памяти, в которую входит и nil.

      Другой пример - P := VirtualAlloc(..., MEM_RESERVE); I := PInteger(P)^; - даст A/V, т.к. просходит обращение к зарезервированному, но не выделенному участку памяти.

      Третий пример: P := VirtualAlloc(...); VirtiulFree(P); PInteger(P)^; - даст A/V, т.к. просходит обращение к не выделенной памяти. Правда, при условии, что в программе - 1 поток (что вы, в общем случае, гарантировать не можете).

      Удалить
    2. Я имел в виду следующее: nil гарантировано указывает "в никуда" и обращение недопустимо. В остальных случаях при обращении по адресу может как возникнуть AV (если скажем память защищена или не выделена) так и нет (нам не повезло и мусорный указатель чисто случайно указал на доступный нам кусок памяти).

      Ещё раз повторюсь, вопрос чисто был академический. Вопрос родился из желания привести студентам пример кода иллюстрирующий полезность FreeAndNil. В принципе, Я ответ получил. Спасибо.

      Удалить
    3. Да, гарантировано вы получите A/V только при попадании в первые 64 Кб (зона отлова нулевых указателей) и в 64 Кб на границе 2 Гб. В остальном - как повезёт.

      Удалить
  40. Александр, здравствуйте.
    Пишу программу на Delphi 7. Испрользую BDE, EhLib 6.2.
    В программе на главной форме есть TPageControl в нем два TabSheet'a, на первом находится DBgridEh1 в котором отражаются отфильтрованные записи из BDEMainTable, на втором TabSheet'e находится DBGridEh2 через который я вношу изменения в BDEMainTable, если я вношу изменения через DBGridEh2, перехожу на TabSheet1 потом снова возвращаюсь на TabSheet2, снова вношу изменения, снова перехожу на TabSheet1, перехожу на TabSheet2 и при нажатии на поле для изменения значения вылетает Access Violation at address 00456D60 in module 'My.exe'.Read of address 00000031. Помогите, пожалуйста разобраться, в чем может быть проблема?

    ОтветитьУдалить
    Ответы
    1. Что вы ожидаете услышать в ответ? "У вас ошибка в строке 152"?

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

      А делать вы это должны - исследуя ситуацию с помощью инструментов. Как? А вот как написано в этой статье.

      Например, читаем раздел "Ищем причину возникновения Access Violation анализом данных", смотрим на "Read of address 00000031". Уже отсюда я могу заключить, что у вас произошла попытка прочитать поле объекта, ссылка на который = nil (т.е. объект уже удалён или не создан). Причём, объект относительно сложный, т.к. смещение 31 - достаточно велико (у простых/небольших объектов это будет 4, 8, 12...).

      Далее, "at address 00456D60" сообщает вам точное место, где произошла ошибка. Я не телепат, я не знаю, что у вас за код находится по этому адресу. А вот вы можете как использовать Search / Go To Address, так и просто отладчик - при исключении он остановит вас именно на этом месте. В любом случае вы посмотрите на строчку кода, проверите значения переменных в ней и увидите, кто из переменных, к которым обращается строчка кода, равна nil. Более того, отладчик покажет вам и стек вызовов - так что вам станет понятно, как вы попали в это место. Всё это описано в статье в разделах "Ищем место возникновения Access Violation" и "Ищем причину возникновения Access Violation анализом кода".

      Теперь у меня возникает вопрос: какое слово в статье не понятно? Что вы пытались сделать самостоятельно?

      Удалить
  41. Ерунда у меня полная и за 22 года впервые. Ибо программа самописная не работает всего лишь на одном компе. Intel Core Duo E8200 (причем работает на Core Duo E7500).
    Все процедуры по включению Range Check Errors делал. (хотя не уверен насколько глубоко зашел Build, ибо может есть модули откомпилированные без него и хранятся в пакетах).
    Т.к. программа х64, то DEP отключал путем "bcdedit.exe /set {current} nx AlwaysOff".
    БИОС облазил. Ничего не нашел да и что искать - ума не приложу. (комп 2010 года. единственное отличие от остальных - зеркальный RAID).
    Причем не работает как-то извращенно. Не открываются отдельные окна. Я полагаю, что есть компоненты, которые не дружат с этим компом. Но как найти подвох. И главное что исправлять, если в остальных случаях работает.
    Могу добавить, что та же программа, но откомпилированная под х86 платформу работает.

    ОтветитьУдалить
    Ответы
    1. А это какое отношение имеет к исключению класса EAccessViolation?

      Удалить
  42. Отношение было...))) Детская ошибка. забыл один каталог скопировать. бывает....

    ОтветитьУдалить
  43. Здравствуйте! Не могу понять, что делаю не так...

    type
    TPathButtons = array of TSpeedButton;
    var
    PathButtons: TPathButtons;
    ...
    try
    PathButtons[i]:= TSpeedButton.Create(pnl_Buttons); // на этой строке получаю AccessViolation
    PathButtons[i].Parent := pnl_PathButtons;
    PathButtons[i].Align := alLeft
    PathButtons[i].Width := 100;
    PathButtons[i].Caption := 'SpeedButton';
    except
    ShowMessage('не работает');
    end;

    ОтветитьУдалить
    Ответы
    1. Забыл указать: pnl_PathButtons и pnl_Buttons это одна и та же TPanel, я случайно удалил кусок строки, когда создавал пост и только потом увидел :)

      Удалить
  44. У меня эта хня происходит но без кнопки Break. Borland C++ Builder

    ОтветитьУдалить
  45. АV при закрытии приложения. Появляется при добавлении на форму нового грида. Добавил прога запустилась работает без проблем, но при закрытии - Аv

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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

Примечание. Отправлять комментарии могут только участники этого блога.