Error type access violation

В чём заключаются причины возникновения критической ошибки «Access violation», где они наиболее остро проявляются, а также какие существуют способы её исправления

Критическая ошибка, она же более известна как «синий экран смерти», всегда влечёт за собой определённые последствия, так как её возникновение свидетельствует о том, что в работе операционной системы возникли серьёзные неполадки. Каждый пользователь, являющийся оператором компьютера, так или иначе, но сталкивался с проявлением подобной проблемы и знает, что далеко не всегда её решение лежит на «поверхности». Порой для исправления возникшей ситуации приходится прибегать к крайним мерам – это фактическая переустановка операционной системы Windows. Стоит ли применять данный способ в решении рассматриваемой в настоящей статье ошибки, в чём заключаются причины её возникновения. Обо всём об этом вы сможете прочитать ниже.

Как исправить ошибку Access violation

Исправление ошибки Access violation.

Причины и решения

Такой универсальный вариант (переустановка ОС) уже давно является наиболее рекомендуемым на различных IT-форумах, а также некоторыми «специалистами» воспринимается как «панацея» от любых проблем, что, конечно же, не отвечает действительности. Да, справедливости ради стоит отметить, что полная чистая переустановка Windows практически гарантированно решит имеющиеся программные проблемы, но ни в коем разе не аппаратные. Поэтому не стоит спешить «сносить» операционную систему, для начала следует определить причинно-следственную связь и только после этого принимать подобные кардинальные решения. Итак, рассматриваемая ошибка «Access violation» сигнализирует о том, что какой-либо процесс при обращении к нужному ему типу памяти встречается с трудностями. Например, при запуске игрового программного обеспечения процесс пытается обратиться не к свойственному ему типу памяти, вызывая при этом упомянутую выше ошибку, которая сопровождается текстовым сообщением в виде «Access violation at address № in module «…dll. Write of address…».

Ошибка Access violation

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

  1. Проведите полную проверку операционной системы на наличие вирусной активности. Не прибегайте исключительно к услугам штатного антивируса, а обратите своё внимание на сторонние утилиты, к примеру, «DoctorWeb Curelt», «Malwarebytes» и «Adwcleaner». Проверьте всё тесно связанное с работой компьютера, включая внешние устройства хранения информации, которые подключались ранее к компьютеру, так как не исключено, что основная «зараза» засела именно там.
  2. Проверьте операционную систему на целостность компонентов, для этого:

Процесс сканирования и внесение правок может быть продолжительным, но прерывать функционирование утилиты не рекомендуется. Дополнительно после завершения работы «sfc/scannow» можно обратиться к помощи команды «dism.exe /online /cleanup-image / restorehealth», инициировать работу, которой можно с помощью той же командной строки. Сканирование диска командой

  1. Также будет не лишним проверить используемые жёсткие диски и/или твердотельные накопители и наличие битых секторов, ошибки. Для этого подойдёт хотя бы штатная утилита «CHKDSK». Проверка утилитой диска

Вышеизложенные рекомендации носят обобщённый характер, так как их применение обосновано не только при возникновении каких-либо проблем в работе операционной системы, но и как профилактические меры по поддержанию качества функционирования Windows. Если ошибка не нашла своего решения и продолжается «терзать» пользователя, то следует прибегнуть к более точечным проверкам, которые заключаются в следующем:

  1. «Проверка реестра»:

Также будет не лишним прибегнуть к функционалу программы «CCleaner» для проверки реестра на ошибки. Основную проблему это может не решить, но добиться улучшения общего состояния операционной системы вполне реально.

  1. «Проверка корректности используемого драйверного обеспечения». Откройте «Диспетчер устройств» и удостоверьтесь, что все компоненты полностью работоспособны и среди них нет неизвестного устройства, которое было бы помечено жёлтым треугольником с восклицательным знаком внутри. Дополнительно можно обратиться к специализированному софту и проверить актуальность всех используемых драйверов.
  2. «Проверка качества работы программного обеспечения». Если рассматриваемая проблема проявилась при открытии какой-либо программы или игры, то попробуйте полностью удалить их и установить заново, но удостоверившись в том, что возможности вашего компьютера, а также версия операционной системы соответствует техническим требованиям продукта. Если игра была получена не совсем «честным/законным» путём, то попробуйте скачать другой репак или сменить основной источник. Если вы уверены в том, что вирусного программного обеспечения нет и проблема проявляется в отдельно взятом программном обеспечении при любых манипуляциях с ним, то можно попробовать внести данное ПО в список исключения DEP, для этого:

Перезагрузите компьютер и проверьте качество работы Windows и работоспособность нужной вам программы/игры.

 Заключение

К сожалению, как видно из всего представленного выше, в рассматриваемой ошибке нет каких-то конкретных причин возникновения. «Access violation», как и её собратья, является лишь следствием каких-либо сбойных моментов в работе Windows, поэтому и все приведённые рекомендации направлены на восстановление штатной работы OS. Всё сказанное в начале про переустановку Windows таковым и остаётся и в завершении статьи.

У разных исключений в .NET есть свои особенности, и знать их бывает очень полезно. Как обмануть CLR? Как остаться в живых в рантайме, поймав StackOverflowException? Какие исключения перехватить вроде бы нельзя, но если очень хочется, то можно?

Под катом расшифровка доклада Евгения (epeshk) Пешкова с нашей конференции DotNext 2018 Piter, где он рассказал про эти и другие особенности исключений.

Привет! Меня зовут Евгений. Я работаю в компании СКБ Контур и занимаюсь разработкой системы хостинга и деплоя приложений под Windows. Суть в том, что у нас есть много продуктовых команд, которые пишут собственные сервисы и хостят их у нас. Мы предоставляем им легкое и простое решение разнообразных инфраструктурных задач. Например, проследить за потреблением системных ресурсов или докинуть реплик к сервису.

Иногда получается, что приложения, которые хостятся в нашей системе, разваливаются. Мы видели очень много способов, как приложение может упасть в рантайме. Один из таких способов — это выкинуть какой-нибудь неожиданный и фееричный exception.

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

План

  1. Поведение исключений в .NET
  2. Обработка исключений в Windows и хаки

Всё рассмотренное далее верно для Windows. Все примеры тестировались на последней версии полного фреймворка .NET 4.7.1. Также будет немного упоминаний .NET Core.

Access Violation

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

Попробуем получить это исключение, используя C#. Для этого запишем байт 42 по адресу 1000 (будем считать, что 1000 — это достаточно случайный адрес и у нашего приложения, скорее всего, доступа к нему нет).

try {
       Marshal.WriteByte((IntPtr) 1000, 42);
}
catch (AccessViolationException) {
       ...
}

WriteByte делает как раз то, что нам нужно: записывает байт по заданному адресу. Мы ожидаем, что этот вызов выбросит AccessViolationException. Этот код действительно выбросит это исключение, его удастся обработать и приложение продолжит работать. Теперь немного изменим код:

try {
       var bytes = new byte[] {42};
       Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length);
}
catch (AccessViolationException) {
       ...
}

Если вместо WriteByte использовать метод Copy и скопировать байт 42 по адресу 1000, то, используя try-catch, AccessViolation поймать не получится. При этом на консоль будет выведено сообщение о том, что приложение завершено из-за необработанного AccessViolationException.

Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length);
Marshal.WriteByte((IntPtr) 1000, 42);

Получается, что у нас есть две строчки кода, при этом первая крашит все приложение с AccessViolation, а вторая выбрасывает обрабатываемое исключение того же типа. Чтобы понять, почему так происходит, мы посмотрим на то, как устроены эти методы изнутри.

Начнем с метода Copy.

static void Copy(...) {
       Marshal.CopyToNative((object) source, startIndex, destination, length);
}
[MethodImpl(MethodImplOptions.InternalCall)]
static extern void CopyToNative(object source, int startIndex, IntPtr destination, int length);

Единственное, что делает метод Copy — вызывает метод CopyToNative, реализованный внутри .NET. Если наше приложение все-таки падает и исключение где-то происходит, то это может происходить только внутри CopyToNative. Отсюда можно сделать первое наблюдение: если .NET-код вызвал нативный код и внутри него произошел AccessViolation, то .NET-код это исключение по какой-то причине обработать не может.

Теперь поймём, почему удалось обработать AccessViolation при использовании метода WriteByte. Посмотрим на код этого метода:

unsafe static void WriteByte(IntPtr ptr, byte val) {
      try {
              *(byte*) ptr = val;
      }
      catch (NullReferenceException) {
               // this method is documented to throw AccessViolationException on any AV
throw new AccessViolationException();
      }
}

Этот метод реализован полностью в managed-коде. Здесь используется C#-pointer, чтобы писать данные по нужному адресу, а также перехватывается NullReferenceException. Если перехватили NRE — выбрасывается AccessViolationException. Так нужно из-за спецификации. При этом все исключения, выброшенные конструкцией throw —  обрабатываемые. Соответственно, если при выполнении кода внутри WriteByte произойдёт NullReferenceException — мы сможем поймать AccessViolation. Мог ли произойти NRE, в нашем случае, при обращении не к нулевому адресу, а к адресу 1000?

Перепишем код с использованием C# pointers напрямую, и увидим, что при обращении к ненулевому адресу действительно выбрасывается NullReferenceException:

*(byte*) 1000 = 42;

Чтобы понять, почему так происходит, нам нужно вспомнить как устроена память процесса. В памяти процесса все адреса – виртуальные. Это значит, что у приложения есть большое адресное пространство и лишь некоторые страницы из него отображаются в реальной физической памяти. Но есть особенность: первые 64 КБ адресов никогда не отображаются в физическую память и не отдаются приложению. Рантайм .NET об этом знает и использует это. Если AccessViolation произошел в managed-коде, то рантайм проверяет, по какому именно адресу в памяти происходило обращение, и генерирует соответствующее исключение. Для адресов от 0 до 2^16 — NullReference, для всех остальных – AccessViolation.

Давайте разберемся, почему NullReference выбрасывается не только при обращении по нулевому адресу. Представьте, что вы обращаетесь к полю объекта ссылочного типа, и ссылка на этот объект нулевая:

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

Но что же происходит, если мы обращаемся к полю объекта, а сам этот объект занимает больше, чем 64 КБ?

Можем ли мы в этом случае получить AccessViolation? Проведем эксперимент. Создадим очень большой объект и будем обращаться к его полям. Одно поле – в начале объекта, второе – в конце:

Оба метода выбросят NullReferenceException. Никакого AccessViolationException не произойдет.
Посмотрим на инструкции, которые будут сгенерированы для этих методов. Во втором случае JIT-компилятор добавил дополнительную инструкцию cmp, которая обращается к адресу самого объекта, тем самым вызывая AccessViolation с нулевым адресом, который будет преобразован рантаймом в NullReferenceException.

Стоит отметить, что для этого эксперимента недостаточно использовать в качестве большого объекта массив. Почему? Оставим этот вопрос читателю, пишите идеи в комментариях :)

Подведем краткий итог экспериментов с AccessViolation.

AccessViolationException ведёт себя по-разному в зависимости от того, где исключение произошло (в managed-коде или в нативном). Кроме того, если исключение произошло в managed-коде, то будет проверяться адрес объекта.

Возникает вопрос: можем ли мы обработать AccessViolationException, который произошел в нативном коде или в управляемом, но не преобразованный в NullReference и не выброшенный с использованием throw? Это иногда полезная возможность, особенно при работе с unsafe-кодом. Ответ на этот вопрос зависит от версии .NET.

В .NET 1.0 вообще не было никакого AccessViolationException. Все ссылки считались либо валидными, либо нулевыми. Ко времени .NET 2.0 стало понятно, что без прямой работы с памятью  – никак, и AccessViolation появился, при этом был обрабатываемым. В 4.0 и выше он по-прежнему остался обрабатываемым, но обработать его уже не так просто. Для перехвата этого исключения теперь нужно пометить метод, в котором находится блок catch атрибутом HandleProcessCorruptedStateException. Видимо, разработчики так сделали, потому что посчитали, что AccessViolationException — это не то исключение, которое надо ловить в обычном приложении.
Кроме того, для обратной совместимости есть возможность использовать настройки рантайма:

  • legacyNullReferenceExceptionPolicy возвращает поведение .NET 1.0 – все AV превращаются в NRE
  • legacyCorruptedStateExceptionsPolicy возвращает поведение .NET 2.0 – все AV перехватываемы

В .NET Core AccessViolation не обрабатывается вообще.

В нашем продакшене была вот такая ситуация:

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

while (isRunning) {
       try {
                    action();
       }
       catch (Exception e) {
                    log.Error(e);
       }
       WaitForNextExecution(... );
}

В этот хелпер мы передавали action из нашего приложения. Так получилось, что он падал с AccessViolation. В результате наше приложение постоянно логгировало AccessViolation, вместо того, чтобы упасть, т.к. код в библиотеке под 3.5 мог его поймать. Нужно обратить внимание, что перехватываемость зависит не от версии рантайма, на котором запущено приложение, а от TargetFramework, под который было собрано приложение, и его зависимости.

Подводим итог. Обработка AccessVilolation зависит от того, где он произошел — в нативном или управляемом коде — а также от TargetFramework и настроек рантайма.

Thread Abort

Иногда в коде нужно остановить выполнение одного из потоков. Для этого можно использовать метод thread.Abort();

var thread = new Thread(() => {
       try {
                   ...
       }
       catch (ThreadAbortException e) {
                   ...
                   Thread.ResetAbort();
       }
});
...
thread.Abort();

При вызове метода Abort в останавливаемом потоке выбрасывается исключение ThreadAbortException. Разберём его особенности. Например, такой код:

var thread = new Thread(() => {
       try {
                    …
       } catch (ThreadAbortException e) {
                    …
       }
});
...
thread.Abort();

Абсолютно эквивалентен такому:

var thread = new Thread(() => {
       try {
                    ...
       }
       catch (ThreadAbortException e) {
                    ...
                    throw;
       }
});
...
thread.Abort();

Если всё-таки нужно обработать ThreadAbort и выполнить еще какие-то действия в останавливаемом потоке, то можно использовать метод Thread.ResetAbort(); Он прекращает процесс остановки потока и исключение перестаёт прокидываться выше по стеку. Важно понимать, что метод thread.Abort() сам по себе ничего не гарантирует — код в останавливаемом потоке может препятствовать остановке.

Еще одна особенность thread.Abort() заключается в том, что он не сможет прервать код в том случае, если он находится в блоках catch и finally.

Внутри кода фреймворка часто можно встретить методы, у которых блок try пустой, а вся логика находится внутри finally. Это делается как раз с той целью, чтобы этот код не могла быть прерван ThreadAbortException.

Также вызов метода thread.Abort() дожидается выброса ThreadAbortException. Объединим эти два факта и получим, что метод thread.Abort() может заблокировать вызывающий поток.

var thread = new Thread(() =>
{
    try { }
        catch { } // <-- No ThreadAbortException in catch
        finally { // <-- No ThreadAbortException in finally
            Thread.Sleep(- 1);
}
});
thread.Start();
...
thread.Abort(); // Never returns

В реальности с этим можно столкнуться при использовании конструкции using. Она разворачивается в try/finally, внутри finally вызывается метод Dispose. Он может быть сколь угодно сложным, содержать вызовы обработчики событий, использовать блокировки. И если thread.Abort был вызван во время выполнения Dispose — thread.Abort() будет его ждать. Так мы получаем блокировку почти на пустом месте.

В .NET Core метод thread.Abort() выбрасывает PlatformNotSupportedException. И я считаю, что это очень хорошо, потому что мотивирует пользоваться не thread.Abort(), а неинвазивными методами остановки выполнения кода, например с помощью CancellationToken.

OUT OF MEMORY

Это исключение можно получить, если памяти на машине оказалось меньше, чем требуется. Или когда мы уперлись в ограничения 32-битного процесса. Но получить его можно, даже если на компьютере много свободной памяти, а процесс — 64-битный.

var arr4gb = new int[int.MaxValue/2];

Код выше выкинет OutOfMemory. Все дело в том, что в дотнете по умолчанию не разрешены объекты более 2 ГБ. Это можно исправить настройкой gcAllowVeryLargeObjects в App.config. В этом случае массив размером 4 ГБ создастся.

А теперь попробуем создать массив ещё больше.

var largeArr = new int[int.MaxValue];

Теперь даже gcAllowVeryLargeObjects не поможет. Все из-за того, что в .NET есть ограничение на максимальный индекс в массиве. Это ограничение меньше, чем int.MaxValue.

Max array index:

  • byte arrays – 0x7FFFFFC7
  • other arrays – 0X7FEFFFFF

В этом случае произойдёт OutOfMemoryException, хотя на самом деле мы уперлись в ограничение типа данных, а не в недостаток памяти.

Иногда OutOfMemory явно выбрасывается управляемым кодом внутри .NET фреймворка:


Это реализация метода string.Concat. Если длина строки-результата будет больше, чем int.MaxValue, то сразу выбрасывается OutOfMemoryException.

Перейдем к ситуации, когда OutOfMemory возникает по делу, когда реально заканчивается память.

LimitMemory(64.Mb());
try {
       while (true)
             list.Add(new byte[size]);
} catch (OutOfMemoryException e) {
      Console.WriteLine(e);
}

Сначала мы ограничиваем память нашего процесса в 64 мБ. Далее внутри цикла выделяем новые массивы байтов, сохраняем их в какой-то лист, чтобы GC их не собирал, и пытаемся поймать OutOfMemory.

В этом случае может произойти все что угодно:

  • Исключение обработается
  • Процесс упадёт
  • Зайдём в catch, но исключение вылетит снова
  • Зайдём в catch, но вылетит StackOverflow

В этом случае программа получится абсолютно недетерминированной. Разберем все варианты:

  1. Исключение может обработаться. Внутри .NET ничто не мешает обрабатывать OutOfMemoryException.
  2. Процесс может упасть. Не нужно забывать, что у нас managed-приложение. Это означает, что внутри него выполняется не только наш код, но и код рантайма. Например, GC. Таким образом, может случиться ситуация, когда рантайм захочет себе выделить память, но не сможет это сделать, тогда мы не сможем перехватить исключение.
  3. Зайдем в catch, но исключение вылетит снова. Внутри catch мы тоже выполняем работу, при которой нам понадобится память (печатаем исключение на консоль), а это может вызвать новое исключение.
  4. Зайдем в catch, но вылетит StackOverflow. Сам StackOverflow происходит при вызове метода WriteLine, но переполнения стека здесь нет, а происходит другая ситуация. Разберём её подробнее.

В виртуальной памяти страницы могут быть не только отображены в физическую память, но и могут быть зарезервированными (reserved). Если страница зарезервирована, то приложение отметило, что собирается её использовать. Если страница уже отображена в реальную память или своп, то она называется «закоммиченной» (committed). Стек использует такую возможность разделять память на зарезервированную и закомиченную. Выглядит это примерно так:

Получается, что мы вызываем метод WriteLine, который занимает какое-то место на стеке. Так получается, что уже вся закоммиченная память закончилась, значит операционная система в этот момент должна взять еще одну зарезервированную страницу стека и отобразить ее в реальную физическую память, которая уже заполнена массивами байтов. Это и приводит к исключению StackOverflow.

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

new Thread(() => F(), 4*1024*1024).Start();

Кроме того, можно использовать настройку рантайма disableCommitThreadStack. Её нужно отключить, чтобы стек потока коммитился заранее. Стоит отметить, что поведение по умолчанию описанное в документации и наблюдаемое в реальности — различно.

Stack Overflow

Разберёмся подробнее со StackOverflowException. Посмотрим на два примера кода. В одном из них мы запускаем бесконечную рекурсию, которая приводит к переполнению стека, во втором мы просто выбрасываем это исключение с помощью throw.

try {
         InfiniteRecursion();
}
catch (Exception) {
          ...
}

try {
         throw new StackOverflowException();
}
catch (Exception) {
          ...
}

Так как все исключения, выброшенные с помощью throw, обрабатываемы, то во втором случае мы поймаем исключение. А с первым случаем все интереснее. Обратимся к MSDN:

«You cannot catch stack overflow exceptions, because the exception-handling code may require the stack.»
MSDN

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

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

RuntimeHelpers.EnsureSufficientExecutionStack();

  • «Ensures that the remaining stack space is large enough to execute the average .NET Framework function.» — MSDN
  • InsufficientExecutionStackException
  • 512 KB – x86, AnyCPU, 2 MB – x64 (half of stack size)
  • 64/128 KB — .NET Core
  • Check only stack address space

В документации по этому методу сказано, что он проверяет, что на стеке достаточно места для выполнения средней функции .NET. Но что же такое средняя функция? На самом деле в .NET Framework этот метод проверяет, что на стеке свободна хотя бы половина от его размера. В .NET Core он проверяет чтобы было свободно 64 КБ.

Также в .NET Core появился аналог: RuntimeHelpers.TryEnsureSufficientExecutionStack() возвращающий bool, а не бросающий исключение.

В C# 7.2 появилась возможность использовать Span и stackallock вместе без использования unsafe-кода. Возможно, благодаря этому stackalloc станет использоваться в коде чаще и будет полезно иметь способ защититься от StackOverflow при его использовании, выбирая, где именно выделить память. В качестве такого способа предложены метод, проверяющий возможность аллокации на стеке и конструкция trystackalloc.

Span<byte> span;
if (CanAllocateOnStack(size))
         span = stackalloc byte[size];
else
         span = new byte[size];

Вернёмся к документации по StackOverflow на MSDN

Instead, when a stack overflow occurs in a normal application, the Common Language Runtime (CLR) terminates the process.»
MSDN

Если есть «normal» application, которые падают при StackOverflow, значит есть и не-«normal» application, которые не падают? Для того, чтобы ответить на этот вопрос придется спуститься на уровень ниже с уровня управляемого приложения на уровень CLR.

«An application that hosts the CLR can change the default behavior and specify that the CLR unload the application domain where the exception occurs, but lets the process continue.» — MSDN
StackOverflowException -> AppDomainUnloadedException

Приложение, которое хостит CLR может переопределить поведение при переполнении стека так, чтобы вместо завершения всего процесса выгружался Application Domain, в потоке котором это переполнение произошло. Таким образом, мы можем превратить StackOverflowException в AppDomainUnloadedException.

При запуске managed-приложение, автоматически запускается рантайм .NET. Но можно пойти по другому пути. Например, написать unmanaged-приложение (на С++ или другом языке), которое будет использовать специальное API для того, чтобы поднять CLR и запустить наше приложение. Приложение, которое запускает внутри себя CLR будем называть CLR-host. Написав его, мы можем сконфигурировать многие вещи в рантайме. Например, подменить менеджер памяти и менеджер потоков. Мы в продакшене используем CLR-host для того, чтобы избежать попадания страниц памяти в своп.

Следующий код конфигурирует CLR-host так, чтобы при StackOverflow выгружался AppDomain (C++):

ICLRPolicyManager *policyMgr;
pCLRControl->GetCLRManager(IID_ICLRPolicyManager, (void**) (&policyMgr));
policyMgr->SetActionOnFailure(FAIL_StackOverflow, eRudeUnloadAppDomain);

Хороший ли это способ спастись от StackOverflow? Наверно, не очень. Во-первых, нам пришлось написать код на C++, чего делать не хотелось бы. Во-вторых, мы должны поменять свой C#-код так, чтобы та функция, которая может выбросить StackOverflowException выполнялась в отдельном AppDomain’е и в отдельном потоке. Наш код сразу превратится вот в такую лапшу:

try {
      var appDomain = AppDomain.CreateDomain("...");
      appDomain.DoCallBack(() =>
      {
              var thread = new Thread(() => InfiniteRecursion());
              thread.Start();
              thread.Join();
      });
      AppDomain.Unload(appDomain);
}
catch (AppDomainUnloadedException) { }
 

Ради того, чтобы вызвать метод InfiniteRecursion, мы написали кучу строк. В-третьих, мы начали использовать AppDomain. А это почти гарантирует кучу новых проблем. В том числе, с исключениями. Рассмотри пример:

public class CustomException : Exception {}
var appDomain = AppDomain.CreateDomain( "...");
appDomain.DoCallBack(() => throw new CustomException());

System.Runtime.Serialization.SerializationException:
Type 'CustomException' is not marked as serializable.
at System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate)

Так как наше исключение не помечено как сериализуемое, то наш код упадет с исключением SerializationException. И чтобы исправить эту проблему, нам недостаточно пометить наше исключение атрибутом Serializable, еще потребуется реализовать дополнительный конструктор для сериализации.

[Serializable]
public class CustomException : Exception
{
      public CustomException(){}
      public CustomException(SerializationInfo info, StreamingContext ctx) : base(info, context){}
}

var appDomain = AppDomain.CreateDomain("...");
appDomain.DoCallBack(() => throw new CustomException());

Это все получается не очень красиво, поэтому идём дальше — на уровень операционной системы и хаков, которые не стоит использовать в продакшене.

SEH/VEH

Обратите внимание, что если между Managed и CLR летали Managed-exceptions, то между CLR и Windows летают SEH-exceptions.

SEH – Structured Exception Handling

  • Механизм обработки исключений в Windows
  • Единообразная обработка software и hardware исключений
  • C# исключения реализованы поверх SEH

SEH — это механизм обработки исключений в Windows, он позволяет одинаково единообразно обрабатывать любые исключения, которые пришли, например, с уровня процессора, или были связаны с логикой самого приложения.

Рантайм .NET знает о SEH-исключениях и умеет их конвертировать в managed-исключения:

  • EXCEPTION_STACK_OVERFLOW -> Crash
  • EXCEPTION_ACCESS_VIOLATION -> AccessViolationException
  • EXCEPTION_ACCESS_VIOLATION -> NullReferenceException
  • EXCEPTION_INT_DIVIDE_BY_ZERO -> DivideByZeroException
  • Unknown SEH exceptions -> SEHException

Взаимодействовать с SEH мы можем через WinApi.

[DllImport("kernel32.dll")]
static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments,IntPtr lpArguments);

// DivideByZeroException
RaiseException(0xc0000094, 0, 0, IntPtr.Zero);
// Stack overflow
RaiseException(0xc00000fd, 0, 0, IntPtr.Zero);

На самом деле, конструкция throw тоже работает через SEH.

throw -> RaiseException(0xe0434f4d, ...)

Здесь стоит отметить, что код у CLR-exception всегда один и тот же, поэтому какой бы тип исключения мы не выбрасывали, оно всегда будет обрабатываемым.

VEH — это векторная обработка исключений, расширение SEH, но работающее на уровне процесса, а не на уровне одного потока. Если SEH по семантике схож с try-catch, то VEH по семантике схож с обработчиком прерываний. Мы просто задаем свой обработчик и можем получать информацию обо всех исключениях, которые происходят в нашем процессе. Интересная возможность VEH — это то, что он позволяет изменить SEH-исключение до того, как оно попадет в обработчик.

Мы можем поставить между операционной системой и рантаймом собственный векторный обработчик, который будет обрабатывать SEH-исключения и при встрече с EXCEPTION_STACK_OVERFLOW изменять его так, чтобы рантайм .NET не крэшил процесс.

С VEH можно взаимодействовать через WinApi:

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr AddVectoredExceptionHandler(IntPtr FirstHandler,  VECTORED_EXCEPTION_HANDLER VectoredHandler);

delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers);
public enum VEH : long
{
        EXCEPTION_CONTINUE_SEARCH = 0,
        EXCEPTION_EXECUTE_HANDLER = 1,
        EXCEPTION_CONTINUE_EXECUTION = -1
}

delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers);

[StructLayout(LayoutKind.Sequential)]
unsafe struct EXCEPTION_POINTERS {
       public EXCEPTION_RECORD* ExceptionRecord;
       public IntPtr Context;
}

delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers);
[StructLayout(LayoutKind.Sequential)]
unsafe struct EXCEPTION_RECORD {
      public uint ExceptionCode;
      ...
}

В Context находится информация о состоянии всех регистров процессора в момент исключения. Нас же будет интересовать EXCEPTION_RECORD и поле ExceptionCode в нем. Мы можем подменить его на собственный код исключения, о котором CLR вообще ничего не знает. Векторный обработчик выглядит так:

static unsafe VEH Handler(ref EXCEPTION_POINTERS e) {
      if (e.ExceptionRecord == null)
              return VEH. EXCEPTION_CONTINUE_SEARCH;
      var record = e. ExceptionRecord;
      if (record->ExceptionCode != ExceptionStackOverflow)
              return VEH. EXCEPTION_CONTINUE_SEARCH;
      record->ExceptionCode = 0x01234567;
      return VEH. EXCEPTION_EXECUTE_HANDLER;
}

Теперь сделаем обёртку, устанавливающую векторный обработчик в виде метода HandleSO, который принимает в себя делегат, который потенциально может упасть со StackOverflowException (для наглядности в коде нет обработки ошибок функций WinApi и удаления векторного обработчика).

HandleSO(() => InfiniteRecursion()) ;

static T HandleSO<T>(Func<T> action) {
       Kernel32. AddVectoredExceptionHandler(IntPtr.Zero, Handler);
       Kernel32.SetThreadStackGuarantee(ref size);
       try {
               return action();
       }
       catch (Exception e) when ((uint) Marshal. GetExceptionCode() == 0x01234567) {}
       return default(T);
}

HandleSO(() => InfiniteRecursion());

Внутри него также используется метод SetThreadStackGuarantee. Этот метод резервирует место на стеке под обработку StackOverflow.

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

Но, что произойдет, если вызвать HandleSO дважды в одном потоке?

HandleSO(() => InfiniteRecursion());
HandleSO(() => InfiniteRecursion());

А произойдёт AccessViolationException. Вернемся к устройству стека.


Операционная система умеет детектировать переполнение стека. В самом верху стека лежит специальная страница, помеченная флагом Guard page. При первом обращении к этой странице произойдет другое исключение – STATUS_GUARD_PAGE_VIOLATION, а флаг Guard page со страницы снимается. Если просто перехватить это переполнение, то этой страницы на стеке больше не будет – при следующем переполнении операционная система не сможет этого понять и stack-pointer выйдет за границы памяти, выделенной под стек. Как итог — произойдет AccessViolationException. Значит нужно восстанавливать флаги страниц после обработки StackOverflow – cамый простой способ это сделать – использовать метод _resetstkoflw из библиотеки рантайма C (msvcrt.dll).

[DllImport("msvcrt.dll")]
static extern int _resetstkoflw();

Аналогичным способом можно перехватить AccessViolationException в .NET Core под Windows, который приводит к падению процесса. При этом понадобиться учесть порядок вызова векторных обработчиков и установить свой обработчик в начало цепочки, так как .NET Core также использует VEH при обработке AccessViolation. За порядок вызова обработчиков отвечает первый параметр функции AddVectoredExceptionHandler:

Kernel32.AddVectoredExceptionHandler(FirstHandler: (IntPtr) 1, handler); 

Изучив практические вопросы, подведем общие итоги:

  • Исключения не так просты, как кажутся;
  • Не все исключения обрабатываются одинаково;
  • Обработка исключений происходит на разных уровнях абстракции;
  • Можно вмешаться в процесс обработки исключений и заставить рантайм .NET работать не так, как было задумано изначально.

Ссылки

→ Репозиторий с примерами из доклада
→ Dotnext 2016 Moscow — Adam Sitnik — Exceptional Exceptions in .NET
→ DotNetBook: Exceptions
→ .NET Inside Out Part 8 — Handling Stack Overflow Exception in C# with VEH — другой способ перехвата StackOverflow.

22-23 ноября Евгений выступит на DotNext 2018 Moscow с докладом «Системные метрики: собираем подводные камни». А еще в Москву приедут Джеффри Рихтер, Грег Янг, Павел Йосифович и другие не менее интересные спикеры. Темы докладов можно посмотреть здесь, а купить билеты — здесь. Присоединяйтесь!

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

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

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

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

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

Содержание

  • STATUS_ACCESS_VIOLATION, проблема, препятствующая просмотру
  • Почему появляется эта проблема
    • Лишние расширения
    • Страницы, загруженные баннерами и плагинами
    • Неуказанный программный код
    • Общие ошибки в браузере
  • Действия по исправлению ошибки STATUS_ACCESS_VIOLATION
    • Обновить страницу
    • Отключить расширения или последние установленные
    • Убедитесь, что в браузере установлена ​​последняя версия.
    • Переименуйте EXE-файл
    • Убедитесь, что нет вредоносных программ
    • Восстановите настроек по умолчанию

STATUS_ACCESS_VIOLATION

STATUS_ACCESS_VIOLATION — один из тех раздражающих кодов ошибок, которые могут появиться в любой момент при просмотре в Chrome или Edge . Мы пытаемся зайти на веб-страницу, сталкиваемся с этим сообщением и видим, что не можем получить доступ к содержимому и увидеть его.

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

В частности, этот сбой вызывает блокировка на веб-странице что мы пытаемся увидеть. Мы не сможем видеть контент напрямую. Следует отметить, что в основном это происходит в Google Chrome, который на сегодняшний день является наиболее часто используемым браузером и поэтому может затронуть многих пользователей. Однако он также присутствует в других, таких как Microsoft Край.

Почему появляется эта проблема

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

Лишние расширения

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

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

Обычно это происходит, когда у нас много установленных, но даже с одним он может появиться. Если, например, мы недавно установили плагин, это может быть причиной этой проблемы, которая блокирует веб-сайты.

Страницы, загруженные баннерами и плагинами

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

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

Неуказанный программный код

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

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

Общие ошибки в браузере

С другой стороны, может случиться так, что есть общие ошибки и ошибки в браузере. Например, версия, которую мы используем, нестабильна (например, используйте Google Chrome Canary) или что программа была заражена каким-либо рекламным ПО, что может вызвать ошибки этого типа.

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

Действия по исправлению ошибки STATUS_ACCESS_VIOLATION

Мы объяснили, каковы основные причины появления этой проблемы. Теперь мы собираемся показать, какие шаги мы можем предпринять, чтобы решить эту ошибку. Мы собираемся показать от самых простых до тех, которые потребуют небольших изменений. Цель та же: избежать ошибки STATUS_ACCESS_VIOLATION.

Обновить страницу

Самая основная и простая вещь — просто обновление страница. Иногда это так же просто, как нажимать F5 и иметь проблемы с перезагрузкой веб-сайта. И да, мы можем исправить определенные ошибки и убедиться, что браузер работает нормально. Но иногда этого бывает недостаточно.

Чтобы обновить эту веб-страницу более «полностью», мы можем очистить кеш и принудительно перезагрузить его. Для этого нужно нажать комбинацию клавиш Ctrl + Shift + I, и откроются инструменты разработчика. Мы также можем перейти в меню «Дополнительные инструменты» и выбрать его там.

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

Vaciar la caché en Chrome

Отключить расширения или последние установленные

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

Что мы собираемся сделать, так это наблюдать, если мы недавно установили расширение, и оно создает конфликт. Это что-то относительно обычное и может появиться в любой момент. Но у нас даже есть возможность на время отключить все плагины и посмотреть, решит ли это проблему.

Убедитесь, что в браузере установлена ​​последняя версия.

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

Таким образом, основной шаг во избежание ошибки STATUS_ACCESS_VIOLATION — это проверить наличие в браузере Последняя версия . В Chrome, например, это очень легко увидеть. Нам нужно перейти в меню выше (три пункта), нажать «Справка» и предоставить информацию о Google Chrome. Он автоматически начнет поиск новой версии и покажет нам, какую из них мы установили.

Обновите Google Chrome

Переименуйте EXE-файл

Изменяя имя файла Chrome или Edge EXE, мы также можем решить ошибки этого типа. Это быстрый и легкий процесс. Иногда конфликты возникают из-за имени, и мы можем решить их простым способом, без необходимости устанавливать или настраивать что-либо.

Для этого нам нужно перейти в то место, где установлен Chrome. Вам просто нужно щелкнуть правой кнопкой мыши значок браузера и нажать Открыть расположение файла . Позже нам просто нужно будет изменить имя Chrome.EXE и поставить что-нибудь еще. Например просто Chro.EXE.

Убедитесь, что нет вредоносных программ

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

Чтобы проверить это, мы можем использовать хороший антивирус . Таким образом мы можем проанализировать компьютер и увидеть, есть ли на нем вредоносное ПО. Важно всегда иметь инструменты безопасности, которые нас защищают.

Восстановите настроек по умолчанию

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

Этот шаг может быть очень полезным, чтобы избежать таких проблем, как STATUS_ACCESS_VIOLATION. Для этого нам просто нужно перейти в меню, войти в «Настройки», «Дополнительные настройки», и внизу мы увидим кнопку «Восстановить настройки по умолчанию».

Восстановить заводские настройки

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

Как исправить ошибку Exception Access ViolationПри запуске игры или программы, а иногда и во время работы с ними вы можете столкнуться с ошибкой Exception Access Violation, в заголовке окна может быть информация об Unhandled Exception, Unexpected Error или Fatal Error, в тексте — коды наподобие 0xc0000005 или указание на DLL. Ошибка типична для Windows 10, предыдущих версий системы и, с большой вероятностью, останется и в Windows 11.

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

  • Антивирусное ПО и Exception Access Violation
  • DEP (Предотвращение выполнения данных)
  • Дополнительные методы исправить ошибку
  • Видео инструкция

Exception Access Violation как результат работы антивирусного ПО

Сообщение об ошибке Exception Access Violation

Среди наиболее часто встречающихся причин ошибки у русскоязычного пользователя, особенно при использовании нелицензионных игр или программ — антивирус: встроенный Windows Defender или какой-либо сторонний.

Возможные действия исправить Exception Access Violation для этого случая:

  1. Проверьте, сохраняется ли ошибка, если временно отключить ваш антивирус.
  2. Добавьте папку с программой или игрой в исключения антивируса. В случае использования Защитника Windows сделать это можно, открыв «Безопасность Windows» — «Защита от вирусов и угроз» — «Управление настройками» и добавив нужную папку в разделе «Исключения». Добавить программу в исключения антивируса

DEP

Функция предотвращения выполнения данных в памяти также может привести к рассматриваемой ошибке для некоторых программ, попробуйте её отключить. Для этого:

  1. Нажмите клавиши Win+R на клавиатуре, введите sysdm.cpl и нажмите Enter.
  2. На вкладке «Дополнительно» в разделе «Быстродействие» нажмите кнопку «Параметры». Открыть дополнительные параметры быстродействия компьютера
  3. Откройте вкладку «Предотвращение выполнения данных», выберите пункт «Включить DEP для всех программ и служб, кроме выбранных ниже» и добавьте в список исполняемый файл программы или игры, которая вызывает ошибку Exception Access Violation. Примените настройки. Отключить DEP для программы в Windows

Дополнительные способы исправить ошибку

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

  1. Для относительно старого ПО — попробовать запустить программу или игру в режиме совместимости с предыдущей версией ОС, подробнее: Режим совместимости Windows 10.
  2. Если ошибка стала появляться в программе, которая до этого работала исправно на этом же компьютере, попробуйте использовать точки восстановления системы на дату, когда проблема ещё не наблюдалась.
  3. В случае, если вы столкнулись с проблемой после переустановки Windows на компьютере или ноутбуке, вручную установите все оригинальные драйверы устройств, включая драйверы чипсета. Вручную — это не с помощью «Обновить драйвер» в диспетчере устройств, а загрузив драйверы с официального сайта производителя материнской платы или ноутбука.
  4. Попробуйте запустить программу или игру от имени администратора.
  5. Проверьте оперативную память на ошибки, этот фактор также может оказаться причиной ошибке Exception Access Violation.
  6. Иногда ошибка возникает после ручного добавления библиотек DLL в систему в папки C:WindowsSysWOW64 и C:WindowsSystem32. Иногда это оказываются не рабочие DLL, иногда — разрядность библиотеки не соответствует расположению.
  7. Для программистов на JAVA: сообщают, что ошибка может возникнуть, когда x64 системах в PATH указание на SysWOW64 идёт перед System32.

Также, если речь идёт о загруженной из Интернета (с неофициального сайта) программе, можно попробовать удалить её, а затем скачать из другого источника.

Видео

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

When you install a software program, whether that’s a video game, word processing application, or a media player, you expect it to work when you launch it and stay working when in use. Unfortunately, if you have corrupted software or memory that is damaged, you may find that you are unable to access certain pieces of software as you normally would.

Exception Access Violation Error Code Example

When access is denied, you end up with an access violation error, which often reads “Exception_Access_Violation” with an error code or address attached. In some cases, this error may pop up with a specific module that’s causing the problem or provide the name of the software causing the issue.

What is the Access Violation at Address Error, and Why Does it Pop Up?

The access violation at address error is Windows-specific and can occur on all operating system versions, including Windows 10. If you have been getting this error, it means that the software you are attempting to launch or run is trying to access a protected memory address. When this occurs, the attempt to access the protected memory is denied, as the program you are trying to run should not have access to this particular address except for the program that is currently using it.

How to Read the Numeric Error Code?

In most cases, the error will be accompanied by a numeric code that is between 7-10 digits in length. For example, 0xC0000005 is a broad access violation error that prevents users from playing audio or accessing the control panel. When you see a numeric error code pop up, you can debug it by reading the exception parameters (the letter and numbers). The first exception parameter (0) will tell you the type of violation, the second exception parameter (1) will tell you the address. You can use this code to identify the cause of the problem and potentially the fix for it as well.

To help you with your access violation at address error, we’ve compiled 9 easy to try solutions below.

1. Inspect Your Computer for a Malware Infection.

Windows Defender Malware Detected

If any of your software is heavily infected with malware, it can corrupt your memory systems which can cause this error to pop up. If you’ve noticed your computer acting up before getting an access violation at address error, such as unprovoked alerts or fake browser windows, then a detailed and deep scan should be done. If you’re just using the built-in antivirus software and it doesn’t find anything, you may want to try another option to see if a malware infection was missed. Getting rid of the malware will solve the issue.

2. Add Your Program to an Exception Access List.

If you have no malware on your computer, you can add the program that you want to run to a data execution prevention exception list. This will allow you to run the program despite the error code. Here is how to do this.

  1. In your computer’s search bar, type in “control panel” and open it.
  2. You’ll want the system choice here.
  3. Click into system and security.
  4. In the system menu, click on the advanced system settings.
  5. In the advanced tab, find the performance section and hit settings.
  6. Across the top, click onto the data executive prevention tab.
  7. The section option will allow you to turn on data execution prevention for all programs and services except those that you choose.
  8. Click the add button and find your program’s .exe file.
  9. Add it to the list, hit apply, and then okay to save the changes.

Add program to data execution prevention list.

Run the program again and check to see if you still get the access violation at address error.

3. Consider Disabling User Account Control.

If you are trying to install or run a Java-based program, the access violation at address is more likely to occur. To fix this issue, you can temporarily disable the user account control to fix the error from coming up. This does have a slight security risk to it, so if you are uncomfortable with this method, disable user account control when you run your desired application and then turn it back on.

  1. In your computer’s search bar, type in “control panel” and open it.
  2. Find the “user accounts” and click into it – twice.
  3. Choose the last option, which is to change the settings of the user account controls.
  4. This window allows you to set when you get notified about changes to your computer. Move the slider down to “never notify” and hit okay.
  5. Try running your program now.

If your program runs, just remember to fix your user account settings once you are done with it for the day.

4. Double Check Your Software Isn’t in Read-Only Mode.

If your program is in a read-only mode, access is going to be denied and it won’t be able to launch or run effectively. Here is how to check this and fix it.

  1. Find the shortcut on your desktop and right-click on the file.
  2. Choose the properties option from the menu.
  3. Find the “general” tab located at the top of the window.
  4. Find the attributes section and make sure read-only is not checked.
  5. If it is checked, uncheck it.
  6. Hit apply/okay to save changes.
Make Sure Software is Not in Read-Only Mode

If your program does not have a desktop shortcut, you can either make one and follow the same steps or right-click on the .exe application file and follow the above steps.

5. Check Your RAM for Corruption.

If you ever had a heavy malware infection on your computer and it managed to cause damage to portions of your random access memory (RAM), then this could be the cause behind your access violation at address error. To check your RAM for corruption, do the following.

  • In your computer’s search bar, type in “Windows memory diagnostic”.
  • Run the program.
  • Choose the first option which is to restart and check for issues.
  • Let it run and wait for the results.
Windows Memory Diagnostic

If your RAM is corrupted, you should replace it.

6. Troubleshoot Your Hardware.

Some individuals have had luck in fixing the access violation at address error by resolving issues with their hardware.

  1. In your computer’s search bar, type in settings, and open it. Or if you are on Windows 10, press down the Windows key + I at the same time.
  2. Click on the “update and security” section.
  3. In the left-hand menu, find troubleshoot and click it.
  4. Run it and follow the on-screen instructions.

Once the troubleshooting has finished, try running your program again to see if the access violation error has been resolved.

How to Troubleshoot.

7. Run Your Program in Compatibility Mode.

If you are trying to run an older program, it may not be fully compatible with a newer operating system like Windows 10. If you are getting the access violation at address error and it’s pointing directly to the software program, then it may be a compatibility issue.

  1. Find your program’s shortcut on your desktop and right-click the file.
  2. Choose the properties option from the menu.
  3. At the top of the properties menu, find the compatibility tab.
  4. Choose to “run this program in compatibility mode” and select an older version of Windows, such as Windows 8.1/7.
  5. Hit apply and okay to save the changes.
  6. Run the program and see if the issue persists.
Run a Program in Compatibility Mode.

You may need to try different compatibility modes depending on how old the program is that you are running.

8. Reinstall the Problematic Software.

It is quite possible that the program you are trying to run has either had portions of it damaged or corrupted from crashes or improper saves or a recent update to it has caused issues. This, in turn, may be the reason behind why the program is now causing access violation errors. Try reinstalling the application to see if the problem persists. To uninstall, do the following.

  1. In your computer’s search bar, type in settings or hit the Windows key + I at the same time.
  2. Choose “apps” from the menu.
  3. Find the application that will not run or launch in the list.
  4. Click uninstall.

How to Uninstall a Program via the Control Panel.

You can also uninstall by going to your computer’s control panel, choosing the programs and features option, and clicking on the uninstall button. If you prefer this method, find the program in the list and hit the uninstall button. Once fully uninstalled, you will need to re-download the program and do a fresh install.

Понравилась статья? Поделить с друзьями:
  • Error type 0x01
  • Error txbuf alloc
  • Error two or more data types in declaration of main
  • Error two declarations cause a collision in the objectfactory class
  • Error tweakdb compilation has failed