Оптимизация компилятора может привести к ошибкам или нежелательному поведению. Вот почему вы можете отключить их.
Один пример: компилятор может оптимизировать доступ для чтения / записи к области памяти, делая такие вещи, как устранение дублирующих чтений или дублирующих записей или переупорядочение определенных операций. Если рассматриваемая область памяти используется только одним потоком и фактически является памятью, это может быть хорошо. Но если ячейка памяти представляет собой регистр ввода-вывода аппаратного устройства, то переупорядочение или устранение записей может быть совершенно неверным. В этой ситуации вам обычно приходится писать код, зная, что компилятор может его «оптимизировать», и, следовательно, зная, что наивный подход не работает.
Обновление: Как отметил Адам Робинсон в комментарии, сценарий, который я описал выше, является скорее ошибкой программирования, чем ошибкой оптимизатора. Но момент, который я пытался проиллюстрировать, заключается в том, что некоторые программы, которые в противном случае являются правильными, в сочетании с некоторыми оптимизациями, которые в противном случае работают должным образом, могут вносить ошибки в программу, когда они объединяются вместе. В некоторых случаях спецификация языка гласит: «Вы должны действовать таким образом, потому что могут произойти такие оптимизации, и ваша программа потерпит неудачу», и в этом случае это ошибка в коде. Но иногда компилятор имеет (обычно необязательную) функцию оптимизации, которая может генерировать неправильный код, потому что компилятор слишком старается оптимизировать код или не может обнаружить, что оптимизация неуместна. В этом случае программист должен знать, когда безопасно включить данную оптимизацию.
Другой пример:
В ядре linux была ошибка , когда потенциально нулевой указатель разыменовывался до того, как проверка на этот указатель была нулевой. Однако в некоторых случаях было возможно сопоставить память с нулевым адресом, что позволило разыменованию успешно завершиться. Компилятор, заметив, что указатель разыменован, предположил, что он не может быть NULL, затем удалил тест NULL и весь код в этой ветви. Это привело к появлению уязвимости в коде , так как функция продолжит использовать недопустимый указатель, содержащий данные, предоставленные злоумышленником. В тех случаях, когда указатель был законно равен нулю, а память не была сопоставлена с нулевым адресом, ядро все равно будет OOPS, как и раньше. Таким образом, до оптимизации код содержал одну ошибку; после того, как он содержал два, и один из них позволил использовать локальный корневой эксплойт.
CERT имеет презентацию под названием «Опасная оптимизация и потеря причинности» Роберта С. Сикорда, в которой перечисляется множество оптимизаций, которые вводят (или выявляют) ошибки в программах. В нем рассматриваются различные виды возможных оптимизаций: от «делать то, что делает аппаратное обеспечение» до «перехватывать все возможные неопределенные действия» и «делать все, что не запрещено».
Некоторые примеры кода, которые прекрасно работают, пока агрессивный оптимизирующий компилятор не получит в свои руки:
Проверка на переполнение
// fails because the overflow test gets removed
if (ptr + len < ptr || ptr + len > max) return EINVAL;
Использование арифметики переполнения вообще:
// The compiler optimizes this to an infinite loop
for (i = 1; i > 0; i += i) ++j;
Очистка памяти от конфиденциальной информации:
// the compiler can remove these "useless writes"
memset(password_buffer, 0, sizeof(password_buffer));
Проблема здесь в том, что на протяжении десятилетий компиляторы были менее агрессивны в оптимизации, и поэтому поколения программистов на С изучают и понимают такие вещи, как добавление дополнений в фиксированный размер и их переполнение. Затем разработчики компиляторов вносят поправки в стандарт языка C, и тонкие правила меняются, несмотря на то, что аппаратное обеспечение не меняется. Спецификация языка C является контрактом между разработчиками и компиляторами, но условия соглашения могут изменяться с течением времени, и не все понимают каждую деталь или соглашаются с тем, что детали даже разумны.
Именно поэтому большинство компиляторов предлагают флаги для отключения (или включения) оптимизаций. Ваша программа написана с пониманием того, что целые числа могут переполниться? Затем вы должны отключить оптимизации переполнения, потому что они могут вносить ошибки. Ваша программа строго избегает наложения указателей? Затем вы можете включить оптимизации, которые предполагают, что указатели никогда не являются псевдонимами. Ваша программа пытается очистить память, чтобы избежать утечки информации? О, в этом случае вам не повезло: вам нужно либо отключить удаление мертвого кода, либо вам нужно заранее знать, что ваш компилятор собирается устранить ваш "мертвый" код и выполнить некоторые действия для этого.