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)
Ответ на: комментарий от aureliano15

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

То, где можно «ignore the situation with unpredictable results». Например, если указатель где-либо разыменовывается, ситуацию, что он будет равен NULL, можно проигнорировать и все дальнейшие провери на неё выбросить.

Стандарт утверждает, что любой указатель на объект, не являющийся массивом, можно рассматривать, как массив из 1 элемента.

Ну допустим.

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

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

&obj-1==(void*)&obj-sizeof(obj)

Вот чего уж точно по стандарту делать нельзя, так это арифметики на (void *).

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

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

gcc-trunk:

<source>: In function 'int main()':
12 : <source>:12:1: error: assuming signed overflow does not occur when assuming that (X + c) < X is always false [-Werror=strict-overflow]
 }
 ^
cc1plus: all warnings being treated as errors
Compiler exited with result code 1
maverik ★★
()
Ответ на: комментарий от vzzo

1) В процессорах itanium регистры были 65-битными, последний бит назывался NaT и процессор ронял программу при попытке доступа к регистру с таким проставленным битом. Только это разрушает наивное представление местных борцов с компиляторами о том, что в переменной могут быть только какие-то битики, имеющие какие-то значения.

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

Во-вторых, что мешало сделать переполнение со знаком не ub, а implemented behavior? Не так уж много существует вариантов поведения.

В-третьих, даже если ub. Ну и пусть этот код корректно выполняется на большинстве платформ. А на Itanium'е при первой же отладке громко упадёт, а не будет тихо «форматировать диск».

Ну и в-четвёртых, при использовании intN_t проблемы вообще не стоит. См. п. 7.20.1.1 стандарта и другой мой камент по этому поводу.

Разыменование указателя *p — это такое же сообщение компилятору о диапазоне значения p, как и if (!p) return.

Э нет. Несколько идущих подряд if(!p) очевидно не нужны (если только p не volatile), т. к. после 1-й проверки p присваиваются ненулевые константы. Это может быть ошибкой, но даже в этом случае логика никак не поменяется, если удалить 3 последних проверки.

Логика же в случае ошибочного разыменовывания недействительного указателя (кстати, необязательно 0) реально поменяется, что может привести к уязвимостям, куда более серьёзным, чем копеечный выигрыш на 1 или даже 3 лишних проверках, которые программист и без оптимизатора может убрать (уж если порой не очевидное ub может, то очевидные лишние проверки и подавно).

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

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

.LC0:
  .string "x+1>x"
.LC1:
  .string "x+1<x"
main:
  push rbp
  mov rbp, rsp
  sub rsp, 16
  mov DWORD PTR [rbp-4], 2147483647
  mov eax, DWORD PTR [rbp-4]
  add eax, 1
  cmp DWORD PTR [rbp-4], eax
  jge .L2
  mov edi, OFFSET FLAT:.LC0
  call puts
.L2:
  mov eax, DWORD PTR [rbp-4]
  add eax, 1
  cmp DWORD PTR [rbp-4], eax
  jle .L3
  mov edi, OFFSET FLAT:.LC1
  call puts
.L3:
  mov eax, 0
  leave
  ret

и icc 17

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

main:
  push rbp #5.1
  mov rbp, rsp #5.1
  sub rsp, 16 #5.1
  mov DWORD PTR [-16+rbp], 2147483647 #6.7
  mov eax, 0 #7.2
  cmp eax, 1 #7.2
  je ..B1.3 # Prob 100% #7.2
  mov eax, offset flat: .L_2__STRING.0 #8.3
  mov rdi, rax #8.3
  mov eax, 0 #8.3
  call printf #8.3
  mov DWORD PTR [-12+rbp], eax #8.3
..B1.3: # Preds ..B1.6 ..B1.1
  mov eax, 0 #11.9
  leave #11.9
  ret #11.9
maverik ★★
()
Ответ на: комментарий от i-rinat

Думаю, ты не представишь себе, сколько будет этих предупреждений

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

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

Будь что будет

А как же с 1-й частью этой фразы: «делай, что должен»? «Что должен», а не разное непотребство!

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

И что самое интересное, gcc-7.2 тоже выдает этот warning, если собирать с -O3, если без - то нет (остальные флаги оставались неизменными: -Wall -Wextra -pedantic -Werror -std=c++17)

Еще интереснее. Шланг выдает (без -O3) код такой же, как и gcc, а вот что с -O3:

main: # @main
  push rax
  mov edi, .Lstr
  call puts
  xor eax, eax
  pop rcx
  ret
.Lstr:
  .asciz "x+1<x"
maverik ★★
()
Ответ на: комментарий от Iron_Bug

вопрос в цене реализации

А цена — серьёзные уязвимости, ссылки на некоторые я уже кидал.

этих второстепенных плюшек.

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

Да и цена плюшек не велика. В этом каменте приведён код бесконечного (из-за ub, и совсем не такого тривиального ub, как банальное разыменование NULL) цикла и предупреждение 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++)
     ^
aureliano15 ★★
() автор топика
Ответ на: комментарий от aureliano15

Я не думаю, что имеет смысл что-то доказывать или объяснять этим людям. Надо просто понять что вы и они думаете вообще по разному. Вы можете говорить А, имея ввиду понятие Х1, а человек во-первых не всегда способен принять А как А, он может и B услышать, а во-вторых, у него А соответствует в голове Y5.

Диалог закончился, не начавшись. Более того, диалог возможен например в stevejobs, который противник С++, говном поди его считает. Но с ним можно дискутировать, то есть ваше Х1 у него в момент «приёма» может стать Х1а, или Х1.4.3b. Но не Y5.

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

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

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

Кроме того, я заметил уже довольно давно, что есть люди, которые скажем так стремятся быть «правильными». То есть они полностью одобряют «общепризнанно-правильные» точки зрения и идеи, гневно осуждают «общепризнанно-плохие» точки зрения и идеи. Действуют строго в «заданных обществом рамках» и очень агрятся когда кто-то за них выходит. Причем все вышеперечисленные понятия в кавычках - они ведь не абсолютная истина, а как правило просто централизовано-транслируемые установки(через СМИ, образование, и т.д.). Надо понимать, что их логика подчинена не формальным законам логики, а необходимости не выходить за границы им разрешённого.

Мозг ведь - он не решает задачи в рамках формальной логики. Он решает задачу согласно тому набору реакций, который в него прошит. Лишь небольшое количество «мутантов» может выходить за рамки «прошивки». Как в опыте с выученной беспомощностью, когда ~15% собак могли перебороть «прошивку» и убежать от опасности когда появилась возможность, а остальные «прошились» и ничего не делали. Этот опыт даже не совсем верно интерпретируют: это очень частный случай общего: когда животные не могут сами создавать новые поведенческие шаблоны в принципе, а только следуют выученным. 15% могут, остальные нет. Как частный случай - не могут убежать от удара током даже когда преграды нет. Вот кстати интересная статья на тему того, что среди людей выделяются разные классы, почти как среди муравьёв(воины, няньки, и т.д.):

http://igor-grek.com/publ/antropologia/evgenika/5-1-0-593 (*)

Наконец, еще один момент тоже важный, который лучше всего продемострировать на примерах. Многие мне пишут про эфир, в духе «ололо», т.к. я подобные темы часто поднимаю там и сям. Ну хочу и буду, не ИГИЛ же рекламирую. Тема всегда оказывается конфликтогенной. Я сколько уже спрашивал условных оппонентов - например: почему у вас вместо уравнений Максвелла(ссылка на оригинал прилагается) какая-то лабуда в частных производных? у вас ошибка же получится вот тут и тут. нельзя менять полную производную на частную. Люди тупо сливаются в два-три хода и начинают придуриваться. Это происходит потому что формальная логика говорит что нельзя нарушать законы матана, но если ей следовать, эти люди выйдут за рамки разрешенного им думать. Этот стимул у них основной, а возразить по существу не дают правила дифференцирования из матана и оригинал работы Максвелла. Поэтому раз за разом начинается придуривание - это защитная реакция психики на попадание в недопустимое состояние. Тоже самое происходило когда я например сообщал общеизвестные сведения о человеке с никнеймом mirin dajo. Люди, видя что реальность выходит за рамки дозволенного для них, просто уходят в отрицание реальности и/или начинают придуриваться.

Собственно, шизофрения по Бэйтсону это выученная защитная реакция на постоянное нахождение в «запрещённой зоне». У неё есть три разновидности, и гебефрения - это как раз уход в придуривание, кататония - отключение от реальности(отрицание её), паранойя - защита через принятие любого сообщения недостоверным. Как показывает практика, эта защита врубается и без диагнозов. Она скорее всего встроенная в человека изначально, и в форме болезни выступает только если в детстве психику поломали ломом дебилы-родаки.

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

1)с ним можно будет вести диалог и он будет вас понимать достаточно «близко к тексту», а если разговор выйдет за пределы его компетенций, то он сможет поддерживать условно. То есть «ну допустим вы правы».

2)он не будет демонстрировать вышеописанные «шизоидные» поведенческие шаблоны, т.е. придуриваться и т.п. Он скорее прекратит общение с неинтересным собеседником.

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

*)я выше примечание пометил возле ссылки. Уверен что идиоты прочитают только ссылку и начнут писать «о, там написано евгеника, не буду читать вы все дураки а я д'артаньян». Я собственно в данном посте объяснил почему так происходит.

Применительно к сабжу, вы писали:

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

Нееееет. Только 15% собак могут перепрыгнуть перегородку в опыте с выученной беспомощностью. А остальные не поддерживают этот функционал аппаратно. У них реально стандарт это абсолютная истина ибо она «общепринята», и раз там написано что ничего не гарантируется, то значит это надо понимать буквально(приводится в пример тест с компилятором), потому что «все так думают». Вы им сообщаете нечто, что для них попадает в запрещённую зону, и эту идею вы просто не загрузите им.

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

UB же нужно только для оптимизаций.

А вот в стандарте написано совсем другое.

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

Ну вот и сделали бы сдвиги implementation behaviour.

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

который противник С++, говном поди его считает.

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

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

stevejobs ★★★★☆
()
Последнее исправление: stevejobs (всего исправлений: 1)
Ответ на: комментарий от i-rinat

алсо. насчет вот этих вот int и UB.

А вот например, пусть например компилятор поддерживает единицы измерения. Например:

double human_height /***meters***/;
double hat_height /***centimeters***/;
double speed /***meters/second***/;

if (door_height < human_height+hat_height) //размерность будет подогнана
{
   бдыщь об дверной косяк;
}
double x = hat_height + speed;//error
и до кучи допустимые значения можно декларировать:

int num_elements /***1***/;//количество единиц int pixel_offset /***-100000~100000***/; ///разумный диапазон

и допустим еще можно контрольные точки объявлять. например

try {
   код;
} catch(...) {
   ...
} invalid {
   ... //поведение алгоритма "код" не определено 
}

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

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

Хорошо мыслишь, но:

1. Видишь ли ты что находишься ровно в том же состоянии? Ты пишешь (и всегда писал) на языке, который существует только в твоей голове. Он хороший и правильный, но это ли не шиза? А когда происходит столкновение с реальностью, твой шаблон рвется и происходит поиск виноватых, будь то жидокодеры или жабарептилоиды.

2. С каких пор программирование строго по спекам считается чем-то плохим? «Вы точно программист?» (с) Тут прямо напрашивается шутка про качество драйверов AMD.

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

не понимает смысла „Undefined behavior“ в стандартах C/C++ и зачем это нужно.

Пусть посмотрит в п. 3.4.3 стандарта C99.

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

Я не специалист по разработке компиляторов, поэтому не знаю, кто чего там знает, но то, что g++ может предупреждать о куда менее тривиальных вещах, чем простое разыменование NULL-указателя, это точно:

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

Вот только, к сожалению, о тривиальных ub он молчит даже при использовании опции -Wall.

Источник здесь, мой более подробный камент об этом примере здесь.

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

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

поддерживает единицы измерения

Уже (man user-defined literals).

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

Одно из решений: класс с operator=(), там проверишь и кинешь. Если границы заранее известны, то static_assert и т.п.

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

Пусть посмотрит в п. 3.4.3 стандарта C99

Да хорош уже туда других отсылать, ты сам внимательно перечитай.

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

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

компилятор это инструмент. он применяется для получения некоторого результата. и поэтому он должен мне.

хотя кому я это говорю. у тебя компилятор служит божеству «стандарту» и должен только ему.

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

а вот я с тобой согласен, что надо бить морду разрабам стандарта

Ну, в стандарте, конечно, могли бы сдвиги, целочисленное переполнение и разыменование NULL-указателя (да и вообще недействительного указателя, ведь если он указывает на системную область, то может быть действительным, даже если компилятор об этом не знает) сделать implementation и unspecified defined. Но даже с ub всё не так страшно, если бы разрабы компиляторов следовали п. 3.4.3 этого самого стандарта, на который они так любят ссылаться.

и компиляторов

А вот этим точно надо надавать.

ты это более красиво сформулировал где-то ранее

Здесь.

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

компилятор это инструмент. он применяется для получения некоторого результата. и поэтому он должен мне.

Нет, не должен. Как минимум хотя бы потому, что

THE SOFTWARE IS PROVIDED «AS IS», WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.

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

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

Он хороший и правильный

Нет, он такой какой есть. У него есть плюсы и минусы. некоторые минусы ну просто пц.

Опять же вопрос поднят не про стандарт, а про то, как должен вести себя компилятор в случае попадания в подобную ситуацию. если компилятор использует UB, то он же это делает не в бессознательном состоянии. он обнаружил возможность UB, а значит программист что-то написал не по стандарту. И компилятор должен спросить «Вы точно программист?». Если мы требуем от человека писать по стандарту, то отклонение от стандарта - это уже ошибка. Ошибочный код не должен компилироваться, даже если формально может.

Компилятор же инструмент человека, а не идолище поганое, служащее богу «стандарту».

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

Это лицензия. Она вообще для юристов написана. Вы прямо эталонно перешли в совершенно другую область, поскольку аппаратно не можете воспринимать запрещённые для вас к думанию темы.

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

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

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

Компилятор — не статический анализатор.

Вообще-то компилятор всегда выдавал ошибки и предупреждения.

Ни в C, ни в плюсах сегфолтов не существует.

Сегфолт существует в Linux. А в DOS вместо сегфолта я спокойно заменю вектор 0-ого прерывания на свой. Но зачем специально создавать уязвимости на ровном месте, я не понимаю.

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

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

А по факту ты же просто играешь в демагогию тут: нож тоже обязан тебе не резать пальцы, когда ты это делаешь явно?

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

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

А я нашёл.

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

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

Хочет получить наконец безопасные ОС и прочие продукты, без уязвимостей или хотя бы со значительно меньшим их числом.

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

Да. Ты правильно говоришь. А если что - вперёд на машине времени во времена PDP-11 и Си без стандарта.

Си без стандарта никогда не было. С самого начала был неофициальный, никем не утверждённый стандарт, называемый в народе K&R. Потом появился C89, потом C99, потом C11.

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

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

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

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

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

В компиляторе нет такого места, где написано: «о, тут вызов по нулевому указателю, заменю-ка я его на рандом». Он собирает и сопрягает инфу постепенно, ты это знаешь. Он видит: (1) вызов по указателю -> это не ноль, (2) указатель либо ноль, либо EraseAll, (3) делаем выводы. На каждом шаге нет UB. Какой из них ты хочешь увидеть в логе? Представляешь ли сколько будет записей на каждом из них?

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

Во-вторых, что мешало сделать переполнение со знаком не ub, а implemented behavior? Не так уж много существует вариантов поведения.

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

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

а уже заметил, что ты не можешь воспринимать запрещённые для тебя понятия.

Но всё таки, у нас тут речь идёт о компиляторах и стандартах. Это технический а не юридический вопрос. У нас тут вообще-то не форум юристов, а сайт посвященный техническим вопросам, связанным с ОС линукс. Если компилятор не следует стандарту(допустим GCC где-то косячит), могут ли его разрабы не фиксить баг потому что у них в лицензия с отказом от гарантий?

Причем могут не в юридическом смысле(т.е. имеют обязанности и могут нести ответственность) а в том плане могут демонстративно отказаться фиксить баг и это будет воспринято воспринято сообществом как норма.

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

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

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

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

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

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

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

Опиши этот здравый смысл в качестве алгоритма для компилятора.

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

так вот. будет ли такой компилятор по вашему лучше, чем существующие?

По-моему, это ты язык F# изобрёл.

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

херня, а не аргументы.

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

и где ты реальный режим x86 в последний раз видел живьём? я в последний раз его видела, когда школу закончила. с тех пор лет 25 прошло уже. а в микроконтроллерах лохи код не пишут.

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

Иллюстрация чего?

Иллюстрация идеи. Автор пишет то же самое словами. Код там просто как ещё один способ выражения.

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

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

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

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

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

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

аппаратно не можете воспринимать запрещённые для вас к думанию темы

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

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

могут демонстративно отказаться фиксить баг и это будет воспринято воспринято сообществом как норма.

Могут, прецеденты в опенсорсных продуктах есть. Как будет воспринято зависит от воспринимающего. Как известно, у нас люди любят оскорбляться по любому поводу.

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

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

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

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

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

это восхитительный тред на самом деле. сколько там постов прошло, 30 кажется. даже страница не сменилась, а люди уже набегают и наглядно собою иллюстрируют.

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

вообще-то утверждалось другое. а именно:

1)переменной может быть присвоено два варианта значений: А и Б.

2)переменная используется способом Х, комбинация которого с вариантом значения Б считается так называемым «неопределённым поведением».

3)компилятор посчитал вариант (Б & Х) невозможным, и исключил вариант значения Б из списка.

4)список сократился до одного значения, и компилятор принял переменную за константу, что и привело к инлайнингу функции.

если бы список вариантов содержал три значения А, Б и В, и комбинации значений А и В с способом использования Х были бы допустимыми, пункт 4 бы не наступил и у нас было бы обращение к нулевому указателю. В чем, собственно можно и убедиться:

https://godbolt.org/g/7VRtFZ

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

2)переменная используется способом Х, комбинация которого с вариантом значения Б считается так называемым «неопределённым поведением».

Переменная используется способом X, откуда следует, что её значение не равно Б.

3)компилятор посчитал вариант (Б & Х) невозможным, и исключил вариант значения Б из списка.

Компилятор посчитал (Б & ¬Б) невозможнным (всегда false) и был абсолютно прав.

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

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

Для тебя указатель не равен нулю не по каким-то логическим законам

Я же обратное утверждал: что указатель равен нулю до вызова NeverCalled. Ну как же так-то.

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

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

Я же обратное утверждал

Ок, обратное. Но по-прежнему ты не смог формально показать неправоту вывода. А ушёл в придуривание про «nullptr != 0» (Вызов никогда не вызываемой функции (комментарий)), что легко опровергается https://wandbox.org/permlink/LBRhdRAFXzDwcHtu

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