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

в 9:36, , рубрики: gcc, GCC 4.9.0, оптимизация кода, переносимость

Новые оптимизации с использованием неопределенного поведения в gcc 4.9.0Отличные новости ждут пользователей 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.1/2 сказано следующее: «если в описании конкретной функции в данном подразделе не сказано иное, то указатели, передаваемые в качестве аргументов при вызове функции, должны иметь допустимые значения, соответствующие требованиям 7.1.4». Функция memmove() описана в 7.21.2.2, т.е. относится к «строковым функциям», в ее описании ничего не сказано о допустимости нулевых указателей на входе.

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

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

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

#include <stdio.h>
#include <string.h>

void magic1( char* to, char* from, size_t count )
{
    memmove( to, from, count );
    if( from == 0 ) {
       printf( "nulln" );
    } else {
       printf( "not nulln" );
    }
}

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( "nulln" );
    } else {
        printf( "not nulln" );
    }
    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( "nulln" );
    } else {
        printf( "not nulln" );
    }
}

При вызове magic3( 0, 0, 0 ) программа выдает «null». В случае использования библиотечной memcpy() выдается «not null».

В описании настроек оптимизации описанная выше в явном виде не упоминается. Самой похожей выглядит -fdelete-null-pointer-checks, и действительно с настройкой -fno-delete-null-pointer-checks эта оптимизация отключается вместе с рядом других оптимизаций, полагающих, что ранее разыменованный указатель нет смысла сравнивать с нулем. Заметим, что в описанной выше оптимизации речь не идет о разыменовании указателя, а только о передаче указателя в качестве параметра строковых функций.

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

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

Автор: DmitryMe

Источник

Поделиться

* - обязательные к заполнению поля