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 строке — не ошибка, а именно такие ссылки.

★★

Последнее исправление: aureliano15 (всего исправлений: 2)

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

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

сегфолт

Ни в C, ни в плюсах сегфолтов не существует. Хочешь получить гарантированный сегфолт — пиши на ассемблере.

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

Ворнинг: b может быть равен nullptr. Рекомендуем добавить проверку.

Для любителей явы можно такой -Wmay_be_null и добавить.

Ворнинг: a может быть не инициализирован.

Работает даже c минимальным -W . Удивительно то, что прогресс с этим предупреждением могли заметить не только, кто программит на Си лет 20-30, а буквально лет 10.

Ворнинг: c используется только в cout на следующей строке и поэтому не будет сохранён на стек.

И что? А вот с определением о неиспользованности далее прогресс тоже примерно такой же как с прошлым пунктом.

Ворнинг: убедитесь, что указатель b не был получен выделением динамической памяти

Это прямо бич какой-то. Все помешались с malloc/free. Как-будто любые другие функции не могут вернуть указатель на полную фигню, где NULL — самое приятное из этой фигни.

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

В Java контейнеры стандартные, и про них можно делать далеко идущие выводы, а в С++ у нас есть libstdc++, Qt, boost, и везде есть какие-нибудь контейнеры например. А бывает и хуже: когда ты вызываешь метод в QWidget а он приводит к срабатыванию доставки событий из очереди. «так получилось».

И как IDE будет про них подсказывать? В С++ это нереализуемо, там слишком много свободы для того, чтоб можно было что-то анализировать серьёзно.

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

вообще насчет «откомпилировать в динамике».

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

«класс implicitly shared». например QString, QIcon - ведет себя как shared_ptr но умеет раскопироваться при изменении.

или

«класс explicitly shared» - то есть ведет себя как shared ptr, небезопасен при изменении.

или

«обычный класс».

так же и контейнеры помечать. например завести абстрактные vector, map, list, а в зависимости от используемых библиотек подставлять под них уже конкретные классы. например «этот класс считать реализацией абстрактного вектора», «этот метод считать таким-то методом абстрактного вектора».

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

Если тебе нужен язык, показывающий кодеру на его ошибки — попробуй питон. Или visual basic.

Эти языки показывают ошибки кодера пользователю.

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

Работает даже c минимальным -W . Удивительно то, что прогресс с этим предупреждением могли заметить не только, кто программит на Си лет 20-30, а буквально лет 10.

А clang-овцы мучаются, пишут memorysanitizer и считают, что в нормальном виде это отслеживается только в рантайме.

И что? А вот с определением о неиспользованности далее прогресс тоже примерно такой же как с прошлым пунктом.

На это я ответил выше.

Это прямо бич какой-то. Все помешались с malloc/free. Как-будто любые другие функции не могут вернуть указатель на полную фигню, где NULL — самое приятное из этой фигни.

Хорошо, давайте переформулируем.

// Ворнинг: убедитесь, что указатель b валиден и указывает на живой в данный момент объект.

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

Эти языки показывают ошибки кодера пользователю.

А топикстартер со своими сегфолтами чего хочет?

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

Какие еще нюансы знает компилятор но не анализатор? У них одно и то же, только фокус внимания разный.

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

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

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

А топикстартер со своими сегфолтами чего хочет?

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

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

для реальных и прочих режимов в языке есть ключевое слово asm

Так я и говорю: забудем, что си изначально был написан для создания Unix, ну и заодно других системных вещей, потому что до этого альтернативы ассемблеру в этом сегменте не было. И снова вернёмся к ассемблеру, а си будем считать немного улучшенной или ухудшенной (в зависимости от критериев) явой.

Кен Томпсон гомерически хохочет и Деннис Ритчи переворачивается в гробу.

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

C был макроассемблером в 70-е,

Это когда такое было? Си с самого начала был языком высокого (или, если угодно, «среднего») уровня.

и тут мелькал говнокод который тогда писали.

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

Да, у меня тоже в свое время порвался не один шаблон, когда я взялся за стандарт си.

У меня тоже.

Но я не владею такой магией, чтобы переводить стрелки на компиляторщиков.

А если у тебя текстовый редактор упадёт из-за встреченного в тексте некорректного символа UTF-8, ты тоже будешь считать, что это нормально? Напишешь баг-репорт, а тебе ответят: проблема не в нашем редакторе, а в вашем тексте. У нас всё в соответствии со стандартом. И ты успокоишься и продолжишь пользоваться таким редактором?

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

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

по стандарту нельзя полагаться на то, что при целочисленном переполнении

У тебя беззнаковые числа, поэтому можно.

Ok. Если взять знаковые, то будет ещё эпичнее. Впрочем, PPP328 предложил намного лучший вариант. Но и он генерит лишнюю арифметическую операцию, что, впрочем, не так критично. Хотя и его код с отрицательными числами корректно работать не будет.

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

И опять нет. Любое переполнение при сдвиге является ub.

Почитайте стандарт, переполнение беззнаковых - не UB.

Да. Я был не прав. Но и вы не правы. Пункт 6.5.7 C99:

4 The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1×2^E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and nonnegative value, and E1×2^E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.

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

А вот и нет. Не знаковые, а только отрицательные и только влево. Сдвиг отрицательных чисел вправо «зависит от реализации», что не совсем ub.

Все сдвиговые действия со знаковыми числами UB потому что как вы сказали «зависят от реализации».

П. 6.5.7:

5 The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1/2^E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.

3.4.1

1 implementation-defined behavior

unspecified behavior where each implementation documents how the choice is made

2 EXAMPLE An example of implementation-defined behavior is the propagation of the high-order bit when a signed integer is shifted right.

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.

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

Undefined behavior — это индульгенция на генерацию бреда.

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf :

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. Прерывать компиляцию и/или выполнение с обязательным выводом диагностических сообщений.

Но про индульгенцию на тихую недокументированную генерацию бреда здесь ничего нет.

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

Что же касается обращения к адресу 0 в реальном режиме, то по стандарту, ЕМНИП, pointer == 0 истинно если и только если pointer == NULL. Но стандарт, ЕМНИП, отнюдь не утверждает, что ((int*) sizeof(int)) - 1 == NULL в обязательном порядке.

Стандарт утверждает, что любой указатель на объект, не являющийся массивом, можно рассматривать, как массив из 1 элемента. А ещё он утверждает, что при вычитании из указателя на элемент массива числа мы должны получить указатель на элемент, индекс которого меньше индекса этого элемента на вычитаемое число. Т. е., по сути, он утверждает, что &obj-1==(void*)&obj-sizeof(obj). Применив это правило к выражению ((int*) sizeof(int)) - 1 получим ((void*)sizeof(int))-sizeof(int)==0.

Я уже не говорю о том, что не только разыменование NULL, но и любого недействительного указателя является ub. А значит и разыменование ((int*) sizeof(int)) - 1 — ub, чему бы оно там ни было равно.

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

Стандарт, ЕМНИП, даже не утверждает что NULL == 0.

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf :

6.3.2.3 Pointers

[skip]

3 An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. 66) If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

[skip]

66) The macro NULL is defined in <stddef.h> (and other headers) as a null pointer constant; see 7.19.

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

Но оптимизировать, а не портить!

бесполезно. они не понимают.

Думаю, польза всё-таки есть, а не только флуд. Вряд ли все сейчас согласятся с этой очевидной мыслью, но в голове что-то у кого-то может и останется. Как там было в фильме Inception?

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

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

Если вы пишете с UB вы даете команду компилятору «делай что хочешь».

Я уже отвечал на этот аргумент цитатой из стандарта и своими комментариями к ней. См. этот камент. Где там «делай что хочешь»? Там чётко прописаны только 3 возможных действия.

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

ISO/IEC 9899:201x

Ну это уже новый. А на старом так же? ANSI-90 который

http://read.pudn.com/downloads133/doc/565041/ANSI_ISO+9899-1990+[1].pdf :

6.2.2.3 Pointers

[skip]

An integral constant expression with the value 0, or such an expression cast to type void *. is called a null pointrer-constant.33 If a null pointer constant is assigned to or compared for equality to a pointer. the constant is converted to a pointer of that type. Such a pointer. called a null poinrer-. is guaranteed to compare unequal to a pointer to any object or function. Two null pointers. converted through possibly different sequences of casts to pointer types. shall compare equal

[skip]

33 The macro NULL is defined in <stddef.h> as a null pointer constant: see 7.1.6

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

сдвиг вправо

соответственно, UB состоит только и только в том

Там вообще никакого ub нет. См. мой камент.

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

Про реализацию сдвига вправо претензий нет. Есть претензии на то что на другой архитектуре отрицательные числа необязательно представлены как дополнение до 2.

7.20.1.1 Exact-width integer types

1 The typedef name intN_t designates a signed integer type with width N, no padding bits, and a two’s complement representation. Thus, int8_t denotes such a signed integer type with a width of exactly 8 bits.

2 The typedef name uintN_t designates an unsigned integer type with width N and no padding bits. Thus, uint24_t denotes such an unsigned integer type with a width of exactly 24 bits.

3 These types are optional. However, if an implementation provides integer types with widths of 8, 16, 32, or 64 bits, no padding bits, and (for the signed types) that have a two’s complement representation, it shall define the corresponding typedef names.

Т. е. без проблем. Если реализация такая, как везде, то эти типы должны быть и должны быть представлены именно дополнением до 2. Иначе этих типов нет, и код либо не компилируется, либо там реализована на уровне препроцессора (#ifdef) другая ветка. Никаких неопределённостей.

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

Это когда такое было? Си с самого начала был языком высокого (или, если угодно, «среднего») уровня.

Это уровень «чуть выше ассемблера», что многие называют макроассемблер. Но главное - это указание на прямолинейную компиляцию 1:1. Этого в си нет давным-давно.

И я не знаю, зачем ты поминаешь java в половине постов. Ее компилятор тоже делает сумасшедшие оптимизации, а JIT добавляет еще, stevejobs есть что про это рассказать. Предсказуемый язык один - (макро)ассемблер.

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

Да.

А если у тебя текстовый редактор упадёт из-за встреченного в тексте некорректного символа UTF-8, ты тоже будешь считать, что это нормально?

Падение компилятора - баг компилятора. А из ошибочного исходника закономерно генерируется ошибочная программа.

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

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

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

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

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

ignoring the situation completely with unpredictable results

Там чётко прописаны только 3 возможных действия

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

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

В `for (int i = 0; i <= m; i++)` UB нет. Его принудительно создает компилятор в процессе своих вычислений.

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

3.4.3

1 undefined behavior

[skip]

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

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

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

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

Это не только абсурдно, но и противоречит стандарту.

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

написанной в соответствии со стандартом 0xFFFFFFFF00000000L

По крайней мере это не ub, а в худшем случае ошибка компиляции. Но если быть совсем дотошным, то можно так: 0xFFFFFFFF00000000ULL. Впрочем, меня уже здесь справедливо поправили, что переполнение unsigned не создаёт ub. Плюс PPP328 предложил тебе намного лучший вариант, который, правда, всё равно делает лишнее сложение (да, это копейки, но вместо 1 операции сложения делать 2 — это проигрыш в 2 раза, ну пусть в 1.5 с учётом того, что в обоих случаях при проверке добавляется сравнение, все операции примерно одинаковой «весовой категории»). Ну и ещё этот код, как и мой, работает только когда оба операнда положительные.

if((c=a+b)<a) // неопределённое поведение.

А существует ли в действительности подобный мразотный оптимизатор, который вырезает такие проверки?

Вот именно такую проверку у меня ни один из 2 оптимизаторов (gcc и clang) не вырезал. А проверку x+1<x выпилил gcc. clang, как ни странно, в дпнном случае оказался умней, не смотря на запугивания разработчика clang'а Криса Латтнера:

Signed integer overflow: If arithmetic on an 'int' type (for example) overflows, the result is undefined. One example is that «INT_MAX+1» is not guaranteed to be INT_MIN. This behavior enables certain classes of optimizations that are important for some code. For example, knowing that INT_MAX+1 is undefined allows optimizing «X+1 > X» to «true». Knowing the multiplication «cannot» overflow (because doing so would be undefined) allows optimizing «X*2/2» to «X». While these may seem trivial, these sorts of things are commonly exposed by inlining and macro expansion.

Вот что получилось у меня:

#include <stdio.h> 
#include <limits.h>

int main()
{
 int x=INT_MAX;
 if(x+1>x)
  printf("x+1>x\n");
 if(x+1<x)
  printf("x+1<x\n");
 return 0;
}
$ gcc -o int_overflow_ub int_overflow_ub.c
$ ./int_overflow_ub
x+1<x
$ gcc -o int_overflow_ub -O2 int_overflow_ub.c
$ ./int_overflow_ub
x+1>x
$ clang -o int_overflow_ub int_overflow_ub.c
$ ./int_overflow_ub
x+1<x
$ clang -o int_overflow_ub -O2 int_overflow_ub.c
$ ./int_overflow_ub
x+1<x
$ clang -o int_overflow_ub -O3 int_overflow_ub.c
$ ./int_overflow_ub
x+1<x

Как видим, gcc без оптимизаций всё делает правильно, а с оптимизацией предполагает, что x+1 всегда больше x. clang же и без оптимизации, и с нею, видимо вычислил это выражение, и не делает таких дурацких предположений. Однако, как я читал где-то в Сети, в некоторых случаях всё-таки может их сделать.

Однако, как я писал в верхнем посте, против такой оптимизации я особо не возражаю, т. к. проверить x на INT_MAX проще, сравнив с INT_MAX, зато разные макросы действительно могут генерить подобного рода код, и вот их-то можно оптимизировать.

Но проблема в том, что, ссылаясь всего лишь на недопример из стандарта, разработчики компилятора могут и if((c=a+b)<a) интерпретировать как хотят, могут даже диск отформатировать, ссылаясь на ub и игнорируя п. 3.4.3 того же стандарта. Более того, они теоретически могут это сделать и без включения режима оптимизации, т. к. про оптимизацию в стандарте вообще ничего нет.

Я тут вообще феерический пример для g++ нашёл:

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

Этот код при компиляции с -O2 в современном g++ приводит к бесконечному циклу. Неожиданно, правда?

Я проверил, действительно приводит.

Но, правда, перед этим во время компиляции честно предупреждает:

$ g++ -o infinity_loop.cpp -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++)
     ^

Т. е. i даже нигде не меняется, кроме как в условии цикла, но просто из-за вывода внутри цикла временных значений, которые при i==174 переполняются, цикл становится бесконечным. :-)

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

Потому что писать надо без UB.

Ага, а потом поменяли тип переменной с uint32_t на uint64_t, и ищи-свищи по всем исходникам подобные конструкции.

Там вообще на самом деле никакой проблемы нет, если рассматривать это ub как следствие разной реализации знаковых целых, потому что в соответствии с п. 7.20.1.1 стандарта типы intN_t должны быть или чётко заданной длины с дополнением до 2, или, если аппаратная реализация не позволяет этого сделать, вообще не должны быть определены. В этом случае будет ошибка компиляции, а не ub. Всё чётко.

Формальная проблема всё-таки есть: эти типы реализованы в заголовке как typedef всё тех же signed char, short, int и long либо long long, а потому вроде как и не отличаются от последних. Но компилятор-то видит исходник. Почему не реализовать такую фичу: если переменная в тексте программы описана как intN_t, то никакого ub даже при знаковом переполнении этой переменной не происходит?

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

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

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

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

Но оптимизировать, а не портить!

А это вообще можно описать достаточно формально, чтобы по описанию можно было построить оптимизатор?

Ну, во-первых, помимо формальностей есть ещё здравый смысл. Если многие на чём-то спотыкаются, то и разработчикам компиляторов есть повод задуматься. Вот здесь большинство говорит, что де пусть быдлокодеры страдают, а мы типа тру-программисты, нам от этого только лучше. Но практика говорит об обратном:

Обнаружена уязвимость в GDK-Pixbuf затрагивающая Chromium, Firefox и VLC. Александр Панасенко 31 августа 2017 - 10:56:

Первая уязвимость вызвана целочисленным переполнением в функции tiff_image_parse, которое при обработке специально оформленного файла в формате TIFF может привести к переполнению кучи. Опасность проблемы смягчает то, что она проявляется только при сборке библиотеки с флагом оптимизации "-O3", который редко применяется для финальных сборок. Примечательно, что в коде библиотеки есть проверки на переполнение, но при использовании флага "-O3" компилятор (проверено в GCC 6.1 и Clang 3.8, также имеются сведения об удалении проверок и при флаге "-O2") относит их к коду с неопределённым поведением («Undefined Behavior») и удаляет как излишний код.

Здорово! Разрабы компиляторов придумывают всё более и более безумные оптимизации, а пользователи этих компиляторов «редко применяют для финальных сборок» такие режимы, чтоб обезопаситься от уязвимостей, но уязвимости их всё равно настигают даже с флагом -O2!

Fun with NULL pointers, part 1. By Jonathan Corbet July 20, 2009:

static unsigned int tun_chr_poll(struct file *file, poll_table * wait)
    {
	struct tun_file *tfile = file->private_data;
	struct tun_struct *tun = __tun_get(tfile);
	struct sock *sk = tun->sk; // Fix!
	unsigned int mask = 0;

	if (!tun)
	    return POLLERR;

The line of code which has been underlined above was added by Herbert's patch; that is where things begin to go wrong. Well-written kernel code takes care to avoid dereferencing pointers which might be NULL; in fact, this code checks the tun pointer for just that condition. And that's a good thing; it turns out that, if the configuring ioctl() call has been made, tun will indeed be NULL. If all goes according to plan, tun_chr_poll() will return an error status in this case.

But Herbert's patch added a line which dereferences the pointer prior to the check. That, of course, is a bug. In the normal course of operations, the implications of this bug would be somewhat limited: it should cause a kernel oops if tun is NULL. That oops will kill the process which made the bad system call in the first place and put a scary traceback into the system log, but not much more than that should happen. It should be, at worst, a denial of service problem.

There is one little problem with that reasoning, though: NULL (zero) can actually be a valid pointer address.

[skip]

So, in this case, running SELinux actually decreased the security of the system.

Получается, что и chromium, и firefox, и даже ядро Linux пишут исключительно быдлокодеры. Но пусть даже так. Страдают-то от этого не только они, но и обычные юзеры. А если говорить об уязвимостях в ядре ОС, то, в зависимости от предназначения машины, на которой запущена эта ОС (а в статье речь шла ни много ни мало о RedHat с SELinux, а не о домашней рабочей станции с Федорой), последствия могут быть очень серьёзными. А компилятор с опцией оптимизации, фактически, сам генерит эти уязвимости. И зачем такой компилятор? Да лучше бы он не так здорово оптимизировал (это, в конце концов, может сделать и программист), но создавал более безопасный код или хотя бы предупреждал, что намерен выкинуть проверку на NULL.

А вот ещё одна статья на Хабре от 6 апреля с. г. об одной из самых надёжных ОС «Как найти 56 потенциальных уязвимостей в коде FreeBSD за один вечер». Ну FreeBSD-то точно одни быдлокодеры пишут, если он за вечер нашёл 56 потенциальных уязвимостей!

Однако, отвечая на вопрос о формализации, в стандарте это как раз формализовано в п. 3.4.3.

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

Этот код — полная дичь. Он будет вылетать и с оптимизациями, и без них, потому что <...>

Этот код не должен работать вообще. Это иллюстрация.

Иллюстрация чего? Полностью нерабочего кода или ub? Если 2-ого, то код должен выполняться и генерить ub, которое можно увидеть.

Оптимизатор предполагает, что P указывает только на массив float. А это значит, он не может указывать на то место, где хранится само значение P. Поэтому в zero_array можно предположить, что P не поменяется.

Это понятно. Но как это согласуется с утверждением автора статьи о том, что никогда нельзя преобразовывать int* во float*:

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

Если в коде float* преобразуется во float**?

И кстати, тут за обсуждениями ub никто так и не ответил на мой более частный вопрос:

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

Мне действительно интересно, стоит писать баг-репорт или так и должно быть?

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

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

Т. е. разработчики firefox, chromium, ядра Linux и FreeBSD все поголовно слабоумные? Но даже если так, почему пользователи этого ПО должны страдать из-за того, что разработчики компиляторов усугубляют ошибки этих «слабоумных»? А ведь RedHat с SELinux и FreeBSD обычно ставят не для игрушек.

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

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

Отчасти на это сейчас есть undefined sanitizer

И это роспись в том что статично задача сейчас не решается.

Как всегда: сначала создаём проблему на пустом месте, а потом героически её решаем. :-)

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

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

Вы не поверите, но даже в защищенном режиме на PDP-11, чтобы тут скотинко не говорил, происходит ровно тоже самое. А PDP-11 и есть та машинка, на которой Си родилось.

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

Примечательно, что в коде библиотеки есть проверки на переполнение, но при использовании флага "-O3" компилятор (проверено в GCC 6.1 и Clang 3.8, также имеются сведения об удалении проверок и при флаге "-O2") относит их к коду с неопределённым поведением («Undefined Behavior») и удаляет как излишний код.

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

bytes = height * rowstride;
        if (bytes / rowstride != height) { /* overflow */
Если я правильно понимаю, они сначала допускают возможное переполнение, а потом проверяют случилось ли оно. То есть на момент проверки UB уже произошло. В чём смысл тогда такой проверки? Я тогда немного поискал по сети и нашёл, что проверку на переполнение делают немного не так, например для умножения:
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 */
Источник

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

все предположения о том что будет за пределами defined behavior - это их личная фантазия

Это не фантазия, а стандарт, п. 3.4.3.

Если нужно чтобы участок кода гарантированно не оптимизировался - есть такая замечательная штука как volatile.

#include <stdio.h>
#include <limits.h>

int main()
{
 int volatile x=INT_MAX;
 if(x+1>x)
  printf("x+1>x\n");
 if(x+1<x)
  printf("x+1<x\n");
 return 0;
}
$ gcc -o int_overflow_ub -O2 int_overflow_ub.c'
$ ./int_overflow_ub
x+1>x
x+1<x

Без volatile этот же код, откомпилированный gcc с опцией -O2, выдавал просто x+1>x, а с volatile совсем сбрендил. :-)

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

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

asm __volatile__

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

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

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

Как говорил мой препод, в программирование приходят либо из математики, либо из железа. На границе между ними и возникает вся эта драма :D

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

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

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

Как всегда: сначала создаём проблему на пустом месте, а потом героически её решаем. :-)

Не на пустом месте, но в целом да. То что си - ад, многие понимали уже давно. Поэтому санитайзеры, поэтому новые языки, поэтому в том же clang диагностика сразу была на новом уровне (и gcc подтянулся). А ты что предлагаешь?

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

Тебе нужно больше предупреждений, это понятно.

Но ведь могут, когда хотят:

#include <iostream>
int main()
{
    for (int i = 0; i < 300; i++)
        std::cout << i << " " << i * 12345678 << std::endl;
}
$ 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++)
     ^

Скажи, ты код всегда с -Weverything собираешь, да?

Я всегда собираю с опцией -Wall, если это мой код. Если чужой и выдаёт кучу предупреждений, то без неё.

Только вот печалька:

#include <stdio.h>
#include <limits.h>

int main()
{
 int volatile x=INT_MAX;
 if(x+1>x)
  printf("x+1>x\n");
 if(x+1<x)
  printf("x+1<x\n");
 return 0;
}
$ gcc -o int_overflow_ub -O2 -Wall int_overflow_ub.c
$ ./int_overflow_ub
x+1>x
x+1<x

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

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

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

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

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

Мне кажется абсурдным, что кодер молча выдаёт UB.

Ясно дело, что все тру-кодеры собрались на ЛОРе, а Linux, FreeBSD и Firefox с Chrom'ом пишут такие абсурдные быдлокодеры.

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

Тогда вопрос снят.

И напрасно. Я бы не снимал:

#include <stdio.h>
#include <limits.h>

int main()
{
    signed x = INT_MAX;
    ::printf("x = %i\n", x);
    if (x > x+1)
    {
        ::printf("overflow\n");
    }
    else
    {
        ::printf("x > x + 1 <- false\n");
    }
    return 0;
}
$ clang t.cpp -O3 -o t
$ ./t
x = 2147483647
overflow

Но:

$ gcc t.cpp -O3 -o t
$ ./t
x = 2147483647
x > x + 1 <- false

:-)

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

Юродивый продолжает юродствовать.

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

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

он должен это сделать.

Нет, не должен. С чего это ты взял, что компилятор тебе что-то должен, помимо того, что написано в стандарте. Не нравится дефолтное поведение? Не хватает правил проверки? Запили pull-request.

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