От переводчика: Переводы статьи о неопределенном поведении в языке Си от Криса Латтнера, одного из ведущих разработчиков проекта LLVM, вызвали большой интерес и даже некоторое непонимание со стороны тех, кто не сталкивался с описанными явлениями на практике.
В своей статье Крис предоставляет ссылку на блог Джона Регера и на его статью 2010 года об UB в C и C++.
Но в блоге Регера есть и гораздо более новые статьи на эту тему (что, впрочем, не отменяет ценности старых).
Хочу предложить вашему вниманию недавнюю статью «Неопределённое поведение в 2017 году».
Исходная статья очень большая, и я разделил ее на части.
В первой части мы поговорим о разных инструментах поиска УБ: ASan, UBSan, Tsan и т. д. Асан — Address Sanitizer от Google, разработанный на базе LLVM. УБСан — Undefine Behavior Sanitizer, предназначенный для обнаружения различных UB в программах на C и C++, доступен для Clang и GCC. Цан — Thread Sanitizer, предназначенный для обнаружения UB в многопоточных программах.
Если эта тема покажется вам далеко не практичной, рекомендую дождаться продолжения, ведь в конце вас ждет поистине огромный список УБов языка C++ (их должно быть около 200!) А еще рекомендую прочитать старые статьи Реггера, они не потеряли своей актуальности.
Об авторе: Джон Регер — профессор компьютерных наук Университета Юты в США.
Мы часто слышим, как некоторые утверждают, что проблемы, возникающие из-за неопределенного поведения (UB) в C и C++, в значительной степени решены благодаря широкому использованию инструментов динамической проверки, таких как ASan, UBSan, MSan и Tsan. Здесь мы покажем очевидное: хотя за последние годы в этих инструментах было много значительных улучшений, проблемы UB далеки от разрешения, и давайте посмотрим на ситуацию повнимательнее.
Valgrind и большинство дезинфицирующих средств предназначены для отладки: они генерируют понятные диагностические сообщения, связанные со случаями неопределенного поведения, произошедшими во время тестирования.
Такие инструменты чрезвычайно полезны и помогают нам перейти от мира, в котором почти каждая нетривиальная программа на C и C++ выполняется как непрерывный поток UB, к миру, в котором значительное количество важных программ в основном свободны от UB в своих наиболее распространенных версиях.
конфигурации и варианты использования.
.
Проблема с инструментами динамической отладки заключается в том, что они не делают ничего, чтобы помочь нам справиться с худшими случаями UB: мы не знаем, как они поведут себя при тестировании, но кто-то другой может выяснить, как UB проявится в релизе и используйте это как уязвимость.
Проблема сводится к качественному тестированию, а это сложно.
Такие инструменты, как afl-fuzz, хороши, но они даже не начинают трогать большие программы.
Один из способов обойти проблему тестирования — использовать статические инструменты обнаружения UB. Они постоянно совершенствуются, но уверенный и точный статический анализ не обязательно легче провести, чем добиться хорошего тестового покрытия.
Конечно, эти две методики направлены на решение одной и той же задачи, выявление возможных способов выполнения программы, но под разными углами.
Эта проблема всегда была очень сложной и, возможно, всегда будет таковой.
О поиске УБ посредством статического анализа написано много, в этой статье мы остановимся на динамических инструментах.
Другой способ решить проблему тестирования — использовать инструменты смягчения UB: они превращают неопределенное поведение в определенное при использовании C и C++, эффективно достигая некоторых преимуществ использования безопасных языков программирования.
Проблемы при разработке инструментов для смягчения последствий недержания мочи заключаются в следующем: — не ломайте код в «краевых» случаях (краевых случаях) - имеют низкие накладные расходы — не добавляйте дополнительные уязвимости, например, требующие связывания с непроверенной библиотекой времени выполнения - затруднять атаку - Совместимость друг с другом (напротив, некоторые инструменты отладки, такие как ASan и Tsan, несовместимы и требуют двух прогонов набора тестов для проекта, для которого требуются оба инструмента).
Прежде чем рассматривать отдельные случаи UB, давайте определим наши цели.
Они применимы к любому компилятору C и C++.
Цель 1: каждый случай UB (да, их около 200, полный список мы приведем в конце) должен быть либо документирован как имеющий определенное поведение, диагностируемый компилятором как фатальная ошибка, либо, в крайнем случае, , имейте дезинфицирующее средство, которое обнаруживает UB во время выполнения.
Это не должно вызывать споров, это своего рода минимальное требование для разработки на C и C++ в современном мире, где сетевые пакеты и оптимизации компилятора могут быть использованы злоумышленниками.
Цель 2: Каждый случай UB должен быть либо задокументирован, диагностирован компилятором как фатальная ошибка, либо иметь дополнительный механизм «смягчения», удовлетворяющий предыдущим требованиям.
Это сложнее.
Мы считаем, что этого можно достичь на многих платформах.
Ядра операционной системы и другой критически важный для производительности код требуют использования других технологий, например формальных методов.
В оставшейся части статьи мы рассмотрим текущую ситуацию для различных классов неопределенного поведения.
Начнем с большого класса UB.
Нарушения безопасности пространственной памяти
Описание: Доступ за пределы хранилища и даже создание таких указателей — это UB в C и C++.В 1988 году червь Морриса намекнул на то, что нас ждет в ближайшие N лет. Как мы знаем, N > = 29, и не исключено, что значение N достигнет 75. Отладка: Valgrind и ASan — отличные инструменты отладки.
Во многих случаях ASan лучше, поскольку он требует меньше накладных расходов.
Оба инструмента представляют адреса как 32- или 64-битные значения и резервируют красную зону вокруг допустимых блоков.
Это надежный подход, который позволяет беспрепятственно работать с обычными двоичными библиотеками, использующими этот инструмент, а также поддерживает обычный код, включающий преобразования указателя в целое число.
Valgrind запускается из исполняемого кода и не может вставлять красные зоны между переменными стека, поскольку размещение объектов в стеке уже закодировано значениями смещения в инструкциях обращения к стеку, а изменить адреса доступа к стеку на стеке невозможно.
летать.
В результате Valgrind имеет ограниченную поддержку обнаружения ошибок, связанных с манипулированием объектами в стеке.
ASan запускается во время компиляции и вставляет красные зоны вокруг переменных стека.
Переменные стека малы и многочисленны, а соображения адресного пространства и локальности не позволяют использовать очень большие красные зоны.
При настройках по умолчанию адреса двух соседних локальных целочисленных переменных x и y будут разделены шестнадцатью байтами.
Другими словами, проверки, выполняемые ASan и Valgrind, касаются только размещения объектов в памяти, а размещение объектов с включенной проверкой отличается от размещения объектов без использования средств проверки.
Некоторым недостатком ASan и Valgrind является то, что они могут пропустить UB, если какой-то код был удален оптимизатором и не может быть запущен, как в примере.
смягчение последствий : У нас уже давно существуют механизмы предотвращения небезопасных операций с памятью, включая ASLR, канарейки стека, защищенные распределители и биты NX. АСЛР Рандомизация структуры адресного пространства — это технология, используемая в операционных системах, при ее использовании случайно изменяется расположение важных структур данных в адресном пространстве процесса, а именно образов исполняемых файлов, загружаемых библиотек, кучи и стека.
https://en.wikipedia.org/wiki/Address_space_layout_randomization
Обратите внимание на перевод
складывать канарейки «штамбовая канарейка» — название происходит от канарейки, которую шахтеры брали с собой для обнаружения повышенных концентраций рудничного газа.
Метод защиты от атаки переполнения буфера, при котором «канарейское значение» записывается перед адресом возврата в кадре стека.
Любая попытка перезаписать адрес с использованием переполнения буфера приведет к перезаписи канареечного значения и обнаружению переполнения буфера.
Обратите внимание на перевод защищенные распределители «Усиленные распределители» — это распределители памяти в LLVM, предназначенные для дальнейшего устранения уязвимостей, связанных с динамически выделяемой памятью.
Более подробную информацию см.
: https://llvm.org/docs/ScudoHardenedAllocator.html Обратите внимание на перевод бит NX NX bit — Атрибут (бит) NX-Bit (англ.
no Execute Bit) — бит запрета выполнения, добавляемый к страницам для реализации возможности предотвращения выполнения данных в виде кода.
Используется для предотвращения уязвимостей переполнения буфера.
Более подробную информацию см.
: https://en.wikipedia.org/wiki/NX_bit Обратите внимание на перевод Позже стал доступен CFI производственного уровня (мониторинг целостности потока управления).
Еще одна интересная недавняя разработка — идентификация указателя в ARMv8.3. В этой статье представлен обзор мер по снижению UB, связанных с безопасностью памяти.
Здесь показан серьезный недостаток ASan как средства защиты от UB:
Другими словами, ASan просто заставит злоумышленника вычислить другое смещение, чтобы испортить нужную область памяти.$ cat asan-defeat.c #include <stdio.h> #include <stdlib.h> #include <string.h> char a[128]; char b[128]; int main(int argc, char *argv[]) { strcpy(a + atoi(argv[1]), "owned."); printf("%s\n", b); return 0; } $ clang-4.0 -O asan-defeat.c $ .
/a.out 128 owned. $ clang-4.0 -O -fsanitize=address -fno-common asan-defeat.c $ .
/a.out 160 owned. $
(Спасибо Юрию Грибову за предложение использовать в ASan флаг -fno-common.) Чтобы смягчить это неопределенное поведение, должна быть фактическая проверка выхода за пределы, а не просто проверка того, что каждый доступ к памяти происходит в допустимом регионе.
Безопасность памяти здесь является золотым стандартом.
Хотя существует множество научных статей по безопасности памяти, а некоторые демонстрируют подходы с разумными издержками и хорошей совместимостью с существующим программным обеспечением, они не получили широкого распространения.
Checked Cis — очень крутой проект в этой области.
Заключение: Инструменты отладки подобных ошибок очень хороши.
Можно значительно смягчить этот тип UB, но для его полного устранения потребуется полная безопасность типов и памяти.
Нарушения безопасности временной памяти
Описание: Нарушением безопасности объектов временной памяти является любое использование области памяти после истечения срока ее жизни.Это включает в себя обращение к автоматическим переменным за пределами срока жизни этих переменных, использование после освобождения, использование висячего указателя для чтения или записи, двойное освобождение, что на практике может быть очень опасно, поскольку free() изменяет метаданные, которые обычно принадлежат создаваемому блоку.
освобожден.
Если блок уже освобожден, запись в эти данные может повредить данные, используемые для других целей, и в принципе может иметь те же последствия, что и любая другая недопустимая запись.
Отладка: ASan предназначен для обнаружения ошибок, связанных с использованием после освобождения, которые часто приводят к трудновоспроизводимому ошибочному поведению.
Он выполняет эту проверку, помещая освобожденные блоки в «карантин», предотвращая их немедленное повторное использование.
Для некоторых программ и входных данных это может увеличить потребление памяти и снизить локальность.
Пользователь может настроить размер карантина, чтобы сбалансировать ложные срабатывания и использование ресурсов.
ASan также может обнаруживать адреса автоматических переменных, которые вышли за пределы области действия этих переменных.
Идея состоит в том, чтобы превратить автоматические переменные в блоки, выделенные в куче, которые компилятор автоматически выделяет, когда выполнение входит в блок, и освобождает (во время помещения в карантин), когда выполнение покидает блок.
По умолчанию эта опция отключена, так как она еще больше требует памяти для программы.
Нарушение безопасности объектов временной памяти в приведенной ниже программе приводит к разнице в поведении между оптимизацией по умолчанию и -O2. ASan может обнаружить проблему в программе без оптимизации, но только если установлена опция define_stack_use_after_return и только если она не была скомпилирована с оптимизацией.
$ cat temporal.c
#include <stdio.h>
int *G;
int f(void) {
int l = 1;
int res = *G;
G = &l;
return res;
}
int main(void) {
int x = 2;
G = &x;
f();
printf("%d\n", f());
}
$ clang -Wall -fsanitize=address temporal.c
$ .
/a.out 1 $ ASAN_OPTIONS=detect_stack_use_after_return=1 .
/a.out ================================================================= ==5425==ERROR: AddressSanitizer: stack-use-after-return .
READ of size 4 at 0x0001035b6060 thread T0 ^C $ clang -Wall -fsanitize=address -O2 temporal.c $ .
/a.out 32767 $ ASAN_OPTIONS=detect_stack_use_after_return=1 .
/a.out 32767 $ clang -v Apple LLVM version 8.0.0 (clang-800.0.42.1) .
В некоторых других примерах дезинфицирующее средство не может обнаружить UB, который был удален оптимизатором, и, таким образом, является безопасным, поскольку удаленный код из UB не может оказать никакого влияния.
Но здесь не тот случай! В любом случае программа бессмысленна, но неоптимизированная программа ведет себя детерминированно, как если бы переменная x была объявлена статической, тогда как оптимизированная программа, в которой ASan не нашел ничего подозрительного, не ведет себя детерминированно и раскрывает внутреннее состояние, не предназначенное для этого.
видимый: $ clang -Wall -O2 temporal.c
$ .
/a.out 1620344886 $ .
/a.out 1734516790 $ .
/a.out
1777709110
Смягчение: Как обсуждалось выше, ASan не предназначен для защиты от уязвимостей, но доступны различные защищенные распределители, которые используют одну и ту же стратегию карантина для закрытия уязвимостей с использованием после освобождения.
Заключение: Используйте ASan (вместе с «ASAN_OPTIONS=detect_stack_use_after_return=1» для тестирования в небольших случаях).
На разных уровнях оптимизации могут быть обнаружены ошибки, которые не будут обнаружены на других уровнях.
Целочисленное переполнение
Описание: Целочисленной защиты от переполнения не существует, но переполнение может быть в обоих направлениях.Переполнение целого числа со знаком - это UB, которое включает в себя INT_MIN/-1, INT_MIN %-1, минус INT_MIN, сдвиги отрицательных чисел, сдвиг числа влево на единицу после знакового бита, а также (иногда) сдвиг числа влево с один в знаковом бите.
Деление на ноль и сдвиг на величину, превышающую разрядность числа, — это UB как для знаковых, так и для беззнаковых чисел.
Также см: Понимание целочисленного переполнения в C/C++ Отладка: UBSan — очень хороший инструмент для поиска UB, связанных с целыми числами.
Поскольку UBSan работает на уровне источника, он очень надежен.
Есть некоторые странности в расчете времени компиляции, например, некоторая программа может поймать исключение, если она скомпилирована как C++11, но не при компиляции в C11. Мы думаем, что это соответствует стандартам, но не вдавались в подробности.
У GCC есть своя версия UBSan, но ей нельзя доверять на 100%, она сворачивает константы перед прохождением инструмента.
Смягчение: UBsan в «режиме перехвата» (когда UB перехватывается, процесс завершается без диагностических выходных данных) может использоваться для смягчения UB. Это эффективно и не добавляет уязвимостей.
Частью использования UBsan в Android является смягчение последствий этого типа UB. Хотя переполнение целого числа по сути является логической ошибкой, в C и C++ такие ошибки особенно опасны, поскольку могут привести к нарушениям безопасности памяти.
В языках с безопасным доступом к памяти они гораздо менее опасны.
Заключение: Целочисленный UB поймать не очень сложно, для этого достаточно UBsan. Проблема в том, что ослабление целочисленного UB приводит к избыточности.
Например, из-за этого SPEC CPU 2006 работает на 30% медленнее.
Здесь есть много возможностей для улучшения: как устранить проверки переполнения там, где они не могут навредить, так и сделать остальные проверки менее обременительными для оптимизатора цикла.
Это должен сделать кто-то, обладающий достаточными ресурсами.
Строгие нарушения псевдонимов
Описание: Правила «строгого псевдонимов» в стандартах C и C++ позволяют компилятору предполагать, что если два указателя относятся к разным типам, они не указывают на один и тот же объект. Это позволяет провести большую оптимизацию, но рискует сломать программы с более гибким взглядом на вещи (что составляет примерно 100% больших программ на C и C++).Более подробный обзор см.
в разделах 1–3 этой статьи ( будет опубликовано в следующей части.
ок.
перевод ).
Отладка: Текущее состояние средств отладки нарушений «строгого псевдонимов» является слабым.
В некоторых простых случаях компиляторы выдают предупреждения, но эти предупреждения очень ненадежны.
libcrunch предупреждает, что указатель был преобразован в тип «указатель на что-то», хотя на самом деле он указывает на что-то другое.
Это позволяет выполнять преобразования типов через указатель на void, но перехватывает недопустимые преобразования указателей, которые также относятся к этому типу UB. Благодаря стандарту C и тому, как компиляторы C интерпретируют то, что они могут сделать с помощью оптимизации TBAA (анализ псевдонимов на основе типов), libcrunch не является ни надежным (он не улавливает некоторые нарушения, возникающие при выполнении программы), ни полным (он предупреждает о преобразовании указателя, если оно выглядит подозрительно, но не нарушает стандарт).
Смягчение: Все просто: вы передаете компилятору флаг (-fno-strict-aliasing), и он отключает оптимизацию, основанную на строгом псевдониме.
В результате компилятор опирается на старую добрую модель памяти, где могут выполняться более или менее произвольные преобразования между типами указателей, а результирующий код ведет себя ожидаемым образом.
Из «большой тройки» этому UB подвержены только GCC и LLVM; MSVC не реализует этот класс оптимизаций.
Заключение: Код, чувствительный к этому UB, требует тщательной проверки: конвертировать указатели во что-либо кроме char * всегда подозрительно и опасно.
В качестве альтернативы вы можете просто отключить оптимизацию TBAA с помощью флага и убедиться, что никто не компилирует код без использования этого флага.
Нарушения выравнивания
Описание: Процессоры RISC имеют тенденцию отключать доступ к памяти по невыровненным адресам.С другой стороны, программы C и C++, использующие невыровненный доступ, имеют UB независимо от целевой архитектуры.
Исторически мы это упускали из виду, во-первых, потому что x86/x64 поддерживают невыровненный доступ, а во-вторых, потому что компиляторы еще не использовали этот UB для оптимизации.
Но даже в этом случае существует отличная статья , который объясняет, как компилятор может сломать невыровненный код на x64. Код в статье нарушает строгое псевдонимирование, помимо нарушения выравнивания, и аварийно завершает работу (проверено на GCC 7.1.0 в OS X), несмотря на флаг -fno-strict-aliasing. Отладка: UBSan может обнаруживать нарушения выравнивания.
Смягчение: неизвестный Заключение: используйте UBSan
Циклы, которые не выполняют ввод-вывод и не завершаются
Описание: Циклы в коде C или C++, которые не выполняют ввод-вывод или завершаются, не определены и могут быть завершены компилятором произвольно.См.
Отладка: нет инструментов Смягчение: Нет, кроме как избегать чрезмерной оптимизации компиляторов.
Заключение: Этот УБ не является практической проблемой (даже если некоторым из нас это неприятно).
Гонки данных
Описание: Гонки данных возникают, когда к фрагменту памяти обращаются более чем один поток, и хотя бы один из них доступен для записи, и доступ не синхронизируется с помощью таких механизмов, как блокировки.Гонки данных приводят к UB в современных версиях C и C++ (они не имеют смысла в старых версиях, поскольку эти стандарты не охватывали многопоточный код).
Обратите внимание на перевод Здесь я не согласен с автором, поскольку многопоточный код можно запускать с использованием API операционной системы, такого как POSIX Threads, и это можно сделать в любой версии C и C++, какой бы старой она ни была.
Кроме того, код, обрабатывающий прерывания в микроконтроллере, может привести к аналогичным эффектам при совместном доступе к данным с основным программным циклом.
Это также не зависит от года выпуска стандарта C и C++.
Обратите внимание на перевод Описание: Tsan — отличный детектор гонок динамической памяти.
Существуют и другие подобные инструменты, например плагин Helgrind для Valgrind, но в последнее время мы ими не пользуемся.
Использование динамических детекторов гонок осложняется тем, что условия гонки очень сложно вызвать, и что хуже всего, их срабатывание зависит от количества ядер, алгоритма планировщика потоков, того, что еще запущено на тестовой машине, фаз луны и т. д. Смягчение: не создавайте темы Заключение: Для этого конкретного UB есть хорошая идея: если вам не нравится блокировать объекты, не используйте код, который выполняется параллельно, а вместо этого используйте атомарные действия.
Непоследовательные модификации
Описание: В C «точка последовательности» ограничивает то, как рано или поздно вступит в силу выражение побочного эффекта, такое как x++.В C++ есть другая, но более или менее эквивалентная формулировка этого правила.
В обоих языках модификации, нарушающие точки последовательности, приводят к UB. Отладка: Некоторые компиляторы выдают предупреждение при очевидных нарушениях следующих правил: $ cat unsequenced2.c
int a;
int foo(void) {
return a++ - a++;
}
$ clang -c unsequenced2.c
unsequenced2.c:4:11: warning: multiple unsequenced modifications to 'a' [-Wunsequenced]
return a++ - a++;
^ ~~
1 warning generated.
$ gcc-7 -c unsequenced2.c -Wall
unsequenced2.c: In function 'foo':
unsequenced2.c:4:11: warning: operation on 'a' may be undefined [-Wsequence-point]
return a++ - a++;
~^~
Однако небольшое косвенное нарушение не вызывает предупреждений: $ cat unsequenced.c
#include <stdio.h>
int main(void) {
int z = 0, *p = &z;
*p += z++;
printf("%d\n", z);
return 0;
}
$ gcc-4.8 -Wall unsequenced.c ; .
/a.out 0 $ gcc-7 -Wall unsequenced.c ; .
/a.out 1 $ clang -Wall unsequenced.c ; .
/a.out
1
Смягчение: Неизвестно, но определить порядок, в котором будут выполняться побочные эффекты, почти тривиально.
Язык Java является примером того, как это делается.
Нам было трудно поверить, что такое ограничение помешает любому современному оптимизирующему компилятору.
Если комитет по стандартизации всем сердцем поверит, что это не так, разработчикам компиляторов придется следовать правилам.
В идеале, все основные составители должны делать то же самое в таких случаях.
Заключение: При некоторой практике нетрудно обнаружить потенциальные нарушения точек последовательности во время проверки кода.
Нам приходится беспокоиться, когда мы видим очень сложные выражения с побочными эффектами.
Такое случается в устаревшем коде, но посмотрите, оно все еще работает, так что, возможно, это не проблема.
Фактически, эта проблема должна быть исправлена в компиляторах.
Не-UB, связанный с нарушениями точек последовательности, является «неопределенно упорядоченным», при котором операторы могут выполняться в порядке, указанном компилятором.
Примером может служить порядок вызова двух функций при вычислении f(a(), b()).
Этот порядок также необходимо определить.
Например, слева направо.
Никакой потери производительности здесь не будет, если не считать совершенно бредовых ситуаций.
Продолжение следует. Теги: #неопределённое поведение #llvm #программирование #C++ #компиляторы #C++
-
Compaq Lte Elite 486: Первый Ноутбук
19 Oct, 24 -
Поиск Находит Личные Темы
19 Oct, 24 -
Интересные Международные События В Марте
19 Oct, 24 -
Мы Сделали Это Ради Лулзов
19 Oct, 24 -
Grabduck: Как Мы Делаем Статьи Из Закладок
19 Oct, 24