Во время обсуждения, которое я провел с парой коллег, на днях я собрал фрагмент кода на C ++, чтобы проиллюстрировать нарушение доступа к памяти.
В настоящее время я нахожусь в процессе медленного возвращения к C ++ после долгого периода почти полного использования языков со сборкой мусора, и, я думаю, моя потеря касания показывает, так как я был довольно озадачен поведением, которое продемонстрировала моя короткая программа.
Данный код таков:
#include <iostream>
using std::cout;
using std::endl;
struct A
{
int value;
};
void f()
{
A* pa; // Uninitialized pointer
cout<< pa << endl;
pa->value = 42; // Writing via an uninitialized pointer
}
int main(int argc, char** argv)
{
f();
cout<< "Returned to main()" << endl;
return 0;
}
Я скомпилировал его с GCC 4.9.2 на Ubuntu 15.04 с -O2
установлен флаг компилятора. Мои ожидания при запуске заключались в том, что произойдет сбой, когда строка, обозначенная моим комментарием как «запись через неинициализированный указатель», будет выполнена.
Однако, вопреки моим ожиданиям, программа успешно завершилась до конца и привела к следующему результату:
0
Returned to main()
Я перекомпилировал код с -O0
флаг (чтобы отключить все оптимизации) и снова запустил программу. На этот раз поведение было таким, как я ожидал:
0
Segmentation fault
(Что ж, почти: Я не ожидал, что указатель будет инициализирован в 0.) Исходя из этого наблюдения, я предполагаю, что при компиляции с -O2
установить, роковая инструкция была оптимизирована. Это имеет смысл, так как никакой дальнейший код не обращается к pa->value
после того, как он установлен ошибочной строкой, то, по-видимому, компилятор определил, что его удаление не изменит наблюдаемое поведение программы.
Я воспроизводил это несколько раз, и каждый раз программа вылетала при компиляции без оптимизации и чудесным образом работала при компиляции с -O2
,
Моя гипотеза была подтверждена, когда я добавил строку, которая выводит pa->value
до конца f()
тело:
cout<< pa->value << endl;
Как и ожидалось, с этой линией программа постоянно аварийно завершает работу, независимо от уровня оптимизации, с которым она была скомпилирована.
Это все имеет смысл, если мои предположения пока верны.
Однако, где мое понимание несколько нарушается, это в случае, когда я перемещаю код из тела f()
прямо к main()
, вот так:
int main(int argc, char** argv)
{
A* pa;
cout<< pa << endl;
pa->value = 42;
cout<< pa->value << endl;
return 0;
}
С отключенной оптимизацией эта программа вылетает, как и ожидалось. С -O2
однако программа успешно работает до конца и выдает следующий вывод:
0
42
И это не имеет смысла для меня.
Этот ответ упоминает «разыменование указателя, который еще не был точно инициализирован», что я и делаю, как один из источников неопределенного поведения в C ++.
Итак, является ли это различие в том, как оптимизация влияет на код в main()
по сравнению с кодом в f()
полностью объясняется тем фактом, что моя программа содержит UB, и, таким образом, компилятор технически свободен в том, чтобы «сходить с ума», или есть какая-то принципиальная разница, о которой я не знаю, между способом кода в main()
оптимизирован, по сравнению с кодом в других подпрограммах?
Написание неизвестных указателей всегда было чем-то, что могло иметь неизвестные последствия. Что противно, так это современная модная философия, которая предполагает, что компиляторы должны предполагать, что программы никогда не получат входные данные, вызывающие UB, и, таким образом, должны оптимизировать любой код, который будет проверять такие входные данные, если такие тесты не будут препятствовать возникновению UB.
Так, например, дано:
uint32_t hey(uint16_t x, uint16_t y)
{
if (x < 60000)
launch_missiles();
else
return x*y;
}
void wow(uint16_t x)
{
return hey(x,40000);
}
32-битный компилятор может законно заменить wow
с безусловным вызовом
launch_missiles
без учета стоимости x
, поскольку x
«не может быть» больше 53687 (любое значение, превышающее это, может привести к переполнению вычисления x * y. Хотя авторы C89 отметили, что большинство компиляторов той эпохи вычислило бы правильный результат в ситуации, подобной вышеизложенное, поскольку Стандарт не предъявляет никаких требований к компиляторам, гиперсовременная философия считает, что для компиляторов «более эффективно» предполагать, что программы никогда не получат входные данные, которые потребовали бы зависимости от таких вещей.
Ваша программа имеет неопределенное поведение. Это означает, что все может случиться. Программа вообще не охвачена стандартом C ++. Вы не должны идти с какими-либо ожиданиями.
Часто говорят, что неопределенное поведение может «запустить ракеты» или «заставить демонов вылететь из носа», чтобы усилить эту точку зрения. Последнее более надумано, но первое выполнимо, представьте, что ваш код находится на месте запуска ядерной программы, и дикий указатель записывает часть памяти, которая начинает глобальную термоядерную войну.