Недостижимый код
Недостижимый код — это часть исходного кода программы, которая никогда не может быть выполнена, потому что не существует ни одного пути потока управления от остальной части программы к этому коду[1].
Недостижимый код иногда также называют мёртвым кодом[2][3], хотя сам термин мёртвый код также может относиться к коду, который исполняется, но не оказывает никакого влияния на вывод программы[4].
Недостижимый код обычно считается нежелательным по нескольким причинам:
- Он бесполезно занимает память;
- Может вызывать ненужные обращения к инструкции-кэшу ЦП;
- Это также может снижать локальность данных;
- На тестирование, сопровождение и документирование кода, который никогда не используется, тратятся время и ресурсы;
- Иногда единственным пользователем такого кода может быть автотест.
Тем не менее у недостижимого кода могут быть и легитимные применения, например, создание библиотеки функций для вызова вручную или перехода через отладчик в момент остановки программы после точки останова. Это может быть полезно для исследования внутреннего состояния программы или его форматированного вывода. Иногда имеет смысл оставлять такой код и в релизе, чтобы разработчик имел возможность подключить отладчик к работающему экземпляру программы клиента.
Причины
Недостижимый код может возникать по разным причинам, например:
- ошибки программирования в сложных ветвлениях;
- результат внутренних трансформаций, выполняемых оптимизирующим компилятором;
- неполное тестирование нового или изменённого кода;
- устаревший код:
- код, заменённый другой реализацией;
- недостижимый код, который решили не удалять из-за его переплетения с достижимым;
- потенциально достижимый код, который не используется в существующих сценариях;
- «дремлющий» код, который осознанно оставлен на случай необходимости в будущем;
- код, используемый только для отладки.
Устаревшим считают код, который ранее был полезен, но теперь уже не применяется или не требуется. Однако недостижимый код может быть частью сложной библиотеки, модуля или подпрограммы и использоваться другими компонентами либо при определённых, редко встречающихся условиях.
Примером условно недостижимого кода может быть реализация универсальной функции форматирования строк в библиотеке времени выполнения компилятора, где реализована обработка всех возможных параметров, из которых реально используется лишь небольшое множество. Обычно компиляторы не могут удалить такие секции как недостижимые на этапе компиляции, поскольку поведение определяется значениями аргументов во время выполнения.
Примеры
В следующем фрагменте кода на языке C:
int foo (int X, int Y)
{
return X + Y;
int Z = X * Y;
}
определение {{{1}}} никогда не будет достигнуто, так как функция всегда завершает выполнение ранее. Соответственно, переменная Z не будет ни выделена, ни инициализирована.
В реализации SSL/TLS от Apple в феврале 2014 года была обнаружена критическая уязвимость, известная официально как, а неформально как ошибка «goto fail»[5][6]. Соответствующий фрагмент кода:[7]
static OSStatus
SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams,
uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
...
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
...
fail:
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
}
Здесь подряд идут два оператора goto fail. Согласно синтаксису языка C, второй оператор безусловен и всегда приводит к пропуску вызова SSLHashSHA1.final. В результате err будет содержать статус выполнения обновления SHA1, и если первый вызов SSLHashSHA1.update прошёл успешно, то проверка подписи никогда не завершится ошибкой[5].
Таким образом, вызов функции final становится недостижимым кодом[6]. Компилятор Clang с опцией -Weverything выполняет анализ недостижимого кода, что в данном случае привело бы к предупреждению[6].
В C++ ряд конструкций определён как имеющих неопределённое поведение. Компилятор может реализовать любой вариант поведения, а оптимизирующий компилятор чаще всего трактует подобный код как недостижимый[8].
Анализ
Обнаружение недостижимого кода является разновидностью анализа потока управления с целью нахождения участков программы, которые не могут быть достигнуты ни при каких входных данных или сценариях выполнения. В некоторых языках программирования (например, Java) некоторые виды недостижимого кода прямо запрещены стандартом[9]. Оптимизация по удалению недостижимого кода называется удалением мёртвого кода.
Недостижимый код может появиться в ходе оптимизаций компилятора (например, при устранении общих подвыражений).
На практике уровень интеллектуальности анализа сильно влияет на качество обнаружения недостижимого кода. Например, свёртывание констант и простой анализ потока показывают, что тело условного оператора в следующем коде не может быть выполнено:
int N = 2 + 1;
if (N == 4)
{
/* недостижимый код */
}
Но гораздо больший уровень анализа потребуется, чтобы определить невозможность выполнения соответствующего блока кода в следующем примере:
double X = sqrt(2);
if (X > 5)
{
/* недостижимый код */
}
Удаление недостижимого кода относится к тому же классу оптимизаций, что и удаление мёртвого кода и устранение избыточных вычислений.
В некоторых случаях на практике может быть полезно использовать сочетание простых критериев недостижимости и профилирования для обнаружения сложных случаев. Профилирование само по себе не может надёжно доказать недостижимость кода, но способно служить эвристикой для поиска потенциально таких участков. После выявления подозрительного участка могут быть использованы другие методы, такие как более мощный статический анализ или анализ вручную, позволяющий определить, действительно ли код недостижим.
Примечания
Литература
- Appel, A. W. Modern Compiler Implementation in Java. Cambridge University Press, 1998.
- Muchnick, S. S. Advanced Compiler Design and Implementation. Morgan Kaufmann, 1997.