#cpp #неопределенное_поведение
Обычно стараюсь все узнавать сам, но в последнее время начал мучать такой вопрос: "Может ли случиться такое, что из-за различного вида ошибок, допущенных программистом, система может, грубо говоря, нагнуться?" Собственно, вопрос возник при чтении книги Джесса Либерти "Освой с++ за 21 день", в которой сказано, что блуждающие указатели могут повести себя по-разному, могут даже удалить файлы. И особенно сильно удивило то, что при присвоении некого значения массиву, который не содержит такого элемента, например, массив A содержит 5 элементов, и мы присваиваем значение 6 элементу, цитирую: Ни в коем случае не запускайте эту программу у себя на компьютере, это может привести к поломке системы. Правда ли это?
Ответы
Ответ 1
Система может "нагнуться". Но если Вы пишите обычное приложение, не запускаете его с под администратора/рута, то "уложить" систему достаточно сложно - современные системы хорошо сопротивляются пользователю. И особенно сильно удивило то что при присвоении некого значения массиву который не содержит такого элемента, например, массив A содержит 5 элементов и мы присваиваем значение 6 элементу, цитирую " Ни в коем случае не запускайте эту программу у себя на компьютере это может привести к поломке системы". Правда ли это? О такой ошибке почти все компиляторы знают. И ничего плохого не случиться - компилятор выделит массив не на 5 элементов, а обычно как минимум на 6. А потом просто проверит, а не записал кто то за пределами массива. И если такое произошло - Вы об этом узнаете явно. (студия может явно ругнуться в консоль). А вот если писать не в 6 элемент, а в 1000...000, вот тут может быть несколько ситуаций. случайно перетрутся данные, которые принадлежат Вашей программе. Другие функции будут "удивлены". Одна из самых типичных ситуаций. перетрутся данные в "паддингах" - пустых местах между переменными. Ничего плохого не будет. случайные данные попытаются записать в защищенную область память - будет исключение и программа скорее всего закрешится. То есть, максимум, что может случиться, это либо программа будет вести себя странно, либо просто упадет. Могут ли "блуждающие указатели" удалить файл? могут. Но обычно эта логика в программе уже есть. Если сильно боитесь эти ужасов, установите виртуальную машину и в ней все делайте. В худшем случае просто восстановите виртуальную машину. Выйти за пределы виртуальной машины ещё нужно постараться.Ответ 2
Дело в том, что современные компиляторы активно пользуются тем, что undefined behavour (например, доступ к массиву за пределами его размера) не имеет права случаться. Так что на современных компиляторах вам не нужно дожидаться того, что случайный указатель затрёт случайные данные, проблема может случиться намного раньше, и результаты будут намного более странными. Вот частичный перевод отличной статьи Реймонда Чена из блога The Old New Thing, который говорит именно об этом. Надеюсь, не нарушил ничьи права. Неопределённое поведение может привести к путешествию во времени (кроме прочих странных вещей, но путешествие во времени прикольнее всего) Языки C и C++ знамениты огромной областью на своей карте, отмеченной надписью «неизведанные, опасные земли» — или, более формально, неопределённым поведением. Когда в программе происходит неопределённое поведение, возможно что угодно. Например, переменная может быть одновременно true и false. [...] Рассмотрим на секундочку вот такую функцию: int table[4]; bool exists_in_table(int v) { for (int i = 0; i <= 4; i++) { if (table[i] == v) return true; } return false; } Вы спросите, ну и причём тут путешествие во времени? Подождите, нетерпеливые. Для начала, вы видите ошибку на 1 в управлении циклом. В результате функция читает элемент за концом массива table перед тем, как завершить цикл. Классический компилятор не сильно заморачивался бы: он бы тупо сгенерировал код, читающий элемент за границей массива (несмотря на то, что по правилам языка это запрещено), и вернул бы true, если бы в памяти за массивом случайно оказалось бы подходящее значение. С другой стороны, современный, пост-классический компилятор вполне мог бы провести следующий анализ: На первых четырёх итерациях цикла функция может вернуть true. Если i == 4, в коде происходит неопределённое поведение (UB). Поскольку UB даёт мне право делать что угодно, я могу просто проигнорировать этот случай, и предположить, что i никогда не равно 4. (Если предположение окажется неверным, произойдёт что-то непредвиденное, но это нормально, потому что наличие UB даёт мне право делать непредсказуемые вещи.) Случай, когда i == 5, невозможен, поскольку чтобы попасть туда, нужно сначала пройти через i == 4, который, как нам разрешено считать , не может произойти. Итак! Все законные пути случаи пробега функции возвращают true. В результате, пост-классический компилятор может оптимизировать функцию до bool exists_in_table(int v) { return true; } Окей, это уже выглядит диковато. Функция оптимизировалась в практически ничего из-за UB. Заметьте, что даже если значения нет в table (включая даже незаконный 5-ый элемент массива), функция всё равно вернёт true. Дальше — больше. Мы можем протащить это пост-классическое поведение ещё на один шаг. Раз компилятор имеет право предположить, что UB никогда не случается (потому что если оно случается, компилятор имеет право делать что угодно), он может использовать его для оптимизации. int value_or_fallback(int *p) { return p ? *p : 42; } Эта функция получает указатель на int и возвращает либо значение по указателю, или 42, если указатель нулевой. Пока ничего особенного. А давайте добавим строчку для отладки. int value_or_fallback(int *p) { printf("The value of *p is %d\n", *p); // <-- return p ? *p : 42; } Добавленная строчка содержит баг. Оно разыменовывает указатель без проверки на NULL. Этот небольшой баг имеет, однако, большие последствия. Пост-классический компилятор соптимизирует функцию в такое: int value_or_fallback(int *p) { printf("The value of *p is %d\n", *p); return *p; // <-- !!! } Он видит, что проверка на NULL не нужна: если указатель был NULL, то printf вызывало бы UB, так что случае, когда p == NULL, компилятор может делать всё, что ему хочется (например, притвориться, что p != NULL). Окей, ну это в принципе не слишком-то и удивительно. Вполне возможно, что такую оптимизацию вы ожидаете от хорошего компилятора. (Например, если тернарный оператор запрятан в макрос, вы наверное ожидаете, что компилятор уберёт проверку, которая гарантировано не выполняется.) Но пост-классический компилятор теперь может использовать эту функцию для путешествия во времени. void unwitting(bool door_is_open) // unwitting = не подозревая о проблемах { if (door_is_open) { walk_on_in(); } else { ring_bell(); // подождать, пока дверь откроется, со значением по умолчанию fallback = value_or_fallback(nullptr); wait_for_door_to_open(fallback); } } Пост-классический компилятор может оптимизировать всю эту функцию до такого: void unwitting(bool door_is_open) { walk_on_in(); } Чё?? Компилятор заметил, что вызов value_or_fallback(nullptr) приводит к UB по всем возможным путям выполнения. Распространяя этот анализ назад, он видит, что если door_is_open == false, в ветке else обязательно происходит UB. Раз так, то вся ветка может считаться невозможной. А вот и путешествие во времени подоспело: void keep_checking_door() { for (;;) { printf("Is the door open? "); // выясним, открыта ли дверь fflush(stdout); char response; if (scanf("%c", &response) != 1) return; bool door_is_open = response == 'Y'; unwitting(door_is_open); // и передадим ответ в функцию } } Пост-классический компилятор может использовать знание о том, что «если door_is_open == false, происходит UB», и переписать эту функцию так: void keep_checking_door() { for (;;) { printf("Is the door open? "); fflush(stdout); char response; if (scanf("%c", &response) != 1) return; bool door_is_open = response == 'Y'; if (!door_is_open) abort(); walk_on_in(); } } Обратите внимание: неоптимизированный код звонил в звонок перед тем, как вылететь. Оптимизированная функция пропускает звонок, и вылетает немедленно. Можно сказать, что компилятор отправился в прошлое, и отменил прозвеневший звонок.
Комментариев нет:
Отправить комментарий