LINUX.ORG.RU

О неопределённом поведении и багах оптимизатора clang на примере разбора цикла статей Криса Латтнера, разработчика clang.

 , , , ,


11

10

про ud2 - лень читать комменты - но кто-то должен был оставить вот этот цикл из трех постов:

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html

stevejobs, спасибо за ссылки. С интересом почитал. Ответ получился довольно длинным, даже движок форума не хочет принимать его в таком виде в качестве простого камента, поэтому я решил, что он тянет на отдельную тему. Тем более, что здесь разбирается не какой-то мелкий баг/фича clang'а, а общий подход к созданию оптимизатора, основанный на изложении и анализе статей одного из разработчиков этого компилятора Криса Латтнера (Chris Lattner). Ну и ещё мне пришлось полностью переписать один из его примеров, чтоб он начал работать и иллюстрировать излагаемые им идеи. Если хочешь, можешь послать ему код, чтоб вставил в свою статью вместо своего, не рабочего. Я не возражаю.

А для тех, кто не в теме, это продолжение темы Вызов никогда не вызываемой функции.

Основная мысль автора цикла статей, как я понял, выражена ближе к концу 3-ей статьи в короткой фразе:

c) is a lot of work to implement.

Т. е. автор, по сути, соглашается, что они реализовали какую-то дичь с этими оптимизациями, объясняя некоторые причины: что де оптимизатор не знает, что на входе получает компилятор и даже не знает, что получает он сам на предыдущем проходе, ну и некоторые другие причины. А в конце говорит: но переделывать эту неудачную архитектуру нам лень, поэтому пользуйтесь тем, что есть. Что ж, fixed. В любом случае, статьи полезны, т. к. проливают свет на реальное (и довольно печальное) положение дел, связанных с оптимизациями в компиляторе clang.

Теперь разберу примеры из статей.

1. В первом примере автор пытается показать, почему к указателю на int нельзя обращаться как к указателю на float:

float *P;
 void zero_array() {
   int i;
   for (i = 0; i < 10000; ++i)
     P[i] = 0.0f;
 }

int main() {
  P = (float*)&P;  // cast causes TBAA violation in zero_array.
  zero_array();
}

Этот код — полная дичь. Он будет вылетать и с оптимизациями, и без них, потому что в переменную P (которая по умолчанию инициализируется 0), записывается её собственный адрес, а дальше, начиная с этого адреса, записываются ещё 40000 байт, которые непременно выдут за границы выделенной памяти.

Я немного переделал этот пример, чтоб он заработал, добавив сразу после P массив достаточного размера, в который и будут записываться числа (тоже undefined, но по факту работает с обоими компиляторами — clang и gcc), и заменив запись 0 на запись адреса &P+1, для чего ввёл объединение ufp, т. к. иначе на первой же итерации P будет указывать на 0, после чего на 2-й итерации произойдёт сегфолт. Ну и ещё добавил printf'ы для вывода информации и return, как того требует gcc и стандарт. Тут же отмечу, что мне пришлось слегка повозиться с unsigned *p, объявленным внутри функции print_array(). Сначала я сделал его глобальным, объявив до указателя float *P, и получил похожий на приведённый, но не совсем верный вывод: вместо ожидаемого 0x601268, 0x0, 0x0, 0x601268 программа без оптимизаций выдавала 0x601268, 0x0, 0x601268, 0x0. После установки watchpoint'а в дебагере выяснилось, что массив повторно модифицируется функцией print_array(). Просмотр адресов глобальных переменных &P и &p показал, что и clang, и gcc вставляют p после P и перед массивом arr, хотя в тексте программы она была объявлена первой. Видимо, компиляторы зачем-то сортируют переменные по типам и располагают указатели на float раньше указателей на unsigned. После переноса unsigned *p внутрь функции, всё стало работать, как и ожидалось. Вот мой рабочий (хоть и намеренно некорректный) вариант:

#include <stdio.h>

float *P;

float arr[10000];

union ufp
{
  float** p; float f;
} fp={&P+1};

void zero_array() {
   int i;
   for (i = 0; i < 10000; ++i)
     P[i] = fp.f;
 }

void print_array() {
   int i;
   unsigned *p;
   printf("&P==%p, P==%p\n", &P, P);
   for(i = -2; i < 10000; ++i)
    {
      p=(unsigned*)P;
      printf("&P[%i]==%p, P[%i]==%f (%p: 0x%X)\n", i, &P[i], i, P[i], p+i, *(p+i));
    }
}

int main() {
  P = (float*)&P;  // cast causes TBAA violation in zero_array.
  zero_array();
  //P=(float*)(&P+1); // restoring P for optimizer
  print_array();
  return 0;
}

При компиляции clang'ом без оптимизации:

clang -o zero_array zero_array.c

этот вариант выдаёт следующее:

&P==0x601260, P==0x601268
&P[-2]==0x601260, P[-2]==0.000000 (0x601260: 0x601268)
&P[-1]==0x601264, P[-1]==0.000000 (0x601264: 0x0)
&P[0]==0x601268, P[0]==0.000000 (0x601268: 0x0)
&P[1]==0x60126c, P[1]==0.000000 (0x60126c: 0x601268)
&P[2]==0x601270, P[2]==0.000000 (0x601270: 0x601268)
&P[3]==0x601274, P[3]==0.000000 (0x601274: 0x601268)
&P[4]==0x601278, P[4]==0.000000 (0x601278: 0x601268)
[skip]
&P[9994]==0x60ae90, P[9994]==0.000000 (0x60ae90: 0x601268)
&P[9995]==0x60ae94, P[9995]==0.000000 (0x60ae94: 0x601268)
&P[9996]==0x60ae98, P[9996]==0.000000 (0x60ae98: 0x601268)
&P[9997]==0x60ae9c, P[9997]==0.000000 (0x60ae9c: 0x601268)
&P[9998]==0x60aea0, P[9998]==0.000000 (0x60aea0: 0x601268)
&P[9999]==0x60aea4, P[9999]==0.000000 (0x60aea4: 0x601268)

и корректно завершается.

Если же включить оптимизацию:

clang -o zero_array -O2 zero_array.c

то получаем следующее:

$ ./zero_array
&P==0x601260, P==0x60126800601268
Ошибка сегментирования

Кстати, такой же результат будет, если откомпилировать эту программу компилятором gcc с включённой оптимизацией (там только адреса будут немного другими). Избавиться от этой ошибки можно 2 способами:

  1. Закомментировав вызов print_array() в функции main().

    Очевидно, что в этом случае никакого вывода мы не получим.

  2. Раскомментировав в main() строчку
    P=(float*)(&P+1); // restoring P for optimizer

    Тогда мы получим такой вывод:

    &P==0x601260, P==0x601268
    &P[-2]==0x601260, P[-2]==0.000000 (0x601260: 0x601268)
    &P[-1]==0x601264, P[-1]==0.000000 (0x601264: 0x0)
    &P[0]==0x601268, P[0]==0.000000 (0x601268: 0x601268)
    &P[1]==0x60126c, P[1]==0.000000 (0x60126c: 0x601268)
    &P[2]==0x601270, P[2]==0.000000 (0x601270: 0x601268)
    &P[3]==0x601274, P[3]==0.000000 (0x601274: 0x601268)
    &P[4]==0x601278, P[4]==0.000000 (0x601278: 0x601268)
    [skip]
    &P[9994]==0x60ae90, P[9994]==0.000000 (0x60ae90: 0x601268)
    &P[9995]==0x60ae94, P[9995]==0.000000 (0x60ae94: 0x601268)
    &P[9996]==0x60ae98, P[9996]==0.000000 (0x60ae98: 0x601268)
    &P[9997]==0x60ae9c, P[9997]==0.000000 (0x60ae9c: 0x601268)
    &P[9998]==0x60aea0, P[9998]==0.000000 (0x60aea0: 0x0)
    &P[9999]==0x60aea4, P[9999]==0.000000 (0x60aea4: 0x0)
    

Проблема тут очевидна: в цикле в P[i] записывается адрес &P+1. Но указатель P указывает на самого себя благодаря присвоению P = (float*)&P. Соответственно, элемент P[0] находится по тому же адресу, что и P. Когда при 1-й итерации цикла мы записываем туда адрес следующего элемента, указатель P меняется. В 64-битной ОС размер указателя равен 8 байтам, а размер float — 4, т. е. P у нас теперь указывает на начало arr. Дальше мы записываем 1-ый элемент от нового начала массива, т. е. по сути 3-й элемент, пропуская таким образом 2 элемента.

Когда же мы включаем оптимизатор (и в clang, и в gcc), он записывает все двойные слова подряд, начиная с 0-ого (в P[-1] у нас 0 потому, что мы перезаписали его после вызова zero_array(), чтобы программа не вылетела при вызове print_array()). Поэтому в первых 2 элементах у нас записано число 0x601268 (если представлять его как беззнаковое целое длиной в 4 байта), но 1-ые 2 элемента одновременно являются адресом, на который указывает P, т. е. адресом 0x0060126800601268 (0x601268 повторенное 2 раза). Если ничего не выводить, то всё тоже проходит успешно. Но как только мы вызываем print_array() (не модифицировав этот дикий адрес), программа сразу пытается отобразить содержимое не валидного адреса 0x601268, а того самого 0x0060126800601268, которого в нашем адресном пространстве просто нет. И получает сегфолт.

Почему printf отображает значения с плавающей точкой, которые в целочисленном виде выглядят как 0x601268, нулями, а не NAN, как по идее должно бы было быть, я не знаю. Видимо, это баг стандартной библиотеки (надо будет послать багрепорт, если никто мне не объяснит, что они правы).

Кстати, оба компилятора по неведомым мне причинам при оптимизации (а я пробовал разные уровни оптимизации) почему-то вместо записи поля fp.f memset'ом продолжают генерить цикл, только более короткий, чем без оптимизации (ассемблерные листинги я тут приводить не буду, кому интересно, могут сами откомпилировать с опцией -S). Хотя при записи константы 0 компилятор clang с вкючённой оптимизацией вместо цикла вызывает memset (gcc и в этом случае генерит цикл).

На всякий случай укажу версии использованных компиляторов:

$ clang --version
Debian clang version 3.5.0-10 (tags/RELEASE_350/final) (based on LLVM 3.5.0)
Target: x86_64-pc-linux-gnu
Thread model: posix

$ gcc --version
gcc (Debian 4.9.2-10) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
Это свободно распространяемое программное обеспечение. Условия копирования
приведены в исходных текстах. Без гарантии каких-либо качеств, включая 
коммерческую ценность и применимость для каких-либо целей.

К сожалению, мне пришлось писать рабочую программу за автора статьи, который не удосужился даже проверить тот код, который опубликовал (хотя там и без проверки видно, что код по-любому не рабочий, хоть оптимизируй, хоть нет). Впрочем, автор всё честно объяснил:

c) is a lot of work to implement.

Такие они, разработчики clang'а.

Но идею я понял: при заполнении массива 0 или другими значениями в цикле, опасно одновременно менять указатель на этот массив. Но при чём тут изначальное утверждение о том, что

It is undefined behavior to cast an int* to a float* and dereference it (accessing the «int» as if it were a «float»).

Как это утверждение иллюстрируется данным примером?

Я уже не говорю о том, что оптимизировать циклы в memset нет никакой необходимости, потому что программист и сам может это сделать, сократив не только получившийся бинарник, но и исходник. А если программисту до этого нет дела, то почему компилятору должно быть дело? Тем более, если программист сделал цикл намеренно, то компилятору совсем незачем это исправлять. Думаю, именно поэтому gcc и не сворачивает циклы в memset. Я уже не говорю о том, что если уж вы сворачиваете их в memset, то будьте последовательны. Почему при заполнении массива константой 0 вместо цикла clang вызывает memset, а при заполнении того же массива одной не меняющейся переменной длиной в 4 байта оставляет цикл? Грош цена такой оптимизации.

Вот заменить вызов memset на ассемблерную команду rep stos действительно было бы полезно, но почему-то ни clang, ни gcc этого не делают.

2. Во втором примере автор цикла показывает, как смертельный указатель может запутать оптимизатор clang так, что тот сгенерит очередную фигню вместо исполняемого кода. Вот пример смертельного кода из 2-й статьи:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

Очевидно, что если передать функции contains_null_check() NULL, то код будет непереносимым (undefined behavior). В защищённом режиме при попытке разыменования такого указателя произойдёт сегфолт. (UPD: практически аналогичная ошибка в ядре Linux 2.6.30 и 2.6.18 для Red Hat привела к серьёзной уязвимости, причём при разыменовании указателя система не падала.) Однако в реальном режиме такой код вполне законный. Более того, если мы рассматриваем язык си как системный язык, то в некоторых случаях без подобного кода в реальном режиме не обойтись. Что у нас лежит по адресу 0 в реальном режиме? — Указатель на обработчик 0-ого прерывания (деление на 0). А что если я хочу зарегистрировать свой обработчик? Для этого и существует неопределённое поведение: в одних системах оно работает так, а в других иначе. Но разработчики clang'а считают, что «неопределённое поведение» — это индульгенция на генерацию разного бреда вместо нормального кода.

Но вернёмся к статье. Автор описывает 2 варианта поведения оптимизатора.

  1. В первом варианте сначала проверяется избыточный код, а затем избыточные проверки. Выглядит это примерно так:
    void contains_null_check_after_DCE(int *P) {
      //int dead = *P;     // deleted by the optimizer.
      if (P == 0)
        return;
      *P = 4;
    }
    

    На этом этапе совершенно справедливо выпилили переменную dead, т. к. она нигде не используется.

    Далее идёт проверка избыточности проверок и делается правильный вывод о том, что проверка P на равенство 0 нужна. Она остаётся. Всё работает, как и задумывалось (и даже не падает в защищённом режиме на радость быдлокодерам).

  2. Во втором варианте оптимизатор сначала проверяет проверки программиста на избыточность, а затем выпиливает ненужные переменные:
    void contains_null_check_after_RNCE(int *P) {
      int dead = *P;
      if (false)  // P was dereferenced by this point, so it can't be null 
        return;
      *P = 4;
    }
    

    Здесь оптимизатор почему-то решил, что раз *P разыменовывается без проверки, то он априори 0 быть не может и проверять его необходимости нет. А то, что программист мог ошибиться, разработчикам оптимизатора даже в голову не приходит. Как и то, что помимо защищённого режима есть ещё и реальный. А бывают ещё компиляторы для разных контроллеров и встроенных специализированных систем, где разыменовывать 0 указатели бывает нужно и иногда даже необходимо. Или clang такие системы не поддерживает? И никогда не сможет поддержать с подобным подходом, ориентированным на работу только защищённых многозадачных ОС.

    Но вернёмся к статье. На следующем этапе выпиливается переменная dead и проверка на 0 и остаётся:

    void contains_null_check_after_RNCE_and_DCE(int *P) {
      *P = 4;
    }
    

    Если раньше программа корректно работала в реальном режиме, а в защищённом падала, то теперь в реальном режиме вектор 0-ого прерывания перезаписывается адресом 4. В результате при любой ошибке деления компьютер намертво зависает (хотя реальный режим clang, как я понимаю, не поддерживает и никогда не сможет поддержать с таким шикарным легаси).

3. Третий пример я разбирать не буду, т. к. согласен с автором, что оптимизация «x > x+1 всегда false» может быть полезна при использовании макросов. А для проверки переполнения существуют константы MAX_*.

4. Четвёртый пример — почти из поста Вызов никогда не вызываемой функции. Его уже разобрали по полочкам, сломали все копья, какие только можно было сломать, в т. ч. и я, поэтому здесь повторяться не буду. Единственно, скажу, что мне было непонятно, зачем заменять вызов функции по 0-ому адресу с неизбежным сегфолтом на недопустимую инструкцию ud2. Автор поясняет во 2-й статье:

2. Clang has an experimental -fcatch-undefined-behavior mode that inserts runtime checks to find violations like shift amounts out of range, some simple array out of range errors, etc. This is limited because it slows down the application's runtime and it can't help you with random pointer dereferences (like Valgrind can), but it can find other important bugs. Clang also fully supports the -ftrapv flag (not to be confused with -fwrapv) which causes signed integer overflow bugs to trap at runtime (GCC also has this flag, but it is completely unreliable/buggy in my experience). Here is a quick demo of -fcatch-undefined-behavior:

В вольном пересказе: мы всегда так делаем, чтобы показать, что код содержит участки с неопределённым поведением. И плевать, что недопустимая инструкция не указывает на некорректный код. Просто ничего лучшего мы не придумали.

Мне, кстати, удалось получить бинарник с такой инструкцией безо всяких экспериментальных флагов.

И напоследок 2 эпические цитаты. Первая из начала 1-ой статьи. Она очень понравилась dzidzitop:

It turns out that C is not a «high level assembler» like many experienced C programmers (particularly folks with a low-level focus) like to think

Вот оно что оказывается. Си — это та же ява, чуть более быстрая и более опасная. А для написания системных вещей переходите на настоящий ассемблер! Кен Томпсон гомерически хохочет и Деннис Ритчи переворачивается в гробу.

А вторая из 3-ей, заключительной статьи:

Ultimately, undefined behavior is valuable to the optimizer because it is saying «this operation is invalid - you can assume it never happens».

В вольном пересказе это обозначает: «Вау! Неопределённое поведение! Ворочу куда хочу!»

UPD: Вот хочу добавить сюда ещё несколько ответов на вопросы, на которые приходится отвечать по всему треду одно и то же:

1. То, что ub обозначает «делай, что хочешь!», мягко говоря, неправда. Вот, что написано в стандарте C99:

3.4.3

1 undefined behavior

behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

2 NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

3 EXAMPLE An example of undefined behavior is the behavior on integer overflow.

Т. е. в стандарте чётко прописаны 3 действия, из которых разработчики компиляторов могут выбрать любое:

1. Игнорировать ситуацию. Смотрим в Ожегове, что обозначает слово «игнорировать»:

Умышленно не заметить, не принять во внимание.

Т. е. «игнорировать» — это сделать вид, что всё нормально и пройти мимо, а не модифицировать или удалять такой код, и уж тем более делать на его основе какие-то бредовые предположения.

2. Компилировать и выполнять такой код в соответствии с документацией, с выводом предупреждающих сообщений или без них. Т. е. напишите явно в документации, что если мы встречаем 0 указатель, то делаем то-то и то-то, и делайте. Но не в тихую.

3. Прерывать компиляцию и/или выполнение с обязательным выводом диагностических сообщений.

И всё. Ни о каком «что хочешь» в стандарте речи не идёт. Вот тут мне в каментах подсказали, что «with unpredictable results» обозначает «что хочешь». Но на самом деле это обозначает лишь то, что результаты могут быть непредсказуемыми, а совсем не то, что компилятор может делать всё, что угодно (хотя разработчикам таких компиляторов подобная трактовка очень удобна).

Ну и тот же человек считает, что «это notes», а значит неважно, что там написано. Но т. н. неопределённое поведение при переполнении целого — вообще example из того же пункта:

EXAMPLE An example of undefined behavior is the behavior on integer overflow.

И больше я нигде никаких упоминаний об ub при арифметическом переполнении не нашёл. Про переполнение при сдвигах — нашёл. А в других случаях — нет. Но все почему-то на этот example ссылаются.

2. Многие говорят, что быдлокодеры должны страдать. Но серьёзные уязвимости, связанные с ub, а точнее с непредсказуемой реакцией компилятора на ub, в разное время обнаруживались в ядре Linux, во FreeBSD и в GDK-Pixbuf, затрагивающая Chromium, Firefox и VLC. Подробнее см. в этом комментарии, чтоб не раздувать и без того длинный верхний пост. Здесь только скажу, что уязвимость в ядре Linux связана с ошибкой, идентичной со 2-м примером из разбираемых статей.

3. Автор статей и многие в этом треде утверждают, что автоматически отыскать такие ошибки очень сложно и дорого, а то и вовсе невозможно. Но это тоже не так. В Интернете я нашёл такой пример си++ программы с ub:

#include <iostream>
int main()
{
    for (int i = 0; i < 300; i++)
        std::cout << i << " " << i * 12345678 << std::endl;
}

Программа из-за переполнения временного результата на 174-й итерации при использовании ключа оптимизации -O2 в g++ попадает в бесконечный цикл.

Запустив компиляцию, я получил следующие предупреждения (причём безо всяких опций -W что-то_там):

$ g++ -o infinity_loop -O2 infinity_loop.cpp
infinity_loop.cpp: В функции «int main()»:
infinity_loop.cpp:5:38: предупреждение: iteration 174u invokes undefined behavior [-Waggressive-loop-optimizations]
         std::cout << i << " " << i * 12345678 << std::endl;
                                      ^
infinity_loop.cpp:4:5: замечание: containing loop
     for (int i = 0; i < 300; i++)
     ^

А ведь здесь случай куда менее очевидный, чем простое разыменование NULL-указателя.

4. Наконец, на Хабре я вычитал, что стандартный макрос

#define offsetof(st, m) ((size_t)(&((st *)0)->m))

тоже содержит UB. Правда, к ошибкам и уязвимостям это пока не приводит. Но именно, что пока, UB ведь, а значит любой компилятор имеет полное право, встретив вызов этого макроса, отформатировать диск, как утверждают создатели компиляторов и многие лорчане.

UPD 2: Вот тут Sorcerer в комментарии кинул ссылку на письмо Линуса Торвальдса в рассылке от 12 января 2009 года, где он пишет о том, что думает о некоторых оптимизациях. Приведу несколько фрагментов этого письма в своём переводе:

Type-based aliasing — это тупость. Это такая невероятная тупость, что даже не смешно. Оно испорчено. И gcc взял испорченную концепцию и настолько её раздул, следуя букве-закона, что получилась бессмысленная вещь.

[skip]

Это НЕНОРМАЛЬНО. Это так невероятно безумно, что люди, которые делают это, просто должны избавиться от своего убожества, прежде чем они смогут восстановить. Но реальные gcc программисты действительно думали, что это имеет смысл, потому что стандарт это позволяет и даёт компилятору максимальную свободу, — потому что он может делать теперь вещи БЕЗУСЛОВНО АБСУРДНЫЕ.

И компиляторщикам абсолютно абсурдные вещи часто кажутся действительно хорошими, потому что им больше не надо беспокоиться о конечном результате: работает оно или нет, — они просто получили добро тупить во имя оптимизации.

[skip] И если кто-то жалуется, что компилятор невменяемый, компиляторщики скажут «ня, ня, разработчики стандарта сказали, что так можно», с абсолютным отсутствием анализа, имеет ли оно СМЫСЛ.

По любому, однажды вы начинаете делать глупости, подобно этой, думая, что стандарт имеет больше смысла, чем человек, пользующийся головой 5 секунд, внезапно вы оказываетесь в ситуации, когда возможно дико переместить хранилище, и всё это 'корректно'.

[skip]

Угадайте, что произойдёт, если вы имеете такой безумный склад ума и пытаетесь сделать безопасный код без alias'а, — вы займёте лишнее пространство на стеке.

По факту, Linux использует -fno-strict-aliasing из-за чертовски веской причины: потому что в gcc понятие «strict aliasing» является огромной зловонной кучей д-рьма. Linux использует этот флаг не потому, что Linux исполняется быстро и свободно, он использует этот флаг, потому что _не_ использует тот безумный флаг.

Type-based aliasing неприемлемо тупо для начала, и gcc вознёс этот идиотизм до совершенно новых высот, фактически не обращая внимания даже на статически видимый aliasing.

Линус

Оригинал (на английском):

Type-based aliasing is _stupid_. It's so incredibly stupid that it's not even funny. It's broken. And gcc took the broken notion, and made it more so by making it a «by-the-letter-of-the-law» thing that makes no sense.

[skip]

That's INSANE. It's so incredibly insane that people who do that should just be put out of their misery before they can reproduce. But real gcc developers really thought that it makes sense, because the standard allows it, and it gives the compiler the maximal freedom - because it can now do things that are CLEARLY NONSENSICAL.

And to compiler people, being able to do things that are clearly nonsensical seems to often be seen as a really good thing, because it means that they no longer have to worry about whether the end result works or not - they just got permission to do stupid things in the name of optimization.

[skip] And if somebody complains that the compiler is insane, the compiler people would say «nyaah, nyaah, the standards people said we can do this», with absolutely no introspection to ask whether it made any SENSE.

Anyway, once you start doing stupid things like that, and once you start thinking that the standard makes more sense than a human being using his brain for 5 seconds, suddenly you end up in a situation where you can move stores around wildly, and it's all 'correct'.

[skip]

Guess what happens if you have that kind of insane mentality, and you then try to make sure that they really don't alias, so you allocate extra stack space.

The fact is, Linux uses -fno-strict-aliasing for a damn good reason: because the gcc notion of «strict aliasing» is one huge stinking pile of sh*t. Linux doesn't use that flag because Linux is playing fast and loose, it uses that flag because _not_ using that flag is insane.

Type-based aliasing is unacceptably stupid to begin with, and gcc took that stupidity to totally new heights by making it actually more important than even statically visible aliasing.

Linus

И ещё спасибо anonymous'у за камент с ещё одним сообщением на ту же тему того же автора от 26 февраля 2003 года.

Ну и от себя добавлю, что не только Линусу не нравится aliasing. Microsoft тоже не спешит реализовывать его в своём Visual C++. Т. е. не нравится это тем, кто помимо разработки компиляторов создаёт и другой софт с использованием этого компилятора, например ОС. А те, кто создают только компиляторы для сферических программистов в вакууме, рьяно эту фичу реализуют, хоть их и никто не заставляет.

Ну и напоследок оставлю несколько полезных ссылок на память:

Стандарт C11 (последний) (pdf), Стандарт C99 (pdf),

http: //read.pudn.com/downloads133/doc/565041/ANSI_ISO%2B9899-1990%2B%5B1%5D.pdf (Стандарт C89) (pdf),

http: //web.archive.org/web/20030222051144/http: //home.earthlink.net/~bobbitts/c89.txt (Стандарт C89) (txt),

https: //web.archive.org/web/20170325025026/http: // www .open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4660.pdf (Стандарт C++17) (последний) (pdf),

Стандарт C++14 (pdf), Стандарт C++11 (pdf),

бумажный перевод стандарта C++17, выполненный Зуевым и Чуприновым, Москва, 2016, на основе Working Draft, Standard for Programming Language C++ от 22 мая 2015 года (номер документа n4527) за 4945 руб. (надеюсь, что эту ссылку не сочтут за рекламу, т. к. к авторам я никакого отношения не имею), а здесь можно скачать начало перевода (предисловие и содержание), ну и ещё торрент-ссылку видел на эту книгу, но здесь её публиковать не буду,

статья на Хабре (из песочницы) от 2014 г. Неопределенное поведение в C++, ещё одна статья там же от 2016 года Находим ошибки в коде компилятора GCC с помощью анализатора PVS-Studio, Разыменовывание нулевого указателя приводит к неопределённому поведению и Про C++ алиасинг, ловкие оптимизации и подлые баги. Это так, ссылки на заметку.

Некоторые ссылки парсер ЛОР'а не принял, поэтому мне пришлось разделить их пробелами, превратив в текст, который можно скопировать в адресную строку браузера, удалив пробелы. Там, где http встречается дважды в 1 строке — не ошибка, а именно такие ссылки.

Ответ на: комментарий от ckotinko

теперь следующий вопрос: почему он работает в рантайме?

anonymous ()
Ответ на: комментарий от anonymous

так я не понял, линус он лох что ли? ниасилил, неудачнег, а отличие от местных удачников-аналитиков

ckotinko ☆☆☆ ()
Ответ на: комментарий от anonymous

ты уверен что он работает в рантайме? ничего не путаешь?

ckotinko ☆☆☆ ()

Лентяи

Накипело

Чего люди не делают из-за лени

вот алгоритм вычисления обратного квадратного корня из википедии

float Q_rsqrt( float number ) {
	long i;
	float x2, y;
	const float threehalfs = 1.5F;

	x2 = number * 0.5F;
	y  = number;
	i  = * ( long * ) &y;
	i  = 0x5f3759df - ( i >> 1 );
	y  = * ( float * ) &i;
	y  = y * ( threehalfs - ( x2 * y * y ) );

	return y;
}
Налицо нарушения strict aliasing. Уставной способ решения это копирование. Работает на всех стандартах, надежен что ппц.
float Q_rsqrt( float number ) {
	long i;
	float x2, y;
	const float threehalfs = 1.5F;

	x2 = number * 0.5F;
	y  = number;
	memcpy(&i, &y, sizeof(float));
	i  = 0x5f3759df - ( i >> 1 );
	memcpy(&y, &i, sizeof(float));
	y  = y * ( threehalfs - ( x2 * y * y ) );

	return y;
}
Причем компилятор скорее всего «догадается» о намерениях программиста и не будет генерировать вызов memcpy.

Это так сложно?

Deleted ()

valgrind и тесты спасут отца демократии

reprimand ★★★★★ ()
Ответ на: комментарий от ckotinko

да на каждый int i; i << n должен быть варнинг о возможном UB. А если компилятором используется предположение, что переполнения не будет, то вот тебе готовый варнинг. И таких примеров много можно сочинить.

dzidzitop ★★ ()
Ответ на: комментарий от anonymous

И я не знаю, зачем ты поминаешь java в половине постов. Ее компилятор тоже делает сумасшедшие оптимизации

А я разве спорю? Именно поэтому и упоминаю. На яве нельзя обратиться к произвольному адресу и даже при обращении к элементам массива можно обращаться только по индексу. Поэтому на яве очень многого нельзя сделать. Но зато именно это отсутствие свободы даёт компилятору и виртуальной машине простор для оптимизаций. Т. е. чем больше свободы мы даём компилятору, тем меньше её оставляем программисту и наоборот.

Вот я и говорю, что си постепенно превращают в слегка улучшенную яву.

AT&T Unix имеется в виду?

Да.

Ну, если самый первый UNIX — это быдло-код, тогда я не знаю, что такое не быдло-код.

Желание диагностики - хорошее желание, но ведь уже несколько раз объяснили почему это так не работает.

А вот я не понимаю, почему компилятор g++ может отследить такое ub:

#include <iostream>
int main()
{
    for (int i = 0; i < 300; i++)
        std::cout << i << " " << i * 12345678 << std::endl;
}

и не может такое:

ptr->field = val;
if(ptr)
 ptr->field2=val2;

не надо молиться на разрабов компиляторов или на кого-то ещё.

Да, но и чтобы спорить с ними нужно неплохо знать матчасть.

А для того, чтобы спорить с разрабами текстового редактора, который падает на некорректном unicode-файле, о том, что это баг, а не фича, тоже надо знать мат. часть или достаточно наличия этого очевидного бага? Не нравится ассоциация с падением, типа cc не падает? Хорошо, переформулирую: при загрузке unicode-документа с единственным недействительным символом и последующем сохранении этого документа, редактор без предупреждений сохраняет мусор, причём не только на месте неверного символа, а во всём документе, да ещё резервную копию не оставляет. Это тоже будет считаться фичей? Типа, страдать должен юзверь, не осиливший utf8?

aureliano15 ()
Ответ на: комментарий от anonymous

Где там «делай что хочешь»?

ignoring the situation completely with unpredictable results

См. мой UPD к верхнему посту:

Вот тут мне в каментах подсказали, что «with unpredictable results» обозначает «что хочешь». Но на самом деле это обозначает лишь то, что результаты могут быть непредсказуемыми, а совсем не то, что компилятор может делать всё, что угодно (хотя разработчикам таких компиляторов подобная трактовка очень удобна).

Тоже нет. Это notes, там примеры вариантов.

Ну и тот же человек считает, что «это notes», а значит неважно, что там написано. Но т. н. неопределённое поведение при переполнении целого — вообще example из того же пункта:

EXAMPLE An example of undefined behavior is the behavior on integer overflow.

И больше я нигде никаких упоминаний об ub при арифметическом переполнении не нашёл. Про переполнение при сдвигах — нашёл. А в других случаях — нет. Но все почему-то на этот example ссылаются.

aureliano15 ()
Ответ на: комментарий от anonymous

или, если аппаратная реализация не позволяет этого сделать, вообще не должны быть определены

т.е. читать ты ещё не научился...

Я научился. А вот ты похоже нет.

aureliano15 ()
Ответ на: комментарий от vodz

И как быть разработчику обработчика прерывания ошибки деления в реальном режиме x86

Вы не поверите, но даже в защищенном режиме на PDP-11

Но я-то спрашивал про реальный режим x86, а не про защищённый pdp-11.

aureliano15 ()
Ответ на: комментарий от Deleted

Там интересный код:

bytes = height * rowstride;
        if (bytes / rowstride != height) { /* overflow */

проверку на переполнение делают немного не так, например для умножения:

int a = <something>;
int x = <something>;
if (a > INT_MAX / x) /* `a * x` would overflow */;
if ((a == -1) && (x == INT_MIN)) /* `a * x` can overflow */
if ((x == -1) && (a == INT_MIN)) /* `a * x` (or `a / x`) can overflow */

И вместо 2 строчек мы получили 4 строки (описания я не считаю, но собственно умножение надо добавить). А вместо одного лишнего сравнения и одного if'а — 4 сравнения, два «и» и 2 if'а. Но дело даже не в этом. Не такой уж это и большой проигрыш (особенно учитывая, что умножение намного дороже), и ради переносимого и безопасного кода оно того стоит. Но вот ошиблись люди, у каждого бывает. А компилятор эту ошибку усугубил, даже не предупредив. И в результате миллионы людей получили уязвимость в системе.

aureliano15 ()
Ответ на: комментарий от kawaii_neko

И как быть разработчику обработчика прерывания ошибки деления в реальном режиме x86, адрес которого как раз находится по 0-му слову, чтобы избежать ub и не прострелить себе ногу?

asm __volatile__

Т. е. мы снова вернулись в 60-ые годы, когда для написания системных программ был только один язык: ассемблер. А си превратили в улучшенную яву. О чём я и говорю.

aureliano15 ()
Ответ на: комментарий от anonymous

Т. е. разработчики firefox, chromium, ядра Linux и FreeBSD все поголовно слабоумные?

Чтобы писать корректный код, нужно понимать основы CS. Чтобы писать ядра, нужно знать железо. Слишком редко эти знания сочетаются в одном человеке.

Т. е. разработчики компиляторов ориентируются не на реальных программистов, а на неких сферических программистов в вакууме, которые всё знают и никогда не совершают ошибок? Ясно.

И как быть разработчику обработчика прерывания ошибки деления в реальном режиме x86, адрес которого как раз находится по 0-му слову, чтобы избежать ub и не прострелить себе ногу?

volatile asm

Как я только что ответил kawaii_neko:

Т. е. мы снова вернулись в 60-ые годы, когда для написания системных программ был только один язык: ассемблер. А си превратили в улучшенную яву. О чём я и говорю.

либо implementation-defined трюки. Например, обращаться к NULL нельзя, но NULL не обязательно должен значить нулевое слово.

А какое же это слово? Можно с примером такого корректного (пусть даже implementation-defined) поведения для real mode x86?

aureliano15 ()
Ответ на: комментарий от anonymous

А ты что предлагаешь?

Улучшать диагностику и аккуратнее оптимизировать.

aureliano15 ()
Ответ на: комментарий от maverik

Удваиваю гражданина. ТС нагло переврал суть. Читайте оригинал.

Выражаясь по-фене, отвечаем за бубню. Что у тс'а «нагло переврано»? Читаем оригинал.

aureliano15 ()
Ответ на: комментарий от proud_anon

И где здесь про генерацию бреда? По-моему, в стандарте чётко прописаны 3 действия, из которых разработчики компиляторов могут выбрать любое

То, где можно «ignore the situation with unpredictable results».

См. мой UPD к верхнему посту:

И всё. Ни о каком «что хочешь» в стандарте речи не идёт. Вот тут мне в каментах подсказали, что «with unpredictable results» обозначает «что хочешь». Но на самом деле это обозначает лишь то, что результаты могут быть непредсказуемыми, а совсем не то, что компилятор может делать всё, что угодно (хотя разработчикам таких компиляторов подобная трактовка очень удобна).

Ну и тот же человек считает, что «это notes», а значит неважно, что там написано. Но т. н. неопределённое поведение при переполнении целого — вообще example из того же пункта:

EXAMPLE An example of undefined behavior is the behavior on integer overflow.

И больше я нигде никаких упоминаний об ub при арифметическом переполнении не нашёл. Про переполнение при сдвигах — нашёл. А в других случаях — нет. Но все почему-то на этот example ссылаются.

Например, если указатель где-либо разыменовывается, ситуацию, что он будет равен NULL, можно проигнорировать и все дальнейшие провери на неё выбросить.

Стоп. Проверка неопределённым поведением не является. Неопределённым поведением является разыменование. Если компилятор учитывает это разыменование, чтобы удалить проверку, то это никак не игнор, а наоборот учёт такого поведения. А значит этот вариант отпадает. А то так можно договориться до того, что обматюгать кого-то на форуме — это то же самое, что и проигнорировать его. Типа я тебя игнорирую, поэтому могу обматюгать, но т. к. на самом деле это был игнор, то можешь считать, что я никого не обматюгал.

Он утверждает, что A[B] — это то же, что *((A + B)) Но где он утверждает обратное, про приведение арифметики указателей, наоборот, к массивам?

Он утверждает, что если 2 указателя указывают на элементы 1 массива или на элемент, находящийся сразу после массива, то они могут быть приведены к массиву. Иначе поведение не определено, т. е. ничем не лучше 0. См. п. 6.5.6 (8-9):

8 When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression. In other words, if the expression P points to the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i−n-th elements of the array object, provided they exist. Moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.

9 When two pointers are subtracted, both shall point to elements of the same array object, or one past the last element of the array object; the result is the difference of the subscripts of the two array elements. The size of the result is implementation-defined, and its type (a signed integer type) is ptrdiff_t defined in the <stddef.h> header. If the result is not representable in an object of that type, the behavior is undefined. In other words, if the expressions P and Q point to, respectively, the i-th and j-th elements of an array object, the expression (P)-(Q) has the value i−j provided the value fits in an object of type ptrdiff_t. Moreover, if the expression P points either to an element of an array object or one past the last element of an array object, and the expression Q points to the last element of the same array object, the expression ((Q)+1)-(P) has the same value as ((Q)-(P))+1 and as -((P)-((Q)+1)), and has the value zero if the expression P points one past the last element of the array object, even though the expression (Q)+1 does not point to an element of the array object. 106)

Кроме того, даже без вычитания, любое разыменование недействительного указателя (даже не 0) — ub, как я уже говорил.

aureliano15 ()
Последнее исправление: aureliano15 (всего исправлений: 1)
Ответ на: комментарий от maverik

И никаких тебе предупреждений.

gcc-trunk:

Мой gcc версии 4.9.2 никаких предупреждений и ошибок не выдаёт. Если в последней версии это поправили, то я рад.

aureliano15 ()
Ответ на: комментарий от maverik

Кстати, сравните, что генерирует gcc (7.2)

.LC0:
  .string "x+1>x"
.LC1:
  .string "x+1<x"

и icc 17

.L_2__STRING.0:
  .long 1043409784
  .word 2680
  .byte 0

Да, 1043409784 вместо «x+1>» — это жесть. :-)

aureliano15 ()
Ответ на: комментарий от vodz

А, ну да, где-то совсем далеко 1-ая часть действительно была. Но в том сообщении, на которое отвечал я, её уже не было.

aureliano15 ()
Ответ на: комментарий от ckotinko

У них реально стандарт это абсолютная истина ибо она «общепринята», и раз там написано что ничего не гарантируется, то значит это надо понимать буквально(приводится в пример тест с компилятором), потому что «все так думают». Вы им сообщаете нечто, что для них попадает в запрещённую зону, и эту идею вы просто не загрузите им.

Ну, судя по более поздним сообщениям в треде, лёд тронулся. Или это просто умные люди подтянулись, которые и раньше так думали.

В любом случае, тут в паре каментов привели цитаты Линуса Торвальдса, который в этом вопросе с нами. (Как минимум частично, т. к. во всех 3 цитатах он говорил не вообще о ub, а об одном конкретном ub, введённом в си99 — strict-aliasing. И с ним, кстати, полностью согласны в Microsoft, в чьём компиляторе этого ub нет. Так что тут можно говорить уже не о личном мнении Торвальдса, а о некоем консенсусе разработчиков ОС, в других вопросах мало в чём согласных друг с другом). Может прочитают мнение своего авторитета и задумаются, если только это не приведёт к шизофрении в качестве защитной реакции на противоположные утверждения разных авторитетов. :-)

aureliano15 ()
Ответ на: комментарий от stevejobs

с другой стороны, от C++ никуда не убежать, он с нами будет еще десятки лет, так что любая теория, которая помогает седеть с ним медленней - подойдет)

Тенденции в си++ ещё печальнее, чем в си. Если в си за всю его историю было всего 4 стандарта: K&R (1-й и неофициальный), c89, c99 и c11, то для c++ в последнее время каждые 3 года новый стандарт выходит, причём вводят всякую дичь, тупо засоряя язык.

Если так пойдёт и дальше, то си++ вполне может умереть раньше срока и быть заменён на что-то другое, например на rust или какой-нибудь форк си++++, очищенный от ненужной шелухи.

aureliano15 ()
Ответ на: комментарий от anonymous

Ты серьезно думаешь что компиляторщики массово заблуждаются, и тут ты приходишь такой весь в белом и поясняешь им за стандарт?

Не я один так думаю. Ещё так думает Линус Торвальдс, например. См. каменты этот и этот.

aureliano15 ()
Ответ на: комментарий от maverik

Нет, не должен. Как минимум хотя бы потому, что
Хотя что объяснять человеку, который думает, что инструмент «что-то ему должен».

Ну да. И вообще все программы могут постоянно падать, а ещё лучше вообще не загружаться. И молотки могут при каждом ударе по гвоздю ломаться, отлетая в голову. Ведь:

THE SOFTWARE IS PROVIDED «AS IS», WITHOUT WARRANTY

И когда у тебя что-то заглючит, ты просто улыбнёшься и скажешь: так и должно быть, потому что так должно быть всегда. Ведь настоящие мужчины... не плачут. :-)

aureliano15 ()
Ответ на: комментарий от anonymous

Кстати, ничего об ub при переполнении, кроме этого короткого примера, в котором, кстати, говорится о любом целочисленном переполнении, а не только знаковом, я в стандарте не нашёл.

А я нашёл.

Рад за тебя. А я не нашёл. Но если ты действительно нашёл, то процитируй это место здесь со ссылкой на документ, в котором ты это нашёл, потому что пока это просто бла-бла-бла. Я тоже могу сказать всё, что угодно.

aureliano15 ()
Ответ на: комментарий от stevejobs

Да лучше бы он не так здорово оптимизировал (это, в конце концов, может сделать и программист)

да бог с тобой, собирай всё с -O0!

Так и собирают так, когда хотят получить что-то надёжное, а не глючное. А раньше так вообще только так и собирали. Но вот зачем нужна опция, которая всё портит? Ну присобачили вы, допустим, какой-то экспериментальный режим компиляции для тестеров, отразив это в документации, тут всё понятно. Но основной-то режим должен нормально работать.

Да, дистры тоже бывает разнае. Например, т. н. стабильная Федора на самом деле никакая не стабильная, но и у неё есть свои почитатели. Но почему все тут не говорят: всё правильно делают разрабы Федоры, пусть у них всё лагает, но это мелочь, главное — стандарты не нарушают! (Хотя, как я уже говорил, и стандарт нарушают на самом деле).

А ведь тут то же самое. Только, в отличие от ситуации с Федорой, альтернатив, кроме как совсем отключить оптимизацию, нет.

А кроме того, не всегда это возможно. Например, в прежних версиях ядра в инлайн-функции fix_to_virt из fixmap.h вызывалась некая функция __this_fixmap_does_not_exist(), которая на самом деле нигде не была объявлена, на что указывает и название, с целью вызвать ошибку линковки. Разработчики ядра при этом исходили из того, что оптимизатор уберёт условие, в котором эта функция вызывается, вместе с нею (там если всё правильно и в fix_to_vitr передана константа, то условие всегда ложно), а в случае ошибки (передачи переменной, а не константы) при попытке линковки выскочит ошибка об отсутствующей функции.

В последних ядрах вызова этой функции нет, а ошибку компиляции в fix_to_virt они генерят нормально. Но где гарантия, что в других частях ядра или где-то ещё не осталось кода, завязанного на оптимизатор (что, конечно, тоже неправильно)?

И, кстати, Линус в этом вопросе с нами. :-)

aureliano15 ()
Ответ на: комментарий от anonymous

Он видит: (1) вызов по указателю -> это не ноль, (2) указатель либо ноль, либо EraseAll, (3) делаем выводы.

Не совсем так. Он видит: сначала указатель разыменовывается, а затем проверяется на 0. Значит в программе явная логическая ошибка: либо программист разыменовывает указатель, который может быть 0, либо он включил лишнюю проверку на 0, что тоже не есть гуд. А значит, компилятор должен выдать в этом месте предупреждение.

aureliano15 ()
Ответ на: комментарий от anonymous

что мешало сделать переполнение со знаком не ub, а implemented behavior?

при последующем портировании, ты выдаёшь компилятору карт-бланш на новое поведение

Это просто непереносимый код. Но не всякий код должен быть переносимым. Если я пишу драйвер для писюхи, то зачем мне переносить его на итаниум, где совсем другое оборудование?

По сути это такой же UB

Это не ub, потому что

1. на каждой отдельно взятой платформе такой код будет работать предсказуемо (и я вангую, что на подавляющем большинстве платформ он будет работать так же, как и на x86);

2. всех возможных вариантов будет не так много, и я смогу при желании реализовать все нужные мне ветки в #ifdef'ах.

в этом случае не надейся, что компилятор выдаст хоть какое-то сообщение с ошибкой.

А вот предупреждение выдать компилятору никто не запрещает. Я только за. Правда, он не выдаёт даже с опцией -Wall. По крайней мере gcc версии 4.9.2 (на более новых версиях, говорят, предупреждает).

aureliano15 ()
Ответ на: комментарий от anonymous

Но могут, когда хотят!

Иногда могут, иногда не могут. И те, и другие. И что?

В приведённом примере ub не очевидно. Надо прогнать цикл до 174, чтоб выяснить, что имеет место ub. А в совершенно очевидных случаях порой предупреждений нема. Вот что тут не мочь:

ptr->fld=9;
if(ptr)
 ptr->fld2=10;

Видно же, что в любом случае логическая ошибка: либо ptr проверяется слишком поздно, либо это лишняя проверка. Но ни то, ни другое ненормально. Так почему бы не предупредить? Тем более, что в ядре Linux уже была такая ошибка, и там при разыменовывании ядро не вылетало, но создавалась уязвимость. Подробнее см. верхний пост после UPD.

aureliano15 ()
Ответ на: комментарий от anonymous

Для того, чтобы X приводило к UB, не обязательно в стандарте должно быть написано «X приводит к UB».

Но это не избавляет компилятор от генерации предупреждения, если он исключает какие-то программные блоки, используя это ub. Либо он должен оставлять всё как есть, и пусть ub выскакивает не из-за него. Тогда к нему претензий не будет.

aureliano15 ()
Ответ на: комментарий от i-rinat

Пусть пишут так компиляторы, я разрешаю.

Спасибо. Торвальдс тоже разрешил, и даже бодался с ними. Но бестолку, они же упёртые. Вот отрывок из его письма:

Anyway, once you start doing stupid things like that, and once you start thinking that the standard makes more sense than a human being using his brain for 5 seconds, suddenly you end up in a situation where you can move stores around wildly, and it's all 'correct'.

aureliano15 ()
Ответ на: комментарий от anonymous

Ну, во-первых, помимо формальностей есть ещё здравый смысл.

Опиши этот здравый смысл

https://www.mail-archive.com/linux-btrfs@vger.kernel.org/msg01647.html

в качестве алгоритма для компилятора.

Алгоритм очень простой: или не делать неочевидных для программиста вещей, как то убирать разные проверки и т. д., либо предупреждать об этом.

aureliano15 ()
Ответ на: комментарий от Iron_Bug

чем больше разработчиков и чем больше кода - тем больше там бардака. имхо, если бы не гнались за новыми релизами каждый месяц и всё нормально тестировали, никаких проблем бы не было.

Я согласен. Но почти всякая проблема имеет несколько аспектов. И одним из них является тихий режим компилятора, меняющего код и не предупреждающего об этом. Что, разумеется, не снимает ответственности и с разработчиков софта, использующих этот компилятор.

и где ты реальный режим x86 в последний раз видел живьём?

Каждый день вижу в GRUB'е, загружая Linux.

А кроме того, процессоры, совместимые с 8086/8088 по-моему до стих пор выпускаются в России и/или на Украине или до недавнего времени выпускались. Наверно, не просто так. Кто-то где-то их использовал, скорее всего вояки.

а в микроконтроллерах лохи код не пишут.

В ядре Linux тоже. Но вот что пишет Линус Торвальдс на этот счёт:

Anyway, once you start doing stupid things like that, and once you start thinking that the standard makes more sense than a human being using his brain for 5 seconds, suddenly you end up in a situation where you can move stores around wildly, and it's all 'correct'.

[skip]

The fact is, Linux uses -fno-strict-aliasing for a damn good reason: because the gcc notion of «strict aliasing» is one huge stinking pile of sh*t. Linux doesn't use that flag because Linux is playing fast and loose, it uses that flag because _not_ using that flag is insane.

Type-based aliasing is unacceptably stupid to begin with, and gcc took that stupidity to totally new heights by making it actually more important than even statically visible aliasing.

aureliano15 ()
Ответ на: комментарий от i-rinat

код должен выполняться и генерить ub, которое можно увидеть.

Как это? Как сгенерировать UB? UB по определению — неопределённое поведение. Нет никаких гарантий, что компилятор сделает что-то неожиданное для человека.

Но я же сделал такой рабочий код на основе кода автора и иллюстрирующий его идею (правда не ту, которую он озвучил). И этот код работает без оптимизации, а с оптимизацией вылетает. А если раскомментировать в нём одну строчку, то будет работать и так, и так, но по-разному, и это можно увидеть.

Конечно, я не могу гарантировать, что он будет так работать всегда и везде. Но на Linux для amd64, если откомпилировать его gcc 4.9.2 или clang 3.5.0 (а скорее всего и другими версиями) — будет.

для разных людей разные вещи неожиданные.

Достаточно, чтоб он работал по-разному с оптимизацией и без неё.

aureliano15 ()
Ответ на: комментарий от andreyu

И ваш комментарий подтверждает «Там в статье — про «signed integer overflow». Он UB, а unsigned — нет.»

Не возражаю.

aureliano15 ()
Ответ на: комментарий от anonymous

Линус про strict aliasing

Да это ж ckotinko перелогинился!

Да чего уж там! Он и в рассылки вместо Линуса пишет, это ж все знают! И ядро вместо него написал! Тот ещё троль.

aureliano15 ()
Ответ на: комментарий от aureliano15

Но бестолку, они же упёртые.

Потому что они правы. Если стандарт кривой, нужно фиксить стандарт, а не полагаться на мнение определённых людей. Вот возникает спорный момент о том, есть баг в компиляторе или нет. И что теперь, звонить Линусу и спрашивать, что должен делать компилятор? Это же бред.

В Linux особый случай. Там очень много кода построено с нарушением этого самого strict aliasing, потому что много кода требует таких преобразований, причём они должны быть гарантированно быстрыми. Полагаться на то, что компилятор оптимизирует трюк с memset в ядре это как-то за гранью. Если долго работать с таким кодом, мозги выворачиваются в определённую сторону.

В одной из презентаций про Subsurface докладчик рассказывал, как Линус его программировал:

— But this is inefficient!!!
— That's UI. It doesn't matter that it takes 200 nanoseconds more...

И ещё раз, суммируя. Когда ты запускаешь компилятор и указываешь ему -std=c99, это означает, что компилятор работает согласно C99, а не желанию Линуса. Просто потому что C99 формализован, а представление Линуса о том, что такое «здравый смысл» — нет.

i-rinat ★★★★★ ()
Ответ на: комментарий от anonymous

Сынок, я тебя разочарую. Множества членов комитета и написателей компиляторов пересекаются.

Да. Но только в комитетах по большей части заседают не написатели компиляторов, а представители фирм-написателей компиляторов. Те же бизнес-чиновники, отстаивающие интересы своих фирм. И стандарт является компромиссом этих интересов, порой несовместимых. Потому и стандарт такой. В си это не так заметно, т. к. большинство фирм больше интересуются плюсами, а вот там действительно жесть!

aureliano15 ()
Ответ на: комментарий от dzidzitop

А что там было и чего не было у K&R - уже давно почти всем пофиг.

Ну вот мне не пофиг. ckotinko не пофиг. А ты говоришь, почти всем.

aureliano15 ()
Ответ на: комментарий от aureliano15

А если раскомментировать в нём одну строчку, то будет работать и так, и так, но по-разному, и это можно увидеть.

На определённой версии определённого компилятора? Кажется, статья была про язык Си, а не про конкретную версию компилятора.

UB на то и undefined, что поведение в на таком коде не определено. Может так сделать, а может сяк.

Я тут не читал все сообщения в теме. Но в тех немногих, что читал, авторы явно не понимают, как компилятор обрабатывает исходники. Похоже, они считают, что компилятор во время обработки одного файла ходит и заглядывает во все остальные, проверяя, что там человек написал, и пытаясь понять, что он на самом деле хотел написать. А объектные файлы, от которых нет исходников, декомпилирует и творчески переосмысливает.

i-rinat ★★★★★ ()
Ответ на: комментарий от dzidzitop

Уверен, что компиляция каждой программы родит десятки подобных «предупреждений». Хотя бы потому, что не каждый UB вообще можно увидеть во время компиляции, а оптимизируют рантайм с рассчётом на корректность программы.

Так пусть рожает! Ждём не дождёмся. Если будут лишние, то их всегда можно в лог записать, а в stderr сказать только, что в логе тоже есть предупреждения, незначительные с точки зрения компилятора. Или пусть по -Wall выводит. Или новую опцию создадут, типа -Wmegall.

aureliano15 ()
Ответ на: комментарий от alexanius

Я и говорю, фантазия, там это чётко задокументированно:

ignoring the situation completely with unpredictable results

Я и говорю, чётко задокументировано:

ignoring the situation

А игнорирование — это не тихо выбросить проверку на 0 или проверку на то, что сумма меньше одного из слагаемых. Как я написал в другом каменте (там подробнее, здесь только выдержка):

Если компилятор учитывает это разыменование, чтобы удалить проверку, то это никак не игнор, а наоборот учёт такого поведения.

Касательно примера - неправильное использование volatile.

Согласен. Ступил.

aureliano15 ()
Ответ на: Лентяи от Deleted

Причем компилятор скорее всего «догадается» о намерениях программиста и не будет генерировать вызов memcpy.

Да, gcc даже без оптимизации memcpy не генерит.

aureliano15 ()
Ответ на: комментарий от reprimand

valgrind и тесты

и другие инструменты тоже. Это понятно. Но и компилятор должен предупреждать. Причём он — в первую очередь.

aureliano15 ()
Ответ на: комментарий от i-rinat

Если стандарт кривой, нужно фиксить стандарт

И с этим тоже согласен.

И что теперь, звонить Линусу и спрашивать, что должен делать компилятор?

Вот я не думаю, что разработчики MS VS с ним консультировались. Однако они тоже не стали реализовывать у себя этот strict aliasing. И я думаю в т. ч. и потому, что их фирма не только создаёт компилятор, но и ещё и пользуется им для создания другого ПО. Т. е. это не только мнение Линуса, но и MS.

Полагаться на то, что компилятор оптимизирует трюк с memset в ядре

Ну, для ядра есть kmemcpy (я полагаю, что имелась в виду memcpy, а не memset). А если компилятор про kmemcpy ничего не знает, то не сложно один раз написать макрос:

#ifdef OPTIMIZE
 #define nomemcpy memcpy
#else
 #define nomemcpy kmemcpy
#endif

Кстати, когда-то они проделывали в ядре подобный трюк с несуществующей функцией __this_fixmap_does_not_exist() для генерации ошибки линковки в случае передачи в fix_to_virt() неправильной константы или переменной вместо константы. И, по-моему, даже не проверяли, включён оптимизатор или нет.

— That's UI. It doesn't matter that it takes 200 nanoseconds more...

В user interface — обычно да. А в каком-нибудь чтении/записи в длинном цикле какого-то важного фонового процесса — может быть важным.

Когда ты запускаешь компилятор и указываешь ему -std=c99, это означает, что компилятор работает согласно C99

А C99, в свою очередь, не требует от компилятора портить такие указатели.

Я тут не читал все сообщения в теме. Но в тех немногих, что читал, авторы явно не понимают, как компилятор обрабатывает исходники. Похоже, они считают, что компилятор во время обработки одного файла ходит и заглядывает во все остальные

Нет, конечно. Но такую функцию (взято из того же письма Торвальдса) можно было бы догадаться не трогать:

unsigned long a;

        a = 5;
        *(unsigned short *)&a = 4;

И в такой функции (уязвимость в ядре Линукс 2009 года) предупреждение напрашивается само собой:

struct sock *sk = tun->sk;
	unsigned int mask = 0;

	if (!tun)
	    return POLLERR;

потому что здесь либо не проверяется tun до разыменования, либо, если он известен (инициализирован в другом модуле), то наоборот лишняя проверка. Однако оптимизирующий компилятор тихо выбирает 2-й вариант (что проверка лишняя), вместо предупреждения.

Естественно, могут быть и нетривиальные случаи, и там на компилятор пенять нельзя. Но такую-то ерунду мог бы проанализировать!

А если раскомментировать в нём одну строчку, то будет работать и так, и так, но по-разному, и это можно увидеть.

На определённой версии определённого компилятора? Кажется, статья была про язык Си, а не про конкретную версию компилятора.

И да, и нет. Первая статья начинается словами:

People occasionally ask why LLVM-compiled code sometimes generates SIGTRAP signals when the optimizer is turned on. After digging in, they find that Clang generated a «ud2» instruction (assuming X86 code) - the same as is generated by __builtin_trap(). There are several issues at work here, all centering around undefined behavior in C code and how LLVM handles it.

Т. е. речь о C вообще, но с упором на clang и llvm.

aureliano15 ()
Ответ на: комментарий от aureliano15

торвальдс -- известный неосилятор

за несколько лет до описаных событий он считал, что volatile — синоним atomic, и если его ведро падает, то виноват gcc

anonymous ()
Ответ на: торвальдс -- известный неосилятор от anonymous

торвальдс — известный неосилятор
за несколько лет до описаных событий он считал, что volatile — синоним atomic

Вспомнились слова Окуджавы о молодых бардах конца 80-х — начала 90-ых: они прекрасно играют на гитаре, гораздо лучше меня, у них хорошо поставлен голос, но песни — никакие (естественно, дословно не помню, но смысл был такой).

Надеюсь, что намёк понят.

aureliano15 ()
Ответ на: комментарий от aureliano15

Я всё как-то в толк не возьму. Идёт 6я страница темы, а всё крутится вокруг одного и того же: «компилятор мог бы предупреждать больше, но не предупреждает в некоторых ситуациях». Clang с флагом -Weverything генерирует тонны сообщений даже для helloworld на gtk. Если вы уверены, что некоторые опасные моменты компилятор мог бы выявлять и сообщать об этом программисту (видимо у вас обширный опыт в разработке компиляторов С в отличие от меня), то зачем тратить время на форуме, когда можно связаться с разработчиками компилятора и поделиться идеей, а ещё лучше — приложить патч? Я думаю все форумчане вам только спасибо скажут.

Deleted ()
Ответ на: торвальдс -- известный неосилятор от anonymous

А что предлагается вместо работы с указателями на несовместимые типы в общем случае, чтобы это было эффективно? Стандартом - ничего (memcpy структуры приличного размера никто же предлагать не будет?). Кстати, до недавнего времени (до C11) поведение при чтении из union не того объекта, что был записан, считалось unspecified. При таком положении дел компилятору нехорошо притворяться идиотом, делая код нерабочим даже для тех ситуаций, когда контекст позволяет понять, что работа идёт с одними и теми же байтами. Для тех, кто особо возмущается, была брошена кость - нестандартный атрибут may_alias. Но даже с этим атрибутом всё печально, т.к. указать его произвольно при объявлении указателя не выходит, он действует только при определении типов. Причём тип с may_alias оказывается несовместимым с типом без may_alias.

Вообще непонятно, зачем нужен undefined behavior при работе с несовместимыми типами. Это выглядит скорее как недоформализация. Аналогичную ситуацию с unspecified behavior в union формализовали, что же мешает рассматривать работу с несовместимыми типами так же, как она рассматривается в случае union? С точки зрения оптимизации такое рассмотрение может показаться вредным, но для разруливания таких ситуаций существует хинт restrict, которым и сейчас успешно пользуются.

И вот вместо того, чтобы работать над уточнением стандарта, компиляторы возводят этот undefined behavior в абсолют, «took that stupidity to totally new heights», как справедливо отмечает Линус.

Sorcerer ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.