Error handling is a mechanism used to resolve/handle errors that arise during the execution of a program. Error handling deals with these events to avoid the program or system crashing; exceptions would disrupt the normal flow of an application without this process. Even if an application is completely standalone, there is the potential for a fault with the computer’s storage or RAM that could affect execution. Therefore, during the execution of a program, interferences from errors must be considered while developing them.
Improper Error Handling:
Improper error handling occurs when errors are improperly handled, resulting in errors being displayed to the end user, which produces undesirable responses to the user and opens the room for exploits being made to capitalize on these vulnerabilities. This is why extensive efforts are made during the development cycle of an application to ensure that no information about the processing within the application is leaked throughout its execution. But the presence of such errors puts all those efforts in vain. Such errors could arise due to null pointer exceptions, system call failure, Out of memory, network timeout, database unavailability, and many other execution interruptions.
Improper Error Handling Working:
Improper handling can completely malfunction the workings of a program or a website. In the case of websites, improper error handling could leak information regarding the type of error produced, which provides insight to the hacker on the types of attacks to use. In the case of programs, it could either result in crashes or unexpected results. The problem with the presence of such errors is that they can be reproduced in the future if the same conditions are met. This provides time for the hacker to make exploit it.
Error Handling Attack & Defense Examples:
Here’s an example of an OWASP HTTP 404 Not Found error that reveals sensitive information.
Not Found The requested URL /page.html was not found on this server. Apache/2.2.3 (Unix) mod_ssl/2.2.3 OpenSSL/0.9.7g DAV/2 PHP/5.1.2 Server at localhost Port 80
This error message is generated when a user requests a URL that doesn’t exist. This code not only informs the user that an error occurred and the file cannot be found, but also provides valuable information about the web server version, operating system, modules, and code used. Attackers can use this information to design attacks.
How To Handle Errors Properly?
Improper error handling can easily be prevented by making use of the following pointers:
- Policies on logging errors must be devised.
- Abstracting the workings behind interfaces.
- Using generalized exception handling as opposed to specific exception handling.
- Testing the programs on a wide range of test cases
- Producing a failsafe mechanism to encounter problems occurred due to unexpected errors.
Last Updated :
22 Oct, 2022
Like Article
Save Article
Недочеты или
некорректное выполнение какой-либо
операции выявляются в процессе
тестирования программы. Чаще всего они
выявляются в процессе реализации
программного продукта. Сам процесс
поиска и устранение ошибок называется
отладкой.
Интегрированная
среда разработки Builder 6.0 предоставляет
программисту мощное средство поиска и
устранения ошибок в программе — отладчик.
Отладчик позволяет выполнять трассировку
программы, наблюдать значения переменных,
контролировать выводимые программой
данные.
Виды тестирования:
Модульное –
процесс проверки отдельных программных
процедур и подпрограмм, входящих состав
программного продукта.
Его элементы:
—
проверка соответствия стандарта
копированием- проверка кода на соответствие
стандартам кодирования компании;
—
технический обзор программного кода.
Интеграционное
тестирование проводится для совместной
работы отдельных модулей и предшествует
тестированию всей системы, как единого
целого.
Его элементы:
—
проверка функциональности-
проверка
соответствия отдельных функций,
выполняемых совокупностями модулей,
функциям, заданным в спецификациях
требований;
—
проверка промежуточных результатов-
проверка всех промежуточных результатов
и файлов на наличие и корректность.
Системное
тестирование предназначено для проверки
программной системы в целом, ее организации
и функционирования.
Его элементами
является:
—
граничное тестирование-
тестирование
в граничных условиях;
—
прогоночное тестирование- тестирование
всех функциональных характеристик
реальной работы системы;
—
целевое тестирование-
тестирование
на целевой платформе;
— проверка
документации- проверка пользовательской
документации на корректность.
Выходное
тестирование — завершающий этап
тестирования, на котором проверяется
готовность программного продукта.
Приемочное
тестирование проводится организацией,
отвечающей за сопровождения программного
продукта и обучения конечного пользователя.
Программная ошибка
– ситуация, когда программа не дает
того, что пользователь от нее ожидает.
Функциональные
недостатки присущи программе, если она
выполняет
одну из своих
функций неверно или не полностью.
Некорректная
обработка ошибок – правильное определение
ошибок, программа должна выдать о ней
сообщения. Отсутствие такого сообщения
является ошибкой в работе программы.
Некорректная
обработка граничных условий. Внутри
границы диапазона программа работает,
а на их границах могут происходить
действия, которые в свою очередь приводят
к ошибкам в работе программного продукта.
Ошибки вычисления.
К ним относятся ошибки, вызванные
неправильным выбором алгоритма
вычислений, неправильными формулами.
Самые частые ошибки
– это ошибки управления потоком. По
логике за первым действием идет второе.
Если после первого идет третье, то это
ошибка.
Недостатки
пользовательского интерфейса. Во время
проверки работоспособности программы,
необходимо оценить правильность работы
программы. После подтверждения
спецификации требований, любое отклонения
от них или невыполнения является ошибкой.
4 Компьютерная
и информационная безопасность
4.1 Эргономика
Эргоно́мика в
традиционном понимании наука о
приспособлении должностных обязанностей,
рабочих мест, предметов и объектов
труда, а также компьютерных программ
для наиболее безопасного и эффективного
труда работника, исходя из физических
и психических особенностей человеческого
организма.
Освещение при
работе с компьютером должно быть не
слишком ярким, но и не отсутствовать
совсем, идеальный вариант — приглушенный
рассеянный свет.
Экран монитора
должен быть абсолютно чистым; если вы
работаете в очках, они тоже должны быть
абсолютно чистыми. Протирайте экран
монитора (лучше специальными салфетками
и/или жидкостью для протирки мониторов)
минимум раз в неделю, следите за
кристальной прозрачностью очков каждый
день.
Располагайте
монитор и клавиатуру на рабочем столе
прямо, ни в коем случае не наискосок.
Центр экрана должен
быть примерно на уровне ваших глаз или
чуть ниже. Держите голову прямо, без
наклона вперед. Периодически на несколько
секунд закрывайте веки, дайте мышцам
глаз отдохнуть и расслабиться.
Иногда встречаются
рекомендации использовать специальные
очки, фильтры. Они действительно способны
поднять какой-то из показателей
видеосистемы, но только в ущерб другому
показателю.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
Какие существуют методы анализа и локализации ошибки
Под тестированием следует понимать процесс исполнения программы с целью обнаружения ошибок, в качестве которых принимается любое отклонение от эталонов. Хорошим считается тест, который имеет высокую вероятность обнаружения еще не выявленных ошибок.
Под отладкой понимается процесс, позволяющий получить программу, функционирующую с требуемыми характеристиками в заданной области входных данных. Таким образом, в результате отладки программа должна соответствовать некоторой фиксированной совокупности правил и показателей качества, принимаемой за эталонную для данной программы.
Существует три основных способа тестирования:
Алгоритмическое тестирование применяется для контроля этапов алгоритмизации и программирования. Проектируются тесты и начинаются готовиться эталонные результаты на этапе алгоритмизации, а используются они на этапе отладки.
Функциональное или аналитическое тестирование
Аналитическое тестирование служит для контроля выбранного метода решения задачи, правильности его работы в выбранных режимах и с установленными диапазонами данных. Тесты проектируют и начинают готовить сразу после выбора метода, а используют их на последнем этапе отладки, в ходе тестирования, наряду со сверкой на совпадение, применяются и качественные оценки результатов.
Содержательное тестирование служит для проверки правильности постановки задачи. Для контроля при этом используются, как правило, качественные оценки и статистические характеристики программы, физический смысл полученных результатов и т.п. в проведении содержательного тестирования, принципы которого формулируются в техническом задании, самое активное участие должны принимать заказчики или идущие пользователи программы.
Содержательные и аналитические тесты проверяют правильность работы программы в целом или крупных ее частей, в то время как алгоритмические тесты в первую очередь должны проверять работу отдельных блоков или операторов программы.
Тот вид контроля, который рассматривался выше, можно назвать тестированием основных функциональных возможностей программы — основной тест.
Этот тест затрагивает работу программы в самой минимальной степени. Обычно тест служит для проверки правильности выполнения самых внешних функций программы, например, обращения к ней и выхода из нее.
Тест граничных значений
Тест проверяет работу программы для граничных значений параметров, определяющих вычислительный процесс. Часто для граничных значений параметра работа программы носит особый характер, который, тем самым, требует и особого контроля.
Тест проверяет реакцию программы на возникновение разного рода аварийных ситуаций в программе, в частности, вызванных неправильными исходными данными. Другими словами, проверяется диагностика, выдаваемая программой, а также окончание ее работы или, может быть, попытка исправления неверных исходных данных.
Локализация ошибок
После того, как с помощью тестов (или каким либо другим путем) установлено, что в программе или в конкретном ее блоке имеется ошибка, возникает задача ее локализации, то есть установления точного места в программе, где находится ошибка.
Процесс локализации ошибок состоит из следующих трех компонент:
Получение на машине тестовых результатов.
Анализ тестовых результатов и сверка их с эталонными.
Выявление ошибки или формулировка предположения о характере и месте ошибки в программе.
Технология отладки автоматизированного рабочего места
При отладке программы использовались следующие методы контроля и локализации ошибок: просмотр текста программы с целью обнаружения явных синтаксических и логических ошибок и трансляция программы (транслятор выдает сообщения об обнаруженных им ошибках в тексте программы).
Тестирование проводилось посредством ввода исходных данных, с дальнейшей их обработкой, выводом результатов на экран. Результаты работы программы сравнивались с требованиями в техническом задании.
1) Отладка программы производилась следующим образом:
2) Запуск программы с набором тестовых входных данных и выявление наличия ошибок.
3) Выделение области программы, в которой может находиться ошибка.
4) Просмотр листинга программы с целью возможного визуального обнаружения ошибок. В противном случае — установка контрольной точки примерно в середине выделенной области.
Новая прогонка программы. Если работа программы прервалась до обработки контрольной точки, значит, ошибка произошла раньше. Контрольная точка переносится, и процесс отладки возвращается к шагу 2.
Если контрольная точка программы была обработана, то далее следует изучение значений стека, переменных и параметров программы с тем, чтобы убедиться в их правильности. При появлении ошибки — новый перенос контрольной точки и возврат к шагу 2.
В случае если ошибка не была обнаружена, далее выполнение программы производится покомандно, с контролем правильности выполнения переходов и содержимого регистров и памяти в контрольных точках. При локализации ошибки, она исправляется, и процесс возвращается к шагу 1.
В данном разделе были рассмотрены вопросы разработки, отладки и тестирования программных продуктов. Было приведено обоснование необходимости и важности этапа отладки в процессе разработки программного обеспечения, даны краткие описания основных способов отладки и тестирования.
В отношении разработанной в специальной части программы было дано описание алгоритма, использовавшегося при ее отладки и тестировании. Представлено обоснование выбора языка программирования.
7. Локализация ошибок
После того, как с помощью контрольных тестов (или каким либо другим путем) установлено, что в программе или в конкретном ее блоке имеется ошибка, возникает задача ее локализации, то есть установления точного места в программе, где находится ошибка.
Процесс локализации ошибок состоит из следующих трех компонент:
1. Получение на машине тестовых результатов.
2. Анализ тестовых результатов и сверка их с эталонными.
3. Выявление ошибки или формулировка предположения о характере и месте ошибки в программе.
По принципам работы средства локализации разделяются на 4 типа :
1. Аварийная печать.
2. Печать в узлах.
АВАРИЙНАЯ ПЕЧАТЬ осуществляется один раз при работе отлаживаемой программы, в момент возникновения аварийной ситуации в программе, препятствующей ее нормальному выполнению. Тем самым, конкретное место включения в работу аварийной печати определяется автоматически без использования информации от программиста, который должен только определить список выдаваемых на печать переменных.
ПЕЧАТЬ В УЗЛАХ включается в работу в выбранных программистом местах программы; после осуществления печати значений данных переменных продолжается выполнение отлаживаемой программы.
СЛЕЖЕНИЕ производится или по всей программе, или на заданном программистом участке. Причем слежение может осуществляться как за переменными (арифметическое слежение), так и за операторами (логическое слежение). Если обнаруживается, что происходит присваивание заданной переменной или выполнение оператора с заданной меткой, то производится печать имени переменной или метки и выполнение программы продолжается. Отличием от печати в узлах является то, что место печати может точно и не определяться программистом (для арифметического слежения); отличается также и содержание печати.
ПРОКРУТКА производится на заданных участках программы, и после выполнения каждого оператора заданного типа (например, присваивания или помеченного) происходит отладочная печать.
По типам печатаемых значений (числовые и текстовые или меточные) средства разделяются на арифметические и логические.
7.2. Классификация средств локализации ошибок
Ниже дана классификация средств локализации.
ТИПЫ СРЕДСТВ ЛОКАЛИЗАЦИИ ОШИБОК :
СРЕДСТВА ЛОКАЛИЗАЦИИ:
1. Аварийная печать (арифметическая).
1.1. Специальные средства языка.
1.2. Системные средства.
2. Печать в узлах (арифметическая).
2.1. Обычные средства языка.
2.2. Специальные средства языка.
3. Слежение (специальные средства).
4. Прокрутка (специальные средства).
8. Технология отладки программы автоматизации учета движения товаров на складе малого предприятия
При отладке программы использовались следующие методы контроля и локализации ошибок (обзор методов см. в теоретической части раздела) :
1. Просмотр текста программы и прокрутка с целью обнаружения явных синтаксических и логических ошибок.
2. Трансляция программы (транслятор выдает сообщения об обнаруженных им ошибках в тексте программы).
3. Тестирование. Тестирование проводилось посредством ввода исходных данных, с дальнейшей их обработкой, выводом результатов на печать и экран. Результаты работы программы сравнивались заданными в техническом задании.
4. При локализации ошибок преимущественно использовалась печать в узлах, которыми являлись в основном глобальные переменные, переменные, используемые при обмене данными основной программы с подпрограммами.
Отладка программы производилась по следующему алгоритму :
1. Прогонка программы с набором тестовых входных данных и выявление наличия ошибок.
2. Выделение области программы, в которой может находиться ошибка. Просмотр листинга программы с целью возможного визуального обнаружения ошибок. В противном случае — установка контрольной точки примерно в середине выделенной области.
3. Новая прогонка программы. Если работа программы прервалась до обработки контрольной точки, значит, ошибка произошла раньше. Контрольная точка переносится, и процесс отладки возвращается к шагу 2.
4. Если контрольная точка программы была обработана, то далее следует изучение значений регистров, переменных и параметров программы с тем, чтобы убедиться в их правильности. При появлении ошибки — новый перенос контрольной точки и возврат к шагу 2.
5. В случае не обнаружения ошибки продолжение выполнения программы покомандно, с контролем правильности выполнения переходов и содержимого регистров и памяти в контрольных точках. При локализации ошибки она исправляется и процесс возвращается к шагу 1.
В качестве тестовых входных данных использовалась последовательность частотных выборок, генерируемых имитатором в режиме 1. (Каждому интервалу соответствует фиксированное значение выборок.)
Итоговый тест по дисциплине «Поддержка и тестирование программных модулей»
Является ли программа аналогом математической формулы?
Варианты ответов
- Да
- Нет
- Математические формулы и программы не сводятся друг к другу
Вопрос 2
Какие подходы используются для обоснования истинности программ?
Варианты ответов
- использование аналогий
- эксперимент над программой
- доказательство программы
- формальный и интерпретационный
Вопрос 3
Отметьте верные утверждения
Варианты ответов
- тестирование – процесс поиска ошибок
- в фазу тестирования входят поиски и исправление ошибок
- отладка – процесс локализации и исправления ошибок
Вопрос 4
Зачем нужна спецификация тестирования?
Варианты ответов
- для формирования команды тестировщиков
- для разработки тестового набора
- для понимания смысла программы
Вопрос 5
Варианты ответов
- выполнение программы в уме
- пошаговое выполнение
- метод контрольных точек и анализа трасс
Вопрос 6
Зачем нужен Log-файл?
Варианты ответов
- для изучения результатов тестирования в режиме on-line
- для фиксации результатов прогона test-suite
- для записи комментариев после прогона тестов
Вопрос 7
Варианты ответов
- разработка тестового набора
- прогон программы на тестовом наборе
- доказательство правильности программы
- анализ результатов тестирования
Вопрос 8
Варианты ответов
- определение областей эквивалентности входных параметров
- анализ покрытия тестами всех возможных случаев поведения
- проверка граничных значений
Вопрос 9
Что такое управляющий граф программы (УГП)?
Варианты ответов
- множество операторов программы.
- граф, вершины которого кодируют операторы программы, а дуги — управления (порядок исполнения) операторов
- множество операторов управления
Вопрос 10
Варианты ответов
- множество связанных дуг УГП
- последовательность вершин и дуг УГП с фиксированными начальной и конечной вершиной
- последовательность ветвей УГП с фиксированными начальной вершиной первой ветви и конечной вершиной последней ветви пути
Вопрос 11
Варианты ответов
- нереализуемый путь недоступен при корректном исполнении программы
- нереализуемый путь недоступен всегда
- нереализуемый путь доступен при сбое
- нереализуемый путь доступен при реализации недопустимых состояний переменных программы
Вопрос 12
Возможно ли тестирование программы на всех допустимых значениях параметров?
Варианты ответов
- да, всегда
- никогда
- возможно в отдельных случаях
Вопрос 13
Какие предъявляются требования к идеальному критерию тестирования?
Варианты ответов
- достаточность
- достижимость
- полнота
- проверяемость
Вопрос 14
Какие классы критериев тестируемости известны
Варианты ответов
- структурные критерии
- мутационные критерии
- функциональные критерии
- сценарные критерии
- стохастические критерии
Вопрос 15
Варианты ответов
- сценарный критерий
- такого критерия не существует
- критерий «черного ящика»
Вопрос 16
Варианты ответов
- критерий тестирования команд
- критерий тестирования ветвей
- критерий тестирования циклов
- критерий тестирования путей
Вопрос 17
Варианты ответов
- не проверяется соответствие со спецификацией
- не проверяется соответствие со спецификацией, не зафиксированное в структуре программы
- не проверяются ошибки в структурах данных
Вопрос 18
Варианты ответов
- тестирование пунктов спецификации
- тестирование классов входных данных
- тестирование классов выходных данных
- тестирование функций
- тестирование правил
Вопрос 19
Варианты ответов
- не проверяется соответствие со спецификацией
- не проверяются ошибки, требования к которым не зафиксированы в спецификации
- не проверяются ошибки в структурах данных, требования к которым не зафиксированы в спецификации
Вопрос 20
Варианты ответов
- создание программ-мутантов на основе изменения модульной структуры основной программы
- создание программ-мутантов с функциональными дефектами
- оценка числа ошибок в программе на основе искусственно внесенных мелких ошибок
Вопрос 21
Варианты ответов
- оценка проекта интегрирует оценки оттестированности модулей
- оценка проекта может вычисляться инкрементально
- в результате получаем наихудшую оценку оттестированности
- в результате получаем наилучшую оценку оттестированности
Вопрос 22
Какие существуют разновидности уровней тестирования?
Варианты ответов
- модульное
- интеграционное
- структурное
- системное
- регрессионное
Вопрос 23
Какие задачи у модульного тестирования?
Варианты ответов
- выявление ошибок при вызове модулей
- выявление ошибок взаимодействия модуля с окружением
- выявление локальных ошибок реализации алгоритмов модулей
Вопрос 24
На основе каких принципов строятся тесты для модульного тестирования?
Варианты ответов
- анализ потоков управления модуля
- анализ потоков данных модуля
- анализ покрытия в соответствии с заданными структурными критериями
Вопрос 25
Варианты ответов
- построение УГП (управляющего графа программы)
- выбор тестовых путей
- генерация тестов, соответствующих выбранным тестовым путям
Вопрос 26
Варианты ответов
- статические
- динамические
- методы реализуемых путей
Вопрос 27
Варианты ответов
- Регрессионное тестирование
- монолитное тестирование
- нисходящее тестирование
- восходящее тестирование
Вопрос 28
Варианты ответов
- необходимость разработки заглушек
- параллельная разработка эффективных модулей
- необходимость разработки среды управления очередностью вызовов модулей
- необходимость разработки драйверов
Вопрос 29
Варианты ответов
- тесты оперируют пользовательским или другими внешними интерфейсами
- структура проекта тестируется на уровне подсистем
- тестированию подлежит система в целом
- тестирование осуществляется по методу «черного ящика»
Вопрос 30
Варианты ответов
- выявление дефектов в функционировании приложения или в работе с ним
- выявление дефектов использования ресурсов
- выявление несовместимости с окружением
- выявление непредусмотренных сценариев применения или использования непредусмотренных комбинаций данных
Вопрос 31
Варианты ответов
- перетестирование предусматривает только контроль частей приложения, связанных с изменениями
- выбор между полным и частичным перетестированием и пополнением тестовых наборов
- регрессионное тестирование является подмножеством системного тестирования
Вопрос 32
Какие типы дефектов выявляются при системном и регрессионном тестировании
Программные ошибки. Методы отладки
Прежде всего определимся с некоторыми понятиями, связанными с отладкой программного обеспечения.
Программная ошибка — это расхождение между программой и ее спецификацией, причем тогда и только тогда, когда спецификация существует и она правильна. Также можно определить, что программная ошибка — это ситуация, когда программа не делает того, что пользователь от нее вполне обоснованно ожидает.
Отладкой называют процесс локализации и исправления ошибок, обнаруженных при тестировании программного обеспечения.
Локализация — это определение оператора/операторов программы, выполнение которого вызвало нарушение вычислительного процесса.
Для исправления ошибки необходимо определить ее причину, т.е. определить оператор или фрагмент, содержащие ошибку. Причины ошибок могут быть и очевидными, и очень глубоко скрытыми.
В соответствии с этапом обработки, на котором появляются ошибки, различают ошибки компиляции, ошибки компоновки, ошибки выполнения (рис. 5.1) [7].
Рис. 5.1. Группы программных ошибок
Ошибки компиляции — это синтаксические ошибки, фиксируемые компилятором (транслятором, интерпретатором). Ошибки компиляции являются самыми простыми, так как синтаксис языка, как правило, строго формализован, и ошибки сопровождаются подробным комментарием с указанием местоположения ошибки. Чем лучше формализованы правила синтаксиса языка, тем больше ошибок из общего количества может обнаружить компилятор и, соответственно, меньше ошибок возникнет на следующих этапах.
Ошибки компоновки — ошибки, обнаруженные компоновщиком (редактором связей) при объединении модулей программы. Ошибки компоновки связаны с проблемами, обнаруженными при разрешении внешних ссылок. Например, предусмотрено обращение к подпрограмме другого модуля, а при объединении модулей данная подпрограмма не найдена или не стыкуются списки параметров.
Ошибки выполнения — ошибки, обнаруженные операционной системой, аппаратными средствами или пользователем при выполнении программы. Ошибки выполнения являются самыми непредсказуемыми. Некоторые из них обнаруживаются и документируются операционной системой. Они могут иметь разную природу и поэтому по-разному проявляться:
- • появление сообщения об ошибке, например, деление на ноль, нарушение адресации, переполнение разрядов и т.п.;
- • появление сообщения об ошибке, обнаруженной операционной системой, например при попытке записи на защищенные устройства памяти, при ссылке на отсутствующий файл и т.п.;
- • «зависание» компьютера (иногда для продолжения работы необходима его перезагрузка);
- • несовпадение полученных результатов с ожидаемыми.
Причины ошибок выполнения очень разнообразны, а потому их
сложно локализовать. Все возможные причины ошибок выполнения можно разделить на следующие группы:
- • ошибки определения данных;
- • логические ошибки;
- • ошибки накопления погрешностей.
Ошибки определения данных (неверное определение исходных данных) возникают при выполнении операций ввода-вывода: ошибки передачи, ошибки преобразования, ошибки перезаписи, ошибки данных. Использование специальных технических средств и программирование с защитой от ошибок позволяют обнаружить и предотвратить только часть этих ошибок.
Логические ошибки имеют разную природу и могут следовать из ошибок, допущенных при проектировании, например при выборе методов, разработке алгоритмов или определении структуры данных (классов), а могут быть непосредственно внесены при кодировании модуля. К ошибкам кодирования относятся:
- • ошибки некорректного использования переменных, например неудачный выбор типов данных, использование переменных до их инициализации, использование индексов, выходящих за границы определения массивов, нарушения соответствия типов данных и т.п.;
- • ошибки вычислений, например некорректная работа с переменными, некорректное преобразование типов данных в процессе вычислений и т.п.;
• ошибки взаимодействия модулей, т.е. межмодульного интерфейса, например нарушение типов и последовательности при передаче параметров, несоблюдение единства единиц измерения формальных и фактических параметров, нарушение области действия локальных и глобальных переменных.
Возможны и другие ошибки кодирования, например неправильная реализация логики программы при кодировании, игнорирование особенностей или ограничений конкретного языка программирования.
Ошибки накопления погрешностей возникают в результате накопления погрешностей результатов числовых вычислений, например при некорректном отбрасывании дробных цифр чисел, при некорректном использовании приближенных методов вычислений и т.п.
Процесс отладки требует от разработчика глубоких знаний специфики среды и языка программирования, используемых технических средств, операционной системы. На сложность отладки оказывают влияние следующие факторы:
- • опосредованное проявление ошибок;
- • возможность взаимного влияния ошибок;
- • возможность получения внешне одинаковых проявлений разных ошибок;
- • стохастические ошибки, которые могут не проявиться от запуска к запуску;
- • может аннулироваться или измениться внешнее проявление ошибок при внесении некоторых изменений в программу, например, при включении в программу диагностических фрагментов. Отладка программы всегда предполагает обдумывание и логическое осмысление всей имеющейся информации об ошибке. Большинство ошибок можно обнаружить, тщательно анализируя текст программы и результаты тестирования.
Методы отладки программного обеспечения можно классифицировать следующим образом [7]:
- • метод ручного тестирования;
- • метод индукции;
- • метод дедукции;
- • метод обратного прослеживания.
Метод ручного тестирования — самый простой и естественный способ отладки программы. При обнаружении ошибки необходимо выполнить тестируемую программу вручную, используя тестовый набор, при работе с которыми была обнаружена ошибка. Метод эффективен, но не применим для больших программных систем и программ со сложными вычислениями. Этот метод часто используют как составную часть других методов отладки.
Метод индукции предусматривает подробный анализ проявления ошибки. Это могут быть неверные результаты вычислений или сообщение об ошибке. Если компьютер просто «зависает», то место проявления ошибки в программном обеспечении определяют исходя из последних полученных результатов и действий пользователя. Полученную таким образом информацию можно изучить, просматривая соответствующий фрагмент программы. В результате выдвигаются гипотезы об ошибках, которые затем проверяются. Если гипотеза верна, то детализируют информацию об ошибке, иначе — выдвигают другую гипотезу. Если в результате изучения данных никаких гипотез не появляется, то необходима дополнительная информация об ошибке.
Метод дедукции работает по следующему алгоритму. Сначала формируют множество причин, которые могли бы вызвать данное проявление ошибки. Затем, анализируя причины, исключают те, которые противоречат имеющимся данным. Если все причины исключены, то необходима дополнительная информация об ошибке и следует выполнить дополнительное тестирование исследуемого фрагмента. В противном случае наиболее вероятную гипотезу пытаются доказать. Если гипотеза объясняет полученные признаки ошибки, то ошибка найдена, иначе — проверяют следующую причину.
Метод обратного прослеживания используется для небольших программ и заключается в следующем. Определяется точка вывода неправильного результата. Затем строится гипотеза о значениях основных переменных, которые могли бы привести к получению этого результата. Исходя из этой гипотезы, делают предположения о значениях переменных в предыдущей точке. Процесс продолжают, пока не обнаружат причину ошибки.
Рассмотрим категории программных ошибок, которые встречаются наиболее часто.
Функциональные недостатки. Данные недостатки присущи программе, если она не делает того, что должна, выполняет одну из своих функций плохо или не полностью. Функции программы должны быть подробно описаны в ее спецификации, и именно на основе утвержденной спецификации тестировщик строит свою работу.
Недостатки пользовательского интерфейса. Лучше всего оценить удобство и правильность работы пользовательского интерфейса может только пользователь в процессе работы с ним. Проверить это возможно с помощью прототипа программного обеспечения, на котором проводятся обкатка и согласование всех требований к пользовательскому интерфейсу с дальнейшей фиксацией их в спецификации требований. После утверждения спецификации требований любые отклонения от нее или невыполнение последних являются ошибкой. Это в полной мере касается и пользовательского интерфейса.
Недостаточная производительность. При разработке некоторого программного продукта очень важной его характеристикой может оказаться скорость работы, иногда этот критерий задается в требованиях заказчика к программному обеспечению. Недопустимо, если программа не удовлетворяет заданным в спецификации требований характеристикам. Это уже ошибка, которая должна быть обязательно устранена.
Некорректная обработка ошибок. Правильно определив ошибку, программа должна выдать о ней сообщение. Отсутствие такого сообщения является ошибкой в работе программы.
Некорректная обработка граничных условий. Существует много различных граничных ситуаций. Любой аспект работы программы, к которому применимы понятия «больше» или «меньше», «раньше» или «позже», «первый» или «последний», «короче» или «длиннее», обязательно должен быть проверен на границах диапазона. Внутри диапазонов программа может работать правильно, а на их границах могут происходить неожиданные ситуации, которые, в свою очередь, приводят к ошибкам в работе программного обеспечения.
Ошибки вычислений. К ошибкам вычислений относятся ошибки, вызванные неправильным выбором алгоритма вычислений, неправильными формулами либо формулами, неприменимыми к обрабатываемым данным. Самыми распространенными среди ошибок вычислений являются ошибки округления.
Ошибки управления потоком. По логике работы программы вслед за первым действием должно быть выполнено второе. Если вместо этого выполняется третье или четвертое действие, значит, в управлении потоком допущена ошибка.
Ситуация гонок. Предположим, в системе ожидаются два события: А и Б. Если первым наступит событие А, то выполнение программы продолжится, а если событие Б, то в работе программы произойдет сбой. Разработчики предполагают, что первым всегда должно быть событие А, и не ожидают, что Б может выиграть гонки и наступить раньше. Такова классическая ситуация гонок. Тестировать ситуации гонок довольно сложно. Наиболее типичны они для систем, где параллельно выполняются взаимодействующие процессы и потоки, а также для многопользовательских систем реального времени. Ошибки в таких системах трудно воспроизвести, и на их выявление обычно требуется очень много времени.
Перегрузки. Сбои в работе программы могут происходить из-за нехватки памяти или отсутствия других необходимых системных ресурсов. У каждой программы свои пределы, программа может не справляться с повышенными нагрузками, например со слишком большими объемами данных. Вопрос в том, соответствуют ли реальные возможности программы, ее требования к ресурсам спецификации программы, и как она себя поведет при перегрузках.
Некорректная работа с аппаратурой компьютера. Программы могут отправлять аппаратным устройствам неверные данные, игнорировать их сообщения об ошибках, пытаться использовать устройства, которые заняты или вообще отсутствуют. Даже если нужное устройство просто неисправно, программа должна понять это, а не «зависать» при попытке к нему обратиться.
| layout | title | tags |
|---|---|---|
|
col-document |
WSTG — Latest |
WSTG |
{% include breadcrumb.html %}
Тестирование некорректной обработки ошибок
| ID |
|---|
| WSTG-ERRH-01 |
Обзор
Все типы приложений (web-приложения, web-серверы, базы данных и т.д.) по различным причинам генерируют ошибки. Разработчики часто игнорируют обработку этих ошибок или отбрасывают мысль о том, что пользователь когда-либо попытается намеренно вызвать ошибку (например, вводя символьную строку вместо ожидаемого целого числа). Когда разработчик рассматривает только позитивный путь, он забывает о других возможных входных данных от пользователя, которые код может получить, но не может обработать.
Ошибки иногда возникают в виде:
- трассировки стека,
- тайм-аута сети,
- несоответствия входных данных,
- дампа памяти.
Некорректная обработка ошибок может позволить злоумышленникам:
- Разобраться в API, используемых внутри компании.
- Сопоставить различные сервисы, интегрированные друг с другом, получив представление об используемых внутренних системах и фреймворках, что открывает возможности для цепочки атак.
- Собрать сведения о версиях и типах используемых приложений.
- Сделать систему недоступной, вызвав взаимную блокировку или необрабатываемое исключение, которое отправляет сигнал паники работающему серверу.
- Управлять обходом, если какое-то исключение не ограничено логикой, настроенной только на позитивный путь.
Задачи тестирования
- Найти вывод по существующим ошибкам.
- Проанализировать различные возвращаемые выходные данные.
Как тестировать
Ошибки обычно считаются безобидными, поскольку они предоставляют диагностические данные и сообщения, которые могут помочь пользователю понять проблему или разработчику отладить эту ошибку.
Пытаясь ввести неожиданные данные или заставляя систему работать с определёнными крайними случаями и сценариями, система или приложение в большинстве случаев будут выдавать какую-то информацию о том, что происходит внутри, если только разработчики не отключили все возможные ошибки и не выдают какое-то своё (нестандартное) сообщение.
Web-серверы
Все web-приложения работают на web-сервере, будь то интегрированный или полноценный. Web-приложения должны обрабатывать и анализировать HTTP-запросы, и поэтому web-сервер всегда входит в состав стека. Одними из самых известных web-серверов являются NGINX, Apache и IIS.
Для каждого web-сервера известны типовые сообщения об ошибках и их форматы. Если кто-то не знаком с тем, как они выглядят, поиск в Интернете даст примеры. Ещё один способ — заглянуть в документацию или просто настроить сервер локально и найти ошибки, просмотрев страницы, которые использует web-сервер.
Чтобы вызвать сообщения об ошибках, тестировщик должен:
- Поискать какие-либо файлы и папки, которые нельзя найти (404-я).
- Запросить существующие папки и посмотреть поведение сервера (403-я, пустая страница или содержание каталога).
- Попробуйте отправить запрос, который нарушает RFC по HTTP. Одним из примеров может быть отправка очень большого пути, нарушение формата заголовков или изменение версии HTTP.
- Даже если ошибки обрабатываются на уровне приложения, нарушение RFC по HTTP может заставить интегрированный web-сервер проявить себя, поскольку он должен обрабатывать запрос, а разработчики забывают переопределить эти ошибки.
Applications
Приложения наиболее подвержены появлению широкого спектра сообщений об ошибках, которые включают в себя: трассировки стека, дампы памяти, некорректно обработанные исключения и типовые ошибки. Это происходит из-за того, что приложения в основном создаются на заказ, поэтому разработчикам необходимо наблюдать и обрабатывать все возможные случаи ошибок (или иметь глобальный механизм обнаружения ошибок), также они могут возникать в результате интеграции с другими сервисами.
Чтобы заставить приложение выдавать эти ошибки, тестировщик должен:
- Определить возможные точки входа, в которых приложение ожидает данные.
- Проанализироватб ожидаемый тип входных данных (строки, целые числа, JSON, XML и т.д.).
- Провести фаззинг каждой точки входа на основе предыдущих шагов для получения более сфокусированного сценария тестирования.
- Фаззинг каждой точки входа со всеми возможными инъекциями — не лучшее решение, если у вас нет неограниченного времени на тестирование и приложение сможет обработать такой объём входных данных.
- Если фаззинг невозможен, выберите перспективные входные данные, которые имеют наибольшие шансы сломать данный парсер (например, закрывающая скобка для тела JSON, большой текст когда ожидается всего пара символов, инъекция CRLF с параметрами, которые могут быть разобраны серверами и форматно-логическим контролем, специальные символы, которые недопустимы в именах файлов и т.д.).
- Фаззинг с использованием такой полезной нагрузки должен проводиться для каждого типа данных, поскольку иногда интерпретаторы ломаются и вне обработки исключений, определённой разработчиком.
- Разберитесь с сервисом, который выдаёт сообщения об ошибках, и попытайтесь составить более точный список векторов для фаззинга, чтобы получить больше информации или подробностей об ошибках из этого сервиса (это может быть база данных, отдельный сервис и т.д.).
Сообщения об ошибках иногда являются основным недостатком проектирования систем, особенно в микросервисной архитектуре. Если сервисы должным образом не настроены на обработку ошибок единым и универсальным образом, сообщения об ошибках позволят тестировщику определить, какой сервис какие запросы обрабатывает, и провести более целенаправленную атаку на каждый сервис.
Тестировщику необходимо внимательно следить за типом ответа. Иногда ошибки возвращаются как 200-е с телом ошибки, иногда скрывают ошибку в 302-й или просто имеют свой способ представления этой ошибки.
Меры защиты
Для исправления ситуации ознакомьтесь с мерой C10 из Top 10 мер проактивной защиты (перевод) и Памяткой OWASP по обработке ошибок.
Тестовый полигон
- Juice Shop — Обработка ошибок
Ссылки
- WSTG: Приложение C — Векторы для фаззинга
- Top 10 мер проактивной защиты — C10: Обработка ошибок и исключений
- ASVS v4 v7.4: Обработка ошибок
- CWE 728 — Improper Error Handling
- Памятка OWASP по обработке ошибок
Существует две фундаментальные стратегии: обработка исправимых ошибок (исключения, коды возврата по ошибке, функции-обработчики) и неисправимых (assert(), abort()). В каких случаях какую стратегию лучше использовать?
Виды ошибок
Ошибки возникают по разным причинам: пользователь ввёл странные данные, ОС не может дать вам обработчика файла или код разыменовывает (dereferences) nullptr. Каждая из описанных ошибок требует к себе отдельного подхода. По причинам ошибки делятся на три основные категории:
- Пользовательские ошибки: здесь под пользователем подразумевается человек, сидящий перед компьютером и действительно «использующий» программу, а не какой-то программист, дёргающий ваш API. Такие ошибки возникают тогда, когда пользователь делает что-то неправильно.
- Системные ошибки появляются, когда ОС не может выполнить ваш запрос. Иными словами, причина системных ошибок — сбой вызова системного API. Некоторые возникают потому, что программист передал системному вызову плохие параметры, так что это скорее программистская ошибка, а не системная.
- Программистские ошибки случаются, когда программист не учитывает предварительные условия API или языка программирования. Если API требует, чтобы вы не вызывали
foo()с0в качестве первого параметра, а вы это сделали, — виноват программист. Если пользователь ввёл0, который был переданfoo(), а программист не написал проверку вводимых данных, то это опять же его вина.
Каждая из описанных категорий ошибок требует особого подхода к их обработке.
Пользовательские ошибки
Сделаю очень громкое заявление: такие ошибки — на самом деле не ошибки.
Все пользователи не соблюдают инструкции. Программист, имеющий дело с данными, которые вводят люди, должен ожидать, что вводить будут именно плохие данные. Поэтому первым делом нужно проверять их на валидность, сообщать пользователю об обнаруженных ошибках и просить ввести заново.
Поэтому не имеет смысла применять к пользовательским ошибкам какие-либо стратегии обработки. Вводимые данные нужно как можно скорее проверять, чтобы ошибок не возникало.
Конечно, такое не всегда возможно. Иногда проверять вводимые данные слишком дорого, иногда это не позволяет сделать архитектура кода или разделение ответственности. Но в таких случаях ошибки должны обрабатываться однозначно как исправимые. Иначе, допустим, ваша офисная программа будет падать из-за того, что вы нажали backspace в пустом документе, или ваша игра станет вылетать при попытке выстрелить из разряженного оружия.
Если в качестве стратегии обработки исправимых ошибок вы предпочитаете исключения, то будьте осторожны: исключения предназначены только для исключительных ситуаций, к которым не относится большинство случаев ввода пользователями неверных данных. По сути, это даже норма, по мнению многих приложений. Используйте исключения только тогда, когда пользовательские ошибки обнаруживаются в глубине стека вызовов, вероятно, внешнего кода, когда они возникают редко или проявляются очень жёстко. В противном случае лучше сообщать об ошибках с помощью кодов возврата.
Системные ошибки
Обычно системные ошибки нельзя предсказать. Более того, они недетерминистские и могут возникать в программах, которые до этого работали без нареканий. В отличие от пользовательских ошибок, зависящих исключительно от вводимых данных, системные ошибки — настоящие ошибки.
Но как их обрабатывать, как исправимые или неисправимые?
Это зависит от обстоятельств.
Многие считают, что ошибка нехватки памяти — неисправимая. Зачастую не хватает памяти даже для обработки этой ошибки! И тогда приходится просто сразу же прерывать выполнение.
Но падение программы из-за того, что ОС не может выделить сокет, — это не слишком дружелюбное поведение. Так что лучше бросить исключение и позволить catch аккуратно закрыть программу.
Но бросание исключения — не всегда правильный выбор.
Кто-то даже скажет, что он всегда неправильный.
Если вы хотите повторить операцию после её сбоя, то обёртывание функции в try-catch в цикле — медленное решение. Правильный выбор — возврат кода ошибки и цикличное исполнение, пока не будет возвращено правильное значение.
Если вы создаёте вызов API только для себя, то просто выберите подходящий для своей ситуации путь и следуйте ему. Но если вы пишете библиотеку, то не знаете, чего хотят пользователи. Дальше мы разберём подходящую стратегию для этого случая. Для потенциально неисправимых ошибок подойдёт «обработчик ошибок», а при других ошибках необходимо предоставить два варианта развития событий.
Обратите внимание, что не следует использовать подтверждения (assertions), включающиеся только в режиме отладки. Ведь системные ошибки могут возникать и в релизной сборке!
Программистские ошибки
Это худший вид ошибок. Для их обработки я стараюсь сделать так, чтобы мои ошибки были связаны только с вызовами функций, то есть с плохими параметрами. Прочие типы программистских ошибок могут быть пойманы только в runtime, с помощью отладочных макросов (assertion macros), раскиданных по коду.
При работе с плохими параметрами есть две стратегии: дать им определённое или неопределённое поведение.
Если исходное требование для функции — запрет на передачу ей плохих параметров, то, если их передать, это считается неопределённым поведением и должно проверяться не самой функцией, а оператором вызова (caller). Функция должна делать только отладочное подтверждение (debug assertion).
С другой стороны, если отсутствие плохих параметров не является частью исходных требований, а документация определяет, что функция будет бросать bad_parameter_exception при передаче ей плохого параметра, то передача — это хорошо определённое поведение (бросание исключения или любая другая стратегия обработки исправимых ошибок), и функция всегда должна это проверять.
В качестве примера рассмотрим получающие функции (accessor functions) : в спецификации на std::vector<T>operator[] говорится, что индекс должен быть в пределах валидного диапазона, при этом at() сообщает нам, что функция кинет исключение, если индекс не попадает в диапазон. Более того, большинство реализаций стандартных библиотек обеспечивают режим отладки, в котором проверяется индекс operator[], но технически это неопределённое поведение, оно не обязано проверяться.
Примечание: необязательно бросать исключение, чтобы получилось определённое поведение. Пока это не упомянуто в исходных условиях для функции, это считается определённым. Всё, что прописано в исходных условиях, не должно проверяться функцией, это неопределённое поведение.
Когда нужно проверять только с помощью отладочных подтверждений, а когда — постоянно?
К сожалению, однозначного рецепта нет, решение зависит от конкретной ситуации. У меня есть лишь одно проверенное правило, которому я следую при разработке API. Оно основано на наблюдении, что проверять исходные условия должен вызывающий, а не вызываемый. А значит, условие должно быть «проверяемым» для вызывающего. Также условие «проверяемое», если можно легко выполнить операцию, при которой значение параметра всегда будет правильным. Если для параметра это возможно, то это получается исходное условие, а значит, проверяется только посредством отладочного подтверждения (а если слишком дорого, то вообще не проверяется).
Но конечное решение зависит от многих других факторов, так что очень трудно дать какой-то общий совет. По умолчанию я стараюсь свести к неопределённому поведению и использованию только подтверждений. Иногда бывает целесообразно обеспечить оба варианта, как это делает стандартная библиотека с operator[] и at().
Хотя в ряде случаев это может быть ошибкой.
Об иерархии std::exception
Если в качестве стратегии обработки исправимых ошибок вы выбрали исключения, то рекомендуется создать новый класс и наследовать его от одного из классов исключений стандартной библиотеки.
Я предлагаю наследовать только от одного из этих четырёх классов:
std::bad_alloc: для сбоев выделения памяти.std::runtime_error: для общих runtime-ошибок.std::system_error(производное отstd::runtime_error): для системных ошибок с кодами ошибок.std::logic_error: для программистских ошибок с определённым поведением.
Обратите внимание, что в стандартной библиотеке разделяются логические (то есть программистские) и runtime-ошибки. Runtime-ошибки — более широкое определение, чем «системные». Оно описывает «ошибки, обнаруживаемые только при выполнении программы». Такая формулировка не слишком информативна. Лично я использую её для плохих параметров, которые не являются исключительно программистскими ошибками, а могут возникнуть и по вине пользователей. Но это можно определить лишь глубоко в стеке вызовов. Например, плохое форматирование комментариев в standardese приводит к исключению при парсинге, проистекающему из std::runtime_error. Позднее оно ловится на соответствующем уровне и фиксируется в логе. Но я не стал бы использовать этот класс иначе, как и std::logic_error.
Подведём итоги
Есть два пути обработки ошибок:
- как исправимые: используются исключения или возвращаемые значения (в зависимости от ситуации/религии);
- как неисправимые: ошибки журналируются, а программа прерывается.
Подтверждения — это особый вид стратегии обработки неисправимых ошибок, только в режиме отладки.
Есть три основных источника ошибок, каждый требует особого подхода:
- Пользовательские ошибки не должны обрабатываться как ошибки на верхних уровнях программы. Всё, что вводит пользователь, должно проверяться соответствующим образом. Это может обрабатываться как ошибки только на нижних уровнях, которые не взаимодействуют с пользователями напрямую. Применяется стратегия обработки исправимых ошибок.
- Системные ошибки могут обрабатываться в рамках любой из двух стратегий, в зависимости от типа и тяжести. Библиотеки должны работать как можно гибче.
- Программистские ошибки, то есть плохие параметры, могут быть запрещены исходными условиями. В этом случае функция должна использовать только проверку с помощью отладочных подтверждений. Если же речь идёт о полностью определённом поведении, то функции следует предписанным образом сообщать об ошибке. Я стараюсь по умолчанию следовать сценарию с неопределённым поведением и определяю для функции проверку параметров лишь тогда, когда это слишком трудно сделать на стороне вызывающего.
Гибкие методики обработки ошибок в C++
Иногда что-то не работает. Пользователи вводят данные в недопустимом формате, файл не обнаруживается, сетевое соединение сбоит, в системе кончается память. Всё это ошибки, и их надо обрабатывать.
Это относительно легко сделать в высокоуровневых функциях. Вы точно знаете, почему что-то пошло не так, и можете обработать это соответствующим образом. Но в случае с низкоуровневыми функциями всё не так просто. Они не знают, что пошло не так, они знают лишь о самом факте сбоя и должны сообщить об этом тому, кто их вызвал.
В C++ есть два основных подхода: коды возврата ошибок и исключения. Сегодня широко распространено использование исключений. Но некоторые не могут / думают, что не могут / не хотят их использовать — по разным причинам.
Я не буду принимать чью-либо сторону. Вместо этого я опишу методики, которые удовлетворят сторонников обоих подходов. Особенно методики пригодятся разработчикам библиотек.
Проблема
Я работаю над проектом foonathan/memory. Это решение предоставляет различные классы выделения памяти (allocator classes), так что в качестве примера рассмотрим структуру функции выделения.
Для простоты возьмём malloc(). Она возвращает указатель на выделяемую память. Если выделить память не получается, то возвращается nullptr, то есть NULL, то есть ошибочное значение.
У этого решения есть недостатки: вам нужно проверять каждый вызов malloc(). Если вы забудете это сделать, то выделите несуществующую память. Кроме того, по своей натуре коды ошибок транзитивны: если вызвать функцию, которая может вернуть код ошибки, и вы не можете его проигнорировать или обработать, то вы тоже должны вернуть код ошибки.
Это приводит нас к ситуации, когда чередуются нормальные и ошибочные ветви кода. Исключения в таком случае выглядят более подходящим решением. Благодаря им вы сможете обрабатывать ошибки только тогда, когда вам это нужно, а в противном случае — достаточно тихо передать их обратно вызывающему.
Это можно расценить как недостаток.
Но в подобных ситуациях исключения имеют также очень большое преимущество: функция выделения памяти либо возвращает валидную память, либо вообще ничего не возвращает. Это функция «всё или ничего», возвращаемое значение всегда будет валидным. Это полезное следствие согласно принципу Скотта Майера «Make interfaces hard to use incorrectly and easy to use correctly».
Учитывая вышесказанное, можно утверждать, что вам следует использовать исключения в качестве механизма обработки ошибок. Этого мнения придерживается большинство разработчиков на С++, включая и меня. Но проект, которым я занимаюсь, — это библиотека, предоставляющая средства выделения памяти, и предназначена она для приложений, работающих в реальном времени. Для большинства разработчиков подобных приложений (особенно для игроделов) само использование исключений — исключение.
Каламбур детектед.
Чтобы уважить эту группу разработчиков, моей библиотеке лучше обойтись без исключений. Но мне и многим другим они нравятся за элегантность и простоту обработки ошибок, так что ради других разработчиков моей библиотеке лучше использовать исключения.
Так что же делать?
Идеальное решение: возможность включать и отключать исключения по желанию. Но, учитывая природу исключений, нельзя просто менять их местами с кодами ошибок, поскольку у нас не будет внутреннего кода проверки на ошибки — весь внутренний код опирается на предположение о прозрачности исключений. И даже если бы внутри можно было использовать коды ошибок и преобразовывать их в исключения, это лишило бы нас большинства преимуществ последних.
К счастью, я могу определить, что вы делаете, когда обнаруживаете ошибку нехватки памяти: чаще всего вы журналируете это событие и прерываете программу, поскольку она не может корректно работать без памяти. В таких ситуациях исключения — просто способ передачи контроля другой части кода, которая журналирует и прерывает программу. Но есть старый и эффективный способ передачи контроля: указатель функции (function pointer), то есть функция-обработчик (handler function).
Если у вас включены исключения, то вы просто их бросаете. В противном случае вызываете функцию-обработчика и затем прерываете программу. Это предотвратит бесполезную работу функции-обработчика, та позволит программе продолжить выполняться в обычном режиме. Если не прервать, то произойдёт нарушение обязательного постусловия функции: всегда возвращать валидный указатель. Ведь на выполнении этого условия может быть построена работа другого кода, да и вообще это нормальное поведение.
Я называю такой подход обработкой исключений и придерживаюсь его при работе с памятью.
Решение 1: обработчик исключений
Если вам нужно обработать ошибку в условиях, когда наиболее распространённым поведением будет «журналировать и прервать», то можно использовать обработчика исключений. Это такая функция-обработчик, которая вызывается вместо бросания объекта-исключения. Её довольно легко реализовать даже в уже существующем коде. Для этого нужно поместить управление обработкой в класс исключений и обернуть в макрос выражение throw.
Сначала дополним класс и добавим функции для настройки и, возможно, запрашивания функции-обработчика. Я предлагаю делать это так же, как стандартная библиотека обрабатывает std::new_handler:
class my_fatal_error
{
public:
// тип обработчика, он должен брать те же параметры, что и конструктор,
// чтобы у них была одинаковая информация
using handler = void(*)( ... );
// меняет функцию-обработчика
handler set_handler(handler h);
// возвращает текущего обработчика
handler get_handler();
... // нормальное исключение
};
Поскольку это входит в область видимости класса исключений, вам не нужно именовать каким-то особым образом. Отлично, нам же легче.
Если исключения включены, то для удаления обработчика можно использовать условное компилирование (conditional compilation). Если хотите, то также напишите обычный подмешанный класс (mixin class), дающий требуемую функциональность.
Конструктор исключений элегантен: он вызывает текущую функцию-обработчика, передавая ей требуемые аргументы из своих параметров. А затем комбинирует с последующим макросом throw:
If```cpp #if EXCEPTIONS #define THROW(Ex) throw (Ex) #else #define THROW(Ex) (Ex), std::abort() #endif
> Такой макрос throw также предоставляется [foonathan/compatiblity](https://github.com/foonathan/compatibility).
Можно использовать его и так:
```cpp
THROW(my_fatal_error(...))
Если у вас включена поддержка исключений, то будет создан и брошен объект-исключение, всё как обычно. Но если поддержка выключена, то объект-исключение всё равно будет создан, и — это важно — только после этого произойдёт вызов std::abort(). А поскольку конструктор вызывает функцию-обработчика, то он и работает, как требуется: вы получаете точку настройки для журналирования ошибки. Благодаря же вызову std::abort() после конструктора пользователь не может нарушить постусловие.
Когда я работаю с памятью, то при включённых исключениях у меня также включён и обработчик, который вызывается при бросании исключения.
Так что при этой методике вам ещё будет доступна определённая степень кастомизации, даже если вы отключите исключения. Конечно, замена неполноценная, мы только журналируем и прерываем работу программы, без дальнейшего продолжения. Но в ряде случаев, в том числе при исчерпании памяти, это вполне пригодное решение.
А если я хочу продолжить работу после бросания исключения?
Методика с обработчиком исключений не позволяет этого сделать в связи с постусловием кода. Как же тогда продолжить работу?
Ответ прост — никак. По крайней мере, это нельзя сделать так же просто, как в других случаях. Нельзя просто так вернуть код ошибки вместо исключения, если функция на это не рассчитана.
Есть только одно решение: сделать две функции. Одна возвращает код ошибки, а вторая бросает исключения. Клиенты, которым нужны исключения, будут использовать второй вариант, остальные — первый.
Извините, что говорю такие очевидные вещи, но ради полноты изложения я должен был об этом сказать.
Для примера снова возьмём функцию выделения памяти. В этом случае я использую такие функции:
void* try_malloc(..., int &error_code) noexcept;
void* malloc(...);
При сбое выделения памяти первая версия возвращает nullptr и устанавливает error_code в коде ошибки. Вторая версия не возвращает nullptr, зато бросает исключение. Обратите внимание, что в рамках первой версии очень легко реализовать вторую:
void* malloc(...)
{
auto error_code = 0;
auto res = try_malloc(..., error_code);
if (!res)
throw malloc_error(error_code);
return res;
}
Не делайте этого в обратной последовательности, иначе вам придётся ловить исключение, а это дорого. Также это не даст нам скомпилировать код без включённой поддержки исключений. Если сделаете, как показано, то можете просто стереть другую перегрузку (overload) с помощью условного компилирования.
Но даже если у вас включена поддержка исключений, клиенту всё равно может понадобиться вторая версия. Например, когда нужно выделить наибольший возможный объём памяти, как в нашем примере. Будет проще и быстрее вызывать в цикле и проверять по условию, чем ловить исключение.
Решение 2: предоставить две перегрузки
Если недостаточно обработчика исключений, то нужно предоставить две перегрузки. Одна использует код возврата, а вторая бросает исключение.
Если рассматриваемая функция не имеет возвращаемого значения, то можете её использовать для кода ошибки. В противном случае вам придётся возвращать недопустимое значение для сигнализирования об ошибке — как nullptr в вышеприведённом примере, — а также установить выходной параметр для кода ошибки, если хотите предоставить вызывающему дополнительную информацию.
Пожалуйста, не используйте глобальную переменную errno или что-то типа GetLastError()!
Если возвращаемое значение не содержит недопустимое значение для обозначения сбоя, то по мере возможности используйте std::optional или что-то похожее.
Перегрузка исключения (exception overload) может — и должна — быть реализована в рамках версии с кодом ошибки, как это показано выше. Если компилируете без исключений, сотрите перегрузку с помощью условного компилирования.
std::system_error
Подобная система идеально подходит для работы с кодами ошибок в С++ 11.
Она возвращает непортируемый (non-portable) код ошибки std::error_code, то есть возвращаемый функцией операционной системы. С помощью сложной системы библиотечных средств и категорий ошибок вы можете добавить собственные коды ошибок, или портируемые std::error_condition. Для начала почитайте об этом здесь. Если нужно, то можете использовать в функции кода ошибки std::error_code. А для функции исключения есть подходящий класс исключения: std::system_error. Он берёт std::error_code и применяется для передачи этих ошибок в виде исключений.
Эту или подобную систему должны использовать все низкоуровневые функции, являющиеся закрытыми обёртками ОС-функций. Это хорошая — хотя и сложная — альтернатива службе кодов ошибок, предоставляемой операционной системой.
Да, и мне ещё нужно добавить подобное в функции виртуальной памяти. На сегодняшний день они не предоставляют коды ошибок.
std::expected
Выше упоминалось о проблеме, когда у вас нет возвращаемого значения, содержащего недопустимое значение, которое можно использовать для сигнализирования об ошибке. Более того, выходной параметр — не лучший способ получения кода ошибки.
А глобальные переменные вообще не вариант!
В № 4109 предложено решение: std::expected. Это шаблон класса, который также хранит возвращаемое значение или код ошибки. В вышеприведённом примере он мог бы использоваться так:
std::expected<void*, std::error_code> try_malloc(...);
В случае успеха std::expected будет хранить не-null указатель памяти, а при сбое — std::error_code. Сейчас эта методика работает при любых возвращаемых значениях. Комбинация std::expected и функции исключения определённо допускает любые варианты использования.
Заключение
Если вы создаёте библиотеки, то иногда приходится обеспечивать максимальную гибкость использования. Под этим подразумевается и разнообразие средств обработки ошибок: иногда требуются коды возврата, иногда — исключения.
Одна из возможных стратегий — улаживание этих противоречий с помощью обработчика исключений. Просто удостоверьтесь, что когда нужно, то вызывается callback, а не бросается исключение. Это замена для критических ошибок, которая в любом случае будет журналироваться перед прерыванием работы программы. Как таковой этот способ не универсален, вы не можете переключаться в одной программе между двумя версиями. Это лишь обходное решение при отключённой поддержке исключений.
Более гибкий подход — просто предоставить две перегрузки, одну с исключениями, а вторую без. Это даст пользователям максимальную свободу, они смогут выбирать ту версию, что лучше подходит в их ситуации. Недостаток этого подхода: вам придётся больше потрудиться при создании библиотеки.






