Новые оптимизации с использованием неопределенного поведения в gcc 4.9.0

daac21c1959343eea2bd68ee5ed63d54.jpgОтличные новости ждут пользователей gcc при переходе на версию 4.9.0 — новые оптимизации с использованием неопределенного поведения могут «сломать» (на самом деле — доломать) существующий код, который, например, сравнивает с нулем указатели, ранее переданные в memmove () и ряд других функций стандартной библиотеки.Например, утверждается, что в таком коде:

int wtf (int* to, int* from, size_t count) { memmove (to, from, count); if (from!= 0) return *from; return 0; } новый gcc может удалить сравнение указателя с нулем и в результате вызов wtf (0, 0, 0) будет приводить к разыменованию нулевого указателя (и аварийному завершению программы).На первый взгляд, выглядит так, как будто компилятор целенаправленно сломал программу. Отдельные читатели уже полны возмущения (особенно «невразумительным» примером кода) и спешат в комментарии, чтобы его высказать. Пока рано. Сначала стоит посмотреть, что сказано по этому поводу в Стандарте C99.В разделе 7.21 описаны «строковые функции», объявляемые в заголовке string.h В 7.21.½ сказано следующее: «если в описании конкретной функции в данном подразделе не сказано иное, то указатели, передаваемые в качестве аргументов при вызове функции, должны иметь допустимые значения, соответствующие требованиям 7.1.4». Функция memmove () описана в 7.21.2.2, т.е. относится к «строковым функциям», в ее описании ничего не сказано о допустимости нулевых указателей на входе.

TL; DR; Смотрим в 7.1.4, там сказано «Если аргумент функции имеет недопустимое значение (такое как <…>, нулевой указатель) <…>, то поведение не определено».

Таким образом, передача нулевых указателей в memmove () приводит к неопределенному поведению, даже если значение третьего параметра (число байт) равно нулю. Компилятор делает из этого следующий вывод: если указатель передается в memmove (), можно считать, что он ненулевой, и оптимизировать остальной код соответствующим образом. Эта идея подробно и с примерами объяснена вот в этой замечательной публикации.

Попробуем это воспроизвести на MinGW с gcc 4.9.0

#include #include

void magic1(char* to, char* from, size_t count) { memmove (to, from, count); if (from == 0) { printf («null\n»); } else { printf («not null\n»); } }

int main () { magic1(0, 0, 0); return 0; } Компилируем: gcc magic.c -O2 -o magic.exe

Запускаем полученный исполняемый файл — получаем в выдаче «not null».Для сравнения, если вызов memmove () перенести ниже:

void magic2(char* to, char* from, size_t count) { if (from == 0) { printf («null\n»); } else { printf («not null\n»); } memmove (to, from, count); } то выдача будет ожидаемая: «null» — с новой оптимизацией работа программы может меняться в зависимости от того, стоит вызов memmove () выше или ниже сравнения указателя с нулем.Это еще не все. Работа программы может измениться при замене библиотечной функции на «велосипед» или наоборот:

void mymemcpy (char* to, char* from, size_t count) { while (count > 0) { *to++ = *from++; count--; } }

void magic3(char* to, char* from, size_t count) { mymemcpy (to, from, count); if (from == 0) { printf («null\n»); } else { printf («not null\n»); } } При вызове magic3(0, 0, 0) программа выдает «null». В случае использования библиотечной memcpy () выдается «not null».В описании настроек оптимизации описанная выше в явном виде не упоминается. Самой похожей выглядит -fdelete-null-pointer-checks, и действительно с настройкой -fno-delete-null-pointer-checks эта оптимизация отключается вместе с рядом других оптимизаций, полагающих, что ранее разыменованный указатель нет смысла сравнивать с нулем. Заметим, что в описанной выше оптимизации речь не идет о разыменовании указателя, а только о передаче указателя в качестве параметра строковых функций.

Вопреки распространенному мнению, по-настоящему переносимый код писать не так легко, как хотелось бы. Использовать size_t для индексирования массивов недостаточно.

Дмитрий Мещеряков, департамент продуктов для разработчиков

© Habrahabr.ru