Язык Си - один из наиболее влиятельных языков программирования за всю историю. Он стал незаменимым инструментом разработки операционных систем, сместив с этого пьедестала языки ассемблера. Изучение Си обязательно для любого уважающего себя программиста. Этот язык любим за свою внешнюю простоту и ненавидим за беспощадность к ошибкам. Благодаря ему у нас есть ядро Linux и тысячи уязвимостей в нём же в придачу. Попробуем понять, что же такое этот противоречивый язык Си - благословение или проклятие?

История языка Си берёт свое начало в недрах американской компании Bell Labs и тесно связана с судьбой операционной системы UNIX. Ее создатели, Кен Томпсон и Деннис Ритчи, разрабатывали свой проект для компьютеров PDP-11, и первые два года их основным инструментом был язык ассемблера. Трудоёмкость написания машинного кода вынуждала искать ему замену, которой в конечном итоге и стал язык Си. С его помощью было полностью переписано ядро операционной системы и большая часть утилит. Язык Си позволял создавать эффективные низкоуровневые программы на PDP-11, практически не используя при этом язык ассемблера.

Со временем встал вопрос портирования UNIX на новые аппаратные платформы. Использование языка Си значительно упростило эту задачу. Ведь если бы в разработке применялся только язык ассемблера, то тогда операционную систему пришлось бы переписывать под каждую компьютерную архитектуру. С другой стороны, исходники UNIX все еще содержали немало кода, созданного специально для компьютера PDP-11. Да и сам язык Си далеко не всегда точно отражал особенности и детали той или иной аппаратной платформы. Последнее еще больше затрудняло процесс переноса и лишало язык одного из его главных достоинств - прозрачной и понятной генерации машинного кода. Чем больше компьютерных архитектур захватывал Си, тем менее очевидной становились его связь с низким уровнем.

В процессе миграции UNIX на новые аппаратные платформы обнаружилась ещё одна проблема. Портированные программы на языке Си исполнялись медленнее, нежели можно было от них ожидать. Чем сильнее отличалась целевая компьютерная архитектура от PDP-11, тем менее эффективным был получаемый код. Чтобы скомпенсировать этот недостаток, разработчики компиляторов всё чаще стали применять неявные оптимизации. И хотя такое решение и улучшало производительность самих программ, Си всё больше отдалялся от низкого уровня. Теперь приходилось не только понимать, как именно определялись конструкции языка для каждой из компьютерных архитектур, но также и то, как они оптимизировались. Разумеется, любой компилятор самостоятельно решал, как именно транслировать исходный код для каждой аппаратной платформы. В итоге написать на языке Си низкоуровневую программу, независящую от используемого компилятора, стало практически невозможно.

Необходимо было понять, как эффективно реализовать высокоуровневые конструкции языка Си, сохранив при этом его низкоуровневые свойства. Попыткой решить эту проблему стала публикация в 1989 году первого стандарта языка. Его принято называть "ANSI C" или "C89", и именно на него мы будем ссылаться в дальнейшем. Создатели стандарта решили окончательно разорвать связь Си с архитектурой PDP-11 и сделать язык полностью высокоуровневым. Была введена так называемая "абстрактная машина" - воображаемый исполнитель кода на языке Си (раздел 2.1.2.3, "Program execution"):

Семантические описания в этом Стандарте описывают поведение абстрактной машины, в которой вопросы оптимизации не имеют значения.

Это означает, что оптимизации компилятора не будут влиять на работу программы, пока её исходный текст согласуется со стандартом. Абстрактная машина должна была решить две проблемы одновременно. Во-первых, следование стандарту давало возможность создавать легко переносимые программы на языке Си. Во-вторых, абстрактная машина могла предоставить компиляторам свободу для оптимизаций. Вот только возникает вполне резонный вопрос - а чем тогда язык Си отличается от любого другого компилируемого языка высокого уровня? Ответ кроется в тексте стандарта. Чтобы всё-таки дать теоретическую возможность программистам писать низкоуровневые процедуры, а значит непереносимые, было введено ещё одно понятие - неопределённое поведение (undefined behavior, раздел 1.6, "DEFINITIONS OF TERMS"):

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

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

Возьмём следующий фрагмент кода на языке Си:

int x = 1;
x = x << sizeof(int) * 8;

Попробуем предположить, какой результат у нас получится. Допустим, мы скомпилировали этот код для процессоров архитектуры ARM. Инструкция битового сдвига в рамках этой аппаратной платформы определена так, что итоговым значением переменной "x" должен быть "0". С другой стороны, мы можем транслировать нашу программу в машинный код архитектуры x86. И уже там битовый сдвиг реализован таким образом, что значение "x" не изменится и останется равным "1". Мы могли бы сделать вывод, что результат работы данного фрагмента кода зависит от того, для какой аппаратной платформы мы его скомпилировали. Но на самом деле это не так.

В действительности данный фрагмент кода может быть обработан компилятором любым возможным и невозможным образом. Причина в следующем: согласно тексту стандарта языка Си битовый сдвиг на величину, большую или равную размеру выражения в битах, является неопределённым поведением. Получается, нет никакой гарантии, что этот кусок кода вообще будет работать. В действительности, даже в рамках одной архитектуры один и тот же компилятор может сгенерировать совершенно разные исполняемые файлы. Приведём примеры компиляции и запуска программы с печатью значения переменной "x". В обоих случаях мы используем компилятор gcc версии 10.2.1 для целевой архитектуры x86-64.

$ cat test.c
#include <stdio.h>

int main()
{
    int x = 1;
    x = x << sizeof(int) * 8;
    printf("%d\n", x);
    return 0;
}
$ gcc test.c -o test
$ ./test
1
$ gcc -O test.c -o test
$ ./test
0

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

Рассмотрим чуть более сложный пример. Ещё одной разновидностью неопределённого поведения является разыменование нулевого указателя. Его тривиальным вариантом будет следующий фрагмент кода:

* (char *) 0;

Разумеется, никто в здравом уме не станет писать что-то подобное в своей программе. Однако совсем необязательно делать разыменование нулевого указателя явным образом, чтобы вызвать неопределённое поведение. В цикле статей "What Every C Programmer Should Know About Undefined Behavior" на сайте blog.llvm.org приводится фрагмент кода, подтверждающий это:

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

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

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

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

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

Так как мы разыменовываем указатель до его проверки, то компилятор спокойно решает, что сам указатель никогда не будет нулевым. Благодаря этому сравнение "p == 0" заменяется на выражение, всегда возвращающее ложь. Затем компилятор запускает первый механизм оптимизации и убирает "мертвый" код:

void contains_null_check(int *p)
{
    *p = 4;
}

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

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

Оптимизации компилятора могут затрагивать также и функции стандартной библиотеки языка Си, в том числе, к примеру, memset. Она широко и печально известна за обилие ошибок, которые допускают программисты при её вызове. Заголовок функции выглядит следующим образом:

void *memset(void *ptr, int value, size_t num);

memset записывает "num" байтов со значением "value" по адресу "ptr". Несмотря на то, что параметр "value" имеет тип int, в действительности используется лишь его младший байт. Функция активно применяется для обнуления больших массивов данных, однако компилятор и сам частенько любит вставить её вызов туда, где это нужно и не очень. Так, любопытный случай обсуждался 15 апреля 2018 года на форуме osdev.org. Пользователь под ником ScropTheOSAdventurer создал тему, в которой рассказал о процессе разработки собственной учебной операционной системы. На свою беду он разрешил компилятору оптимизировать исходный код проекта, в результате чего последний перестал работать. В процессе отладки программист обнаружил ошибку в следующем фрагменте кода:

void *memset(void *ptr, int value, size_t num)
{
    unsigned char *ptr_byte = (unsigned char *) ptr;
    for(size_t i = 0; i < num; ptr_byte[i] = (unsigned char) value, i++);
    return ptr;
}

Для своей операционной системы разработчик решил использовать собственную реализацию функции memset. Но он не учёл, что в процессе трансляции компилятор gcc обнаружит в этом коде весьма заманчивую возможность для оптимизации. Фактически функция была в итоге преобразована к следующему виду:

void *memset(void *ptr, int value, size_t num)
{
    return memset(ptr, value, num);
}

Вполне вероятно, что среди разработчиков компилятора gcc были непревзойденные мастера софистики. В любом случае способности компилятора к оптимизациям явно превосходят все доступные человеческому разуму пределы.

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

int check_password(const char *pwd)
{
    char real_pwd[32];
    get_password(real_pwd);
    return !strcmp(pwd, real_pwd);
}

Есть лишь одна проблема - после вызова check_password в стеке останется строка с настоящим паролем пользователя. Если в вашей программе есть хотя бы одна уязвимость, позволяющая читать данные из памяти, то существует реальная вероятность украсть пароль из стека. Примером подобной уязвимости стал печально известный баг "Heartbleed". Чтобы снизить возможные риски, проще всего очистить содержащий пароль фрагмент стека:

int check_password(const char *pwd)
{
    int result;
    char real_pwd[32];
    get_password(real_pwd);
    result = !strcmp(pwd, real_pwd);
    memset(real_pwd, 0, sizeof(real_pwd));
    return result;
}

Казалось бы, решение найдено, однако не все так просто. Искушённый в вопросах оптимизации компилятор может решить, что вызов memset здесь лишний, и спокойно удалит его из тела функции. Действительно, для работы самой программы это действие абсолютно бесполезно. Что ещё хуже, компилятор может сгенерировать код, в котором пароль окажется в одном из регистров процессора. В таком случае получить его, используя уязвимость в программе, может оказаться еще проще. И у вас даже не получится заставить компилятор очистить содержимое регистра. Более подробно о данной проблеме можно прочитать по ссылке.

Одной из наиболее коварных разновидностей неопределённого поведения является strict aliasing. Термин может быть переведён как "строгое наложение", однако традиционного названия на русском языке у него не существует. По этой причине мы будем использовать оригинальный английский термин. Текст стандарта дает такое описание для strict aliasing (раздел 3.3, "EXPRESSIONS"):

Значение объекта должно быть доступно только с помощью lvalue-выражения одного из следующих типов:

- объявленный тип объекта,

- квалифицированная версия объявленного типа объекта,

- знаковый или беззнаковый тип, соответствующий объявленному типу объекта,

- знаковый или беззнаковый тип, соответствующий квалифицированной версии объявленного типа объекта,

- тип массива, структуры или объединения, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член внутренней структуры, массива или объединения),

- символьный тип.

Проще всего strict aliasing проиллюстрировать на конкретном примере:

int x = 42;
float *p = &x;
*p = 13;

Чтобы вызвать неопределенное поведение, достаточно обратиться к какой-либо переменной по типу, несовместимому с объявленным. Это ограничение можно обойти, применив символьный тип (char), на который не распространяются правила strict aliasing:

int x = 42;
char *p = &x;
*p = 13;

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

union u { int a; short b };
union u x;
x.a = 42;
x.b = 13;

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

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

int get_pixel(const char *buf, int width, int x, int y)
{
    buf += get_header_size(buf);
    return ((const int *) buf)[y * width + x];
}

При вызове функции ей передается адрес области данных с содержимым файла, включая его заголовок, ширину изображения, а также координаты пикселя, цвет которого следует вернуть. Вместо типа int мы могли бы выбрать любой другой с известным нам размером. Но все это неважно, потому что функция get_pixel абсолютно неверна с точки зрения стандарта, так как нарушает правила strict aliasing. Чтобы использовать каламбур типизации, придется переписать весь код, связанный с используемым буфером, в том числе и тот, который ответственен за чтение файла.

Существует огромное количество примеров программ, не удовлетворяющих правилам strict aliasing. В их число входит знаменитая функция вычисления быстрого обратного квадратного корня из игры Quake 3:

float FastInvSqrt(float x)
{
    float xhalf = 0.5f * x;
    int i = *(int *) &x;
    i = 0x5f3759df - (i >> 1); /* What the fuck? */ 
    x = *(float *) &i;
    x = x * (1.5f - (xhalf * x * x));
    return x;
}

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

Остается один вопрос - зачем вообще нужен этот strict aliasing? Все дело в том, что он позволяет создателям компиляторов применять крайне агрессивные оптимизации исходного кода. Правила strict aliasing распространяются на обращения к любой памяти, в том числе и динамической. Так комитет стандартизации отметил, что следующий фрагмент кода (источник):

void f(int *x, double *y)
{
    *x = 0;
    *y = 3.14;
    *x = *x + 2;
}

может быть преобразован таким образом:

void f(int *x, double *y)
{
    *x = 0;
    *y = 3.14;
    *x = 2;
}

Согласно правилам strict aliasing указатель y не может содержать адрес того же участка памяти, что и указатель x. Именно этот факт и позволяет заменить выражение "*x = *x + 2" на "*x = 2". Активное использование компиляторами подобных оптимизаций сломало огромное количество старого кода. Так, в письме от 12 июля 1998 года один из разработчиков компилятора gcc Jeff Law, отвечая на вопросы по strict aliasing и связанными с ним ошибками, пишет (источник):

> Существует очень много кода, который нарушает правила strict aliasing. Одним из таких примеров является "переносимая" универсальная функция контрольной суммы IP, которая содержится в исходных кодах BSD для работы с сетями.

ИМХО, такого кода становится все меньше и меньше - современные компиляторы уже какое-то время используют strict aliasing в анализе, в результате чего люди были вынуждены исправлять свой код. Безусловно, это не относится к Linux и некоторым другим свободным проектам, так как они используют только gcc.

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

Укажите им на стандарт языка Си :-) :-)

Правила strict aliasing для компилятора gcc можно разрешить, используя флаг "-fstrict-aliasing", и запретить флагом "-fno-strict-aliasing". Последний рекомендуется применять, если вы не уверены, нарушаете ли вы текст стандарта - скорее всего, нарушаете. Говоря об упомянутом в письме ядре Linux, его автор Линус Торвальдс также дал свою оценку strict aliasing в частности и работе комитета в целом. Так, критикуя желание одного из разработчиков операционной системы лишний раз перестраховаться от нарушения стандарта, Линус написал такое письмо (источник):

Честно говоря, все это кажется мне сомнительным.

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

Дело в том, что использование объединений для реализации каламбура типизации - это обычный и СТАНДАРТНЫЙ для этого способ. В действительности он является документированным для gcc, и используется в том случае, если вы, будучи не слишком умным (оригинал: "f*cking moron"), применили "-fstrict aliasing", и теперь вам необходимо избавиться от всего того ущерба, который навязывает этот мусорный стандарт.

Энди, что послужило причиной для всего этого идиотизма? И не надо говорить мне, что текст стандарта "недостаточно ясный". Текст стандарта, совершенно ясно, является дерьмовой чушью (см. выше о правилах strict aliasing), и в таких случаях его нужно игнорировать. Для этого необходимо использовать средства компилятора, чтобы избежать ущерба. Аналогично нужно поступать и в ситуациях, где нет полной ясности.

Это то, почему мы используем "-fwrapv", "-fno-strict-aliasing" и другие флаги.

Я уже говорил об этом раньше и повторю еще раз: когда текст стандарта противоречит реальности - он является обычным куском туалетной бумаги. Он не имеет абсолютно никакой важности. В действительности, вместо него я лучше возьму рулон настоящей туалетной бумаги - так хотя бы я не испачкаю свою задницу чернилами (оригинал: "won't have splinters and ink up my arse").

Видимо, Линус Торвальдс плохо изучил язык Си - настоящему программисту на Си такое в голову бы не пришло.

Впрочем, одним лишь strict aliasing стандарт не полон. Чтобы вызвать неопределённое поведение, необязательно даже разыменовывать указатель:

void f(int *p, int *q)
{
    free(p);
    if(p == q) /* Undefined behaviour! */
        do_something();
}

Использование значения указателя после того, как память по нему была освобождена, запрещено текстом стандарта (раздел 4.10.3, "Memory managment functions"):

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

Программисту важно понимать, что указатели в Си не являются низкоуровневыми. Стандарт постарался полностью искоренить какую-либо связь языка с реальным миром. Даже сравнение указателей, ссылающихся на разные объекты, объявлено неопределённым поведением (раздел 3.3.8, "Relational operators"):

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

Вот небольшой фрагмент кода, демонстрирующий некорректное с точки зрения стандарта сравнение:

int *p = malloc(64 * sizeof(int));
int *q = malloc(64 * sizeof(int));
if(p < q) /* Undefined behaviour! */
    do_something();

Однако наиболее интересным примером здесь будет служить исходный код следующей программы:

#include <stdio.h>

int main()
{
    int x;
    int y;
    int *p = &x + 1;
    int *q = &y;
    printf("%p %p %d\n", (void *) p, (void *) q, p == q);
    return 0;
}

Если транслировать приведенный выше текст компилятором gcc, передав ему флаг "-O", то полученный исполняемый файл при запуске выдаст примерно следующую строку:

0x1badc0de 0x1badc0de 0

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

Большая часть примеров, связанных с работой указателей, была взята с сайта kristerw.blogspot.com. На нём вы сможете найти больше информации о текстах стандарта языка Си, а также загадочных оптимизациях компиляторов.

Может показаться, что в случае выключенных оптимизаций все вышеописанные проблемы обойдут вас стороной. Просто не передавайте компилятору флаг "-O", и вы получите тот результат, на который рассчитываете. Но на самом деле это не так. В январе 2007-ого года на сайте gcc.gnu.org пользователь под ником felix-gcc выложил исходный код следующей программы:

#include <assert.h>

int foo(int a) {
  assert(a + 100 > a);
  printf("%d %d\n",a + 100, a);
  return a;
}

int main() {
  foo(100);
  foo(0x7fffffff);
}

Функция foo проверяет на переполнение сумму поданного знакового числа и константы "100". Как известно, на подавляющем большинстве компьютерных архитектур отрицательные числа задаются в виде дополнительного кода. В случае переполнения такое число меняет знак на противоположный, благодаря чему проверка "a + 100 > a" возвращает ложь. В теле функции main felix-gcc дважды вызывает foo. Сначала он подает на вход число, которое не приведёт к переполнению. Затем, исходя из того, что размер типа данных int равен четырем байтам, felix-gcc вызывает foo с наибольшим положительным числом данного типа. Логично предположить, что в таком случае сравнение вернёт ложь, и assert прервёт работу программы. Однако вот какой вывод получил felix-gcc после запуска исполняемого файла:

200 100
-2147483549 2147483647

Фактически gcc решил удалить проверку на переполнение, и это при том, что никаких флагов компилятору передано не было. И что еще интереснее, ранние версии gcc при тех же условиях не убирали проверку, в результате чего получаемая программа вела себя иначе. На резонную просьбу felix-gcc исправить неожиданный баг компилятора ответил пользователь под ником Andrew Pinski. Будучи разработчиком gcc, Andrew Pinski заметил, что данное поведение не является ошибочным. Более того, он сам оказался автором изменения в коде компилятора, которое и создало столь странный результат. Далее приводится фрагмент диалога felix-gcc и Andrew Pinski. Комментарии излишни:

Andrew Pinski
Переполнение знакового числа - это неопределённое поведение в тексте стандарта языка Си, используйте беззнаковый тип или флаг "-fwrapv".

felix-gcc
Вы должно быть шутите?
Различные проблемы безопасности вызваны переполнениями чисел, и вы просто так говорите мне, что в gcc 4.1 я больше не могу тестировать их для знаковых типов? Вы явно чего-то не понимаете. ДОЛЖЕН быть способ обойти эту проблему. Существующее программное обеспечение использует знаковые числа, и я не могу просто поменять тип на беззнаковый - мне все равно нужно проверить переполнение! Не похоже, что я мог бы использовать какой-нибудь обходной путь для этого. Что вы хотите от меня - чтобы я привел тип к беззнаковому, сдвинул вправо на один, затем сложил или что вообще?!
ПОЖАЛУЙСТА, ОТМЕНИТЕ ЭТО ИЗМЕНЕНИЕ. Оно создаст СЕРЬЁЗНЫЕ ПРОБЛЕМЫ БЕЗОПАСНОСТИ во ВСЕВОЗМОЖНЫХ ПРОГРАММАХ. Меня не волнует, что ваши стандартизаторы говорят о том, что gcc исправен. ВСЕ ЭТО ПРИВЕДЁТ К ТОМУ, ЧТО ЛЮДЕЙ ВЗЛОМАЮТ. Я обнаружил эту проблему, так как одна из проверок безопасности, которая предотвращает взлом, провалилась.
ЭТО НЕ ШУТКА. ИСПРАВЬТЕ ЭТО! СЕЙЧАС ЖЕ!

Andrew Pinski
Я не шучу, стандарт языка Си прямо говорит, что переполнение знакового числа - это неопределённое поведение.

felix-gcc
Так, послушайте, Эндрю, вы что, действительно думаете, что эта проблема исчезнет, если вы продолжите закрывать баги достаточно быстро? Проверка, которую я написал, покрывала все возможные ситуации. Не требуется даже уточнения того, что за тип используется - указатель, беззнаковое или знаковое число. Ну, указатели вы тоже сломали, но ваши изменения были исправлены. Парень, который сделал это тогда, должен появиться здесь, нам нужен кто-то с трезвой головой и видением ситуации как у него.
Давайте посмотрим правде в глаза - вы облажались по полной (оригинал: "fucked up this royally"), и теперь вы пытаетесь прикрыть все ошибки как можно скорее, чтобы никто не заметил, сколько ущерба вы нанесли. Вы, сэр, непрофессиональны и позорите команду разработчиков gcc. Эта ошибка останется открытой до тех пор, пока вы не вернёте все обратно или не сделаете упомянутый вами флаг по умолчанию. Пока вы будете ломать программы, чьи авторы по глупости включили оптимизации, мне всё равно. Но я не позволю вам делать моё окружение менее безопасным только потому, что ваш непрофессионализм не позволяет вам разобраться с оптимизациями после того, как было показано, что они наносят больше вреда, чем пользы. Сколько еще доказательств вам необходимо предоставить? Боже мой, да autoconf считает, что ваши "оптимизации" нужно отключать повсеместно. Вы вообще замечаете взрывы вокруг самих себя?

Andrew Pinski

http://c-faq.com/misc/sd26.html

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

felix-gcc
МОЙ КОД НЕ СЛОМАН.
Попытки обесценить проблему или оскорбить меня ничего не решат.

Andrew Pinski
Вы написали ошибку, поэтому я и решил, что ваш код сломан.

felix-gcc
Итак, скажите мне, какая часть моей аргументации вам непонятна? Я мог бы использовать слова попроще, чтобы вы смогли меня понять на этот раз.
Ребята, ваша задача - это не просто реализовать стандарт Си. Вы также обязаны не нарушать работу программ, которые зависят от вас. А от вас зависит МНОГО программ. Когда вы нарушили точность вычислений с плавающей точкой, то вы сделали это доступным с помощью флага (-ffast-math). Когда вы добавили strict aliasing, вы так же сделали эту функцию доступной через флаг (-fstrict-aliasing). Если я правильно помню, вы тогда тоже цитировали текст стандарта, пока люди с более адекватным пониманием мира вас не остановили. И я собираюсь оставить эту ошибку открытой до тех пор, пока не повторится та же история.

Andrew Pinski
Я думаю, нам не следовало делать это необязательным, но меня не было в тот момент, когда было принято это решение. Также помните, что у нас был релиз, когда strict aliasing был включен, но затем нам пришлось его отключить по умолчанию. За это время люди исправляли свои программы, пока оптимизация была активна. И мы уже сделали оптимизацию знакового переполнения опциональной с помощью "-fwrapv". Я не понимаю, к чему вы приводите свои аргументы.

felix-gcc
Вы не можете просто так потенциально сломать кучу свободного ПО лишь потому, что изменили свое мнение по поводу того, какую свободу вам дает стандарт.
Повзрослейте или уйдите, позволив более ответственным людям заниматься вашими делами.

Andrew Pinski
Подождите, но эта оптимизация была еще с 1994-ого года, и если какой-либо код, начиная с этого момента, использовал знаковое переполнение, то авторы этих программ сами напросились.

felix-gcc
Знаете ли вы, что ракета Ариан-5 взорвалась (и могла убить людей!) из-за ошибки переполнения? Что если люди погибнут из-за того, что вы решили, что стандарт позволяет вам выкидывать проверки безопасности, написанные людьми?

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

felix-gcc
Еще раз: НЕ ИМЕЕТ ЗНАЧЕНИЯ ТО, ЧТО ГОВОРИТ СТАНДАРТ. Вы сломали программы, и люди пострадали от этого. Теперь верните все обратно. Меньшее, что вы можете, это сделать "-fwrapv" по умолчанию. Вам все еще придётся заставить его работать правильно (я слышал, что он неверно работает в определенных ситуациях), но это уже другая история.

Andrew Pinski
Он будет по умолчанию в тех языках, где определено именно такое поведение. Я дал вам способ написания проверок переполнения, и если вам не нравится то, что говорит стандарт языка Си, то это не моя вина.
Запомните: компилятор gcc также является оптимизирующим компилятором, и если вам нужны оптимизации, то вы должны следовать правилам того языка, на котором вы пишете, вместо создания неверных программ, что и происходит с Си и Си++ в целом.

felix-gcc
В ранних версиях компилятора такое поведение происходило только в случае включённых оптимизаций. Если немножко присмотреться, то окажется, что все ваши аргументы ничего не стоят.
Потому как gcc 4.1 выкидывает этот код уже без включённых оптимизаций. Вот и все ваши аргументы.
Пожалуйста, сделайте "-fwrapv" по умолчанию, и я заткнусь.

Andrew Pinski
Попробуйте проверить время исполнения программы с "-fwrapv" и без него. Вы увидите, что без него код работает быстрее.

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

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

А в самом конце обсуждения Andew Pinski заявил следующее:

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

В заключение хочется привести еще одну цитату Линуса Торвальдса:

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

И в этом, похоже, заключена главная проблема языка Си. Но подобное не могло произойти на пустом месте - в конечном итоге мы сами позволили этому случиться. Язык Си уже очень давно перестал выполнять возложенные на него функции и превратился в уродливую пародию на самого себя. Но мы этого не заметили, потому что смирились с тем, что наши программы не работают. Мы, как программисты, настолько привыкли к ошибкам, что они стали неотъемлемой частью нашей жизни. Зачастую на отладку и тестирование программ уходит больше времени, чем на проектирование и написание самого кода. И ведь это немудрено - людям свойственно ошибаться. Большую часть багов и уязвимостей программисты вносят случайно, совершенно не задумываясь, и мы ничего не можем с этим сделать. Однако неизбежность ошибок не оправдывает их существование. Задача программиста в том, чтобы писать код, который работает. Даже если это неочевидно, трудно и невозможно, мы не имеем права делать ошибки. Потому что иначе все бессмысленно, и мы перестаем понимать, что можно делать, а что нельзя, что красиво, а что уродливо. В погоне за эффективностью разработчики компиляторов забыли о том, для чего на самом деле нужен язык Си. Он инструмент программиста, а плохим инструментом нельзя написать хорошую программу. Эта история - показательный пример того, что не всякая деятельность плодотворна, и не каждое изменение ведет к лучшему результату. Стараниями комитета стандартизации и разработчиков компиляторов мы в конечном итоге потеряли язык Си. Как инструмент разработки он стал абсолютно бесполезен и даже вреден, и мы обязаны признать это. В противном случае наши программы никогда не будут работать. Пренебрежительное отношение к ошибкам должно уйти в прошлое, а вместе с ним должен умереть и язык Си.

P.S. Если вы все ещё верите, что язык Си можно спасти, ознакомьтесь по ссылке со следующей выдержкой за авторством одного из двух редакторов текста стандарта языка Си:

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

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

Соавтор статьи: @aversey

Изначальная публикация: cmustdie.com

Комментарии (583)


  1. ciubotaru
    29.11.2021 14:39
    +12

    Правильно ли я понимаю, что:

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

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

    ?


    1. amarao
      29.11.2021 14:39
      +24

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


      1. aversey
        29.11.2021 14:49
        +5

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


        1. amarao
          29.11.2021 15:06
          +11

          Си должен умереть, да, и пока что Rust - самое близкое из того, что у нас есть для замены Си. Однако, я с большим интересом смотрю на срачики вокруг Rust-драйверов в ядре. Претензии, которые предъявляют к Rust'у весьма любопытны. Ядро не должно падать по "одной ошибке", даже если это логическая ошибка, а в Rust весьма трудно избежать паник при нарушении инвариантов (а драконы из земли uninitialized memory весьма хотят их нарушить...)


          1. apro
            29.11.2021 15:26
            +7

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

            Это на мой взгляд самый слабый аргумент против. В изначальном ядре, без патчей для добавления поддержки Rust, есть функция "panic", и куча ее вызовов с помощью макросов BUG/BUG_ON и так далее. Поэтому почему Rust в отличие от С должен быть особенным и не содержать неявных вызовов panic! совершенно непонятно.


          1. aversey
            29.11.2021 15:33
            +23

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

            Что касается замены Си, Rust мне кажется больше заменой плюсам, но если потянет -- интересно будет посмотреть на мир, где железо заржавело. =)


            1. amarao
              29.11.2021 15:43
              +7

              Не совсем так. Насколько я понимаю, речь идёт о том, что в rust некоторые виды действий не имеют Option, и если условие не выполняется, то BUG_ON/panic! единственная опция. А в Си с этим умудряются жить.

              Если ядро обнаруживает, что pagetable коррапченная, то это BUG_ON. Но если драйвер GP устройства обнаруживает, что ему не дали 4кб памяти, то это не BUG_ON никаким образом (на что именно ругались я не смотрел, я читал сам текст ругани).

              В принципе, на Rust'е можно писать полностью низкоуровневый код (включая naked functions для обработки прерываний). Хотя я погуглил, плюсы тоже умеют naked. Так что разница только в здравом смысле Rust'а (наличии safe-подмножества).

              ... В то же самое время, как в условиях ядра справятся с ownership - это интересно.


              1. apro
                29.11.2021 16:51
                +2

                Насколько я понимаю, речь идёт о том, что в rust некоторые виды действий не имеют Option, и если условие не выполняется

                А какие именно "действий"? В голову приходит только "fallible allocation", но обертку над менеджером памяти ядра все равно нужно будет делать отдельно, стандартные функции выделения памяти использовать вряд ли удастся, например из-за наличия kmalloc и vmalloc. А раз придется делать заново, то в чем сложность добавить Result/Option в новые функции выделения памяти не очень понятно.


                1. domix32
                  30.11.2021 11:52

                  Собственно первый ржавый патч c драйвером как-то так и делает. Там макрос типа try_allocate! который вернет либо результат аллокации либо ошибку.


              1. vkni
                29.11.2021 19:37
                +3

                Не совсем так.

                Именно так.

                Я сильно сомневаюсь, что простому системному язычку нужно 5 (пять) видов макросов. И никакой язык не может избавить от всех ошибок. Это просто невозможно.

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

                А если язык сложен как Rust, написать второй компилятор очень тяжело.


                1. amarao
                  29.11.2021 20:28
                  +1

                  В целом, требование второго компилятора вполне обосновано, и я его много раз слышал в контексте "коммититься в язык".

                  В целом, вот, люди пытаются. https://github.com/thepowersgang/mrustc


                  1. vkni
                    29.11.2021 21:36
                    +7

                    Нет, "я требую" не второго компилятора, а двадцать второго.

                    То есть, для замены текущего С, как lingua franca современных ЯВУ, должна быть спецификация, компилятор по которой усердный студент 6 курса реализует ну за пол года. Без оптимизаций, разумеется, без хитрых проверок, но вполне рабочую. Стандартной библиотеки тоже несколько реализаций.

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


                    1. cepera_ang
                      29.11.2021 21:59
                      +9

                      А си-то сможет усердный студент реализовать за полгода? Прямо по стандарту или просто "какое-нибудь минимальное" подмножество языка?


                      1. vkni
                        29.11.2021 22:07
                        +3

                        89 сможет, кмк.


                      1. cepera_ang
                        29.11.2021 22:11
                        +3

                        И как много кода сейчас пишется или, прости господи, компилируется в режиме С89?


                      1. BioHazzardt
                        29.11.2021 22:17
                        +4

                        я под тайзен в апреле на C89 писал


                      1. vkni
                        29.11.2021 22:19
                        +5

                        Пишется, кмк, достаточно много того, что легко отпортируется на С89. Тот же ocamlrun - интерпретатор Ocaml до недавнего времени был на C89. И, не сказать, что это достоинство новых ревизий С - относительная сложность спецификации.

                        Кроме того, C вроде бы множество компиляторов уже есть. А для предполагаемой перспективной замены С как системного языка - нет.



                        Вот простейшая штука - SPARC 64. Где для него компилятор Rust'а? Хоть какой-нибудь, пусть без borrow-checker'а, без оптимизаций, но способный запустить экосистему? А как там у Эльбрусовцев - оно работает или нет?


                      1. amarao
                        29.11.2021 22:58
                        +9

                        sparc64-unknown-linux-gnu

                        входит в список плохо поддерживаемых платформ. Поддержка Эльбруса растом - это вопрос к эльбрусовцам, потому что продукт маргинальный и за них поддержку llvm (а это основное требование) никто не будет писать для всякой экзотики.


                      1. vkni
                        29.11.2021 23:09

                        Ну вот плохо - это как?

                        Вопрос к Эльбрусовцам в то и упирается, что коллектив разработчиков нового ЦП должен малыми силами суметь разработать компилятор системного языка. Этот самый lingua franca.

                        А ведь Эльбрус - это мощнейшая контора с гос. финансированием, долгими традициями и т.д. А что могут авторы какого-то исследовательского процессора?

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


                      1. amarao
                        29.11.2021 23:24
                        +1

                        Портировал gcc и llvm, дальше оно само подтянется.

                        mrust как раз хорошая цель для начала портирования. Хотя без llvm ничего не выйдет, извините.

                        Вообще, задача неоптимизирующего минимального компилятора Rust с нативной кодогенерацией (без LLVM) - это отличная интересная область. Например, все generic'и (в вопросах кодогенерации) превращаются в динамическую диспетчеризацию... Хотя, может, лучше llvm спортировать?


                      1. vkni
                        30.11.2021 00:44
                        -1

                        Портировал gcc и llvm, дальше оно само подтянется.

                        llvm и gcc в отличие от спецификации c89 эволюционируют. И у маленького коллектива может просто не хватить ресурсов на поддержку.


                      1. cepera_ang
                        29.11.2021 23:34
                        +34

                        Эльбрус — это мощнейшая контора с гос. финансированием, долгими традициями и т.д.

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


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


                      1. vkni
                        30.11.2021 00:42

                        Ну, то есть, все группы с исследовательскими ЦПУ идут в известное место?


                      1. cepera_ang
                        30.11.2021 07:13
                        +4

                        Что за группы такие? Давайте предметно разговаривать, пока получается гипотетический разговор в пользу бедных — какие-то мифические группы, которые не могут осилить порт gcc/llvm, но могут осилить свой процессор и компилятор. И чего дальше они с этим компилятором делать будут? Любоваться на свой процессор? Давайте ссылку на пример таких исследователей, о которых вы говорите.


                      1. gecube
                        30.11.2021 20:24
                        +4

                        А ведь Эльбрус - это мощнейшая контора с гос. финансированием, долгими традициями и т.д. А что могут авторы какого-то исследовательского процессора?

                        че-то ору....


                      1. cepera_ang
                        29.11.2021 23:00
                        +6

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


                        Вот простейшая штука — SPARC 64. Где для него компилятор Rust'а?

                        Я может не туда смотрю, но rustc для SPARC 64 и есть компилятор раста для SPARC 64. А насчёт эльбрусов — они что-то там хвалились насчёт поддержки ещё в начале года, но это ж чисто гипотетическая платформа, зачем ей какая-то поддержка Раста :)


                      1. vkni
                        29.11.2021 23:10
                        +1

                        А чем вообще ценность такого упражнения, кроме занятости студента?

                        Возможность разработки какой-то новой экосистемы, с новыми процессорами, новыми шинами и т.д.

                        Иначе мы завязнем в болоте x86. О, блин.


                      1. cepera_ang
                        29.11.2021 23:37
                        +5

                        Вот что говорит о поддерживаемых платформах тулчейн LLVM у меня на компе, на который опирается компилятор раста:


                        Registered Targets:
                        aarch64 — AArch64 (little endian)
                        aarch64_32 — AArch64 (little endian ILP32)
                        aarch64_be — AArch64 (big endian)
                        arm — ARM
                        arm64 — ARM64 (little endian)
                        arm64_32 — ARM64 (little endian ILP32)
                        armeb — ARM (big endian)
                        avr — Atmel AVR Microcontroller
                        bpf — BPF (host endian)
                        bpfeb — BPF (big endian)
                        bpfel — BPF (little endian)
                        hexagon — Hexagon
                        mips — MIPS (32-bit big endian)
                        mips64 — MIPS (64-bit big endian)
                        mips64el — MIPS (64-bit little endian)
                        mipsel — MIPS (32-bit little endian)
                        msp430 — MSP430 [experimental]
                        nvptx — NVIDIA PTX 32-bit
                        nvptx64 — NVIDIA PTX 64-bit
                        ppc32 — PowerPC 32
                        ppc32le — PowerPC 32 LE
                        ppc64 — PowerPC 64
                        ppc64le — PowerPC 64 LE
                        riscv32 — 32-bit RISC-V
                        riscv64 — 64-bit RISC-V
                        sparc — Sparc
                        sparcel — Sparc LE
                        sparcv9 — Sparc V9
                        systemz — SystemZ
                        thumb — Thumb
                        thumbeb — Thumb (big endian)
                        wasm32 — WebAssembly 32-bit
                        wasm64 — WebAssembly 64-bit
                        x86 — 32-bit X86: Pentium-Pro and above
                        x86-64 — 64-bit X86: EM64T and AMD64

                        Мало? А теперь давайте компилятор С, кроме gcc и clang'a, который может без переписывания половины кода такой же список показать.


                      1. inferrna
                        30.11.2021 11:00

                        Справедливости ради, переписывание кода на расте, всё-же, может потребоваться.


                      1. cepera_ang
                        30.11.2021 11:20

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


                      1. netch80
                        30.11.2021 11:47

                        > Программа размером сто мегабайт на восьмибитном контроллере не особо запустится

                        Вообще-то банально — для этого нужно банкирование памяти :) (которое и так очень часто есть на таких контроллерах, но не в таких масштабах).

                        Но в здравом уме и твёрдой памяти, конечно, никто так делать не будет (превышение в 64 раза, как на старших PDP-11, вроде был максимум достигнутого), поэтому это замечание чисто вскользь.


                      1. cepera_ang
                        30.11.2021 12:42
                        +1

                        В теории есть, а на практике такие компьютеры существуют? А что на них будет делать эта гипотетическая гигантская программа — запускаться первые N лет после включения устройства? :)


                      1. BigBeaver
                        30.11.2021 12:43

                        Емнип, была где-то статья про линукс на ардуино.


                      1. inferrna
                        30.11.2021 12:19

                        Не, я про использование, к примеру, 64-битных примитивов на 32-битных платформах и тому подобное.


                      1. GarryC
                        30.11.2021 17:58

                        А почему, собственно, без gcc ? А если я потребую у Вас Rust без llvm ?


                      1. cepera_ang
                        30.11.2021 18:01
                        +2

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


                        А если окажется, что замечательная поддержка различных платформ у Си благодаря gcc и llvm, то это уже не так уж и сильно отличается от раста :)


                      1. ApeCoder
                        30.11.2021 07:02
                        +2

                        А почему надо писать для новой экосистемы весь компилятор а не только бекэнд?


                      1. domix32
                        30.11.2021 12:06
                        +2

                        Весь раст прячет за абстракциями llvm, так что порт кодогенерации из llvm IR в спарковский байткод вполне решит проблему. Расту останется только указать необходимый бэкэнд. Есть неофициальная поддержка и 16-битных досов и под амигу помнится что-то делали. Это уже не говоря про зоопарк embed микроконтроллеров, часть из которых потом в спутниках вокруг земли вертится.


                      1. vkni
                        30.11.2021 18:28
                        -1

                        И вы начинаете зависеть от llvm. Нет, это путь в никуда для небольшой группы.

                        Кроме того, системный язык на то и системный, чтобы быть близок к текущему железу => у него должен быть достаточно простой компилятор.


                      1. cepera_ang
                        30.11.2021 18:33
                        +4

                        И вы начинаете зависеть от llvm

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


                        системный, чтобы быть близок к текущему железу => у него должен быть достаточно простой компилятор.

                        Непонятно как одно следует из другого.


                      1. domix32
                        30.11.2021 22:23
                        +3

                        Насколько должна быть небольшой группа, чтобы ей понадобился отдельный компилятор под собственное не мейнстримное железо и при этом страдало бы от зависимости от llvm? Звучит как мифический персонаж из африки у которого из девайсов только телефон с парой мегабайт памяти и ллвм туда просто не влезет. Опять же встаёт вопрос каков шанс, что такая мифическая железка нужна в массовых количествах и будет иметь хоть какие-то требования к безопасности и отказоустойчивости? Мне кажется более вероятным что кто-то будет писать сразу на ассемблере для такого, чем пытаться изобразить ещё один компилятор Си.


                      1. BigBeaver
                        30.11.2021 10:44

                        Емнип, я перешел на С99 только из-за каких-то нюансов с хвостовой рекурсией (надо было сэкономить десяток то ли байт, то ли тактов), а так в embedded C89 вполне норм себя чувствует.


                      1. RomanArzumanyan
                        30.11.2021 12:05

                        И как много кода сейчас пишется или, прости господи, компилируется в режиме С89?

                        Много чего для видео. FFMpeg, например. До сих пор ругается на смешанные объявления переменных и код.


                    1. amarao
                      29.11.2021 22:55
                      +4

                      Вопрос интересный. Я, с одной стороны, понимаю мотивацию требовать простой для реализации язык. Но, с другой стороны, вы же этого не требуете от ОС? Времена, когда вы могли нашкрябать аналог MS-DOS за пол-года 6 курса давно прошли, и чем дальше, тем сложнее, однако, никто не выкидывает новые компьютеры на том основании, что студент под новую умную сетевуху драйвера уже не осилит написать. Аналогично с компиляторами. Альтернативные (условно простые) реализации компилятора - да, требование охватности компилятора неподготовленным мозгом - нет, это произвольное требование.

                      Альтернативную библиотеку к расту уже написали/пишут (насколько оно применимо не смотрел): https://github.com/eloraiby/alt-std


                      1. BigBeaver
                        30.11.2021 10:48

                        Потомучто далеко не все процессоры предназначены для запуска ОС.


                      1. amarao
                        30.11.2021 13:22

                        Разумеется. Хотя нижняя планка "можно запустить ОС" (в центах на штуку) с каждым годом всё ниже и ниже.


                      1. BigBeaver
                        30.11.2021 13:27

                        Можно не означает нужно. От применения зависит.


                      1. cepera_ang
                        30.11.2021 14:53


                    1. Amomum
                      30.11.2021 01:40
                      +23

                      Простите, но зачем?..

                      Миллионы несовместимых компиляторов С (и стандартных библиотек, не забывайте еще и про них) - это огромная проблема, причем это проблемы программистов, вы вспомните, сколько ифдефов нужно написать, просто чтобы обеспечить совместимость с gcc/clang/msvc!

                      Когда появился С - не было интернета, распространить единый компилятор было очень трудно, а книжку - относительно легко, вот все и писали эти компиляторы..

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

                      Вот, в посте есть замечательная ссылка на блок члена комитета по стандартизации С - он прямо об этом же пишет:

                      (краткий пересказ-перевод)

                      С постоянно продает себя как "простой язык". Но это ложь! На С даже два числа сложить нельзя, не подглядывая в стандарт (ой, а не закралось ли UB?), но менеджменту говорили не это! Им говорили "любой хороший разработчик может сляпать компилятор за пару дней", эту иллюзию поддерживают все нормальные компиляторы (gcc, clang и т.д.), ведь все их оптимизации и проверки - необязательные!

                      На самом деле стандарт гарантирует так мало, что это просто смешно - но даже это реализовать правильно ужасающе сложно!

                      Именно поэтому все попытки производителей чипов сделать свой компилятор приводили лишь к забагованным кускам <вырезано цензурой>.

                      Now now, I say that like the compiler authors are being vindictive. The reality of the matter is that C has repeatedly and perpetually sold itself as being a “simple” language. And I mean…

                      Is it?

                      Can’t add 2 numbers together in C without consulting the holy standard about whether or not some UB’s been tripped, let alone with a well-defined way to figure out how to stop it. We recently just had to reinforce a Defect Report where we stated that “yes, even if a compiler can figure out that your array bounds are, in fact, a constant number, we have to treat the creation and usage of the array like a VLA because the Standard’s constant expression parser isn’t smart enough!”.

                      C is not a simple language.

                      That’s not what management gets told, of course. What management hears is the spicy dream, the “any good developer can bang out a C compiler drunk out of their mind on a weekend”. The way the Standard supports that dream is by making all of the good stuff people get used to in GCC, Clang, EDG or whatever else “optional” or “recommended”. What’s actually guaranteed to you by the C Standard is so pathetically miniscule it’s sad (and even that tiny little bit is still complex!). It’s why every person who ran off to “write their own C compiler” did a miserable job, why every embedded chipset thought they could roll their own C compilers and ended up with a bug-ridden mess. It’s why there are so many #ifdef __SUN_OS and // TODO: workaround, please removes that end up becoming permanent fixtures for 17 years.

                      Насчет vendor-specific компиляторов - это ведь реально так, даже ARM сливает свой armcc и вместо него переходит на форк clang'a, потому что сделать свой нормальный компилятор для своей же архитектуры оказалось слишком сложно!

                      Простите, что-то у меня пригорело слишком сильно..

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


                      1. DustCn
                        01.12.2021 01:10
                        -1

                        Ну сложно не сложно, Интел бросил свой ICC в пользу Clang по одной простой причине - Clang ковыряют тысячи бесплатных разработчиков и миллионы тестировщиков. И если стандарты на С++ будут продолжать печь с той же скоростью, то чтобы обеспечить полный охват Интелу надо будет продавать свое процессорное подразделение и нанимать больше программистов компилятора. А не потому что запил архитектуры сложен...

                        Утрированно конечно, но в целом описывает проблему новых стандартов в целом в отрасли.


                      1. Amomum
                        01.12.2021 01:14
                        +1

                        По-моему это тоже самое другими словами :) Сложно => дорого и/или долго => экономически нецелесообразно.


                    1. Alex_ME
                      30.11.2021 01:59
                      +1

                      А зачем писать целиком компилятор, включая парсер, все эти IR, MIR, borrow checker и даже пролого-подобный солвер (https://github.com/rust-lang/chalk), если можно портировать LLVM?


                    1. domix32
                      30.11.2021 11:58
                      +2

                      А почему именно это свойство профитное? Я бы предпочел чтобы студент 6 курса смог за то же время написать формализатор на каком-нибудь Coq и мог гарантировать, что конкретная имплементация компилятора гарантирует такой-то набор гарантий пускай без все тех же оптимизаций. Такое по крайней мере будет относительно безопасно использовать.


                      1. vkni
                        30.11.2021 18:26

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


                      1. cepera_ang
                        30.11.2021 18:36
                        +1

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


                      1. domix32
                        30.11.2021 22:46

                        Писать компиляторы ради того чтобы писать компиляторы не звучит как здравая цель. В прошлом веке конечно был бум на такое, потому что архитектуры тогдашних устройств отличались, но каждый компилятор имел кучу своих проблем, как с UB так и с производительностью, не говоря уже о специфичных багах, которые могли окуклить устройства из-за нюансов компилятора. Да и за безопасность в то время особо не думали. Ну подумаешь можно всю память компьютера прочитать вместе с паролями. Хакеров еще толком нет, худшее что могло быть — залетный фрик (phreak) который взял себе бесплатный доступ к телефонной линии. Сейчас же мир страдает от того что половина устройств в сети торчит со вшитыми дефолтными паролями, получить доступ к которому плёвое дело. Есть огромный спрос на секурность и надёжность и самописный компилятор едва ли поможет с решением этих проблем в 99.99 процентах случаев.
                        Вторая причина почему простота системного языка имела значение в прошлые года — нишевость программистов. Проще научить чему-нибудь простому и понятному, чтобы бизнес мог делать ХХП и грести баблишко. Риски от ошибок были относительно маленькими, да и вариантов получше не существовало в принципе. Так что простота софта и разработки это скорее исторический этап, нежели какое-то неотъемлемое свойство. Простота написания Hello world не гарантирует примерно ничего, кроме собственно вывода Hello world. Так что велик шанс что стоит пересмотреть приоритеты.


                  1. Mingun
                    29.11.2021 21:43
                    +1

                    В целом, требование второго компилятора вполне обосновано,

                    А зачем?


                    1. vkni
                      29.11.2021 22:08

                      Ну это же системный язык, на котором можно писать OS для любой экзотической архитектуры, для которой нет хорошо отлаженной системы llvm. Например, для Эльбруса. ;-)


                    1. amarao
                      29.11.2021 23:00
                      +1

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

                      Требование более чем разумное.


                      1. cepera_ang
                        29.11.2021 23:03
                        +4

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


                      1. amarao
                        29.11.2021 23:19

                        Ну, как минимум, есть MS и LCC, из того, что я могу назвать.


                      1. 0xd34df00d
                        29.11.2021 23:07
                        +10

                        Если бы он был единственным компилятором, то, вероятно, не умер бы.


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


                      1. amarao
                        29.11.2021 23:20
                        +2

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


                      1. 0xd34df00d
                        29.11.2021 23:31
                        +3

                        А на плюсы 5-10 лет назад смотрели?


                      1. amarao
                        30.11.2021 13:19
                        +2

                        Смотрели и даже используют. Компиляторов С++ достаточно много.


                      1. 0xd34df00d
                        30.11.2021 19:34
                        +1

                        Можно примеры двух альтернативных ненаколенных компиляторов C++, скажем, в 2010-м под линукс?


                      1. vkni
                        30.11.2021 19:39
                        +1

                        Я не уверен насчёт OpenWatcom, но ICC точно был.


                      1. 0xd34df00d
                        30.11.2021 20:04
                        +2

                        ICC был, но я его вообще ни разу нигде не видел в бою.


                        Де-факто gcc альтернатив не было.


                      1. Sdima1357
                        30.11.2021 20:18
                        +1

                        но я его вообще ни разу нигде не видел в бою

                        Я видел. В 2000 он крешил ядро операционки на темплейтах - сам компилятор,kernel panic при запуске компиляции от юзера 8). Где то в 2004-2007 давал очень неплохую SIMD оптимизацию, раскладывал циклы в 4xfloat32, выигрывая у gcc до 40 процентов. На немного более нетривиальном коде с ветвлениями слегка медленнее чем gcc. Потом AMD немного поучаствовала в GCC и он стал побыстрее. А так было время что часть кода у нас собиралась под icc , а часть под gcc.


                      1. vkni
                        30.11.2021 20:51
                        +1

                        В известной ветке на IXBT про системы команд процессоров народ наоборот, ничего никогда с помощью gcc в PROD не компилирует. :-) Мир большой.


                      1. Sdima1357
                        30.11.2021 19:39
                        +1

                        Можно примеры двух

                        1 . g++ -GNU

                        2 . icc -Intel


                      1. cepera_ang
                        30.11.2021 19:59

                        А за пределами x86 жизни нет?


                      1. Sdima1357
                        30.11.2021 20:10
                        +1

                        А за пределами x86 жизни нет?

                        ??? icc -это от вендора . Естественно что интел пишет для интеля. Для ARM - Arm Compiler for Linux in Arm Allinea Studio : https://developer.arm.com/tools-and-software/server-and-hpc/compile/arm-compiler-for-linux


                      1. amarao
                        30.11.2021 20:37
                        +1

                        Не альтернативы:

                        gcc

                        clang

                        Альтернативы:

                        intel C++ compiler

                        AOCC

                        arm compiler

                        cl (MS)


                      1. Gordon01
                        30.11.2021 22:32

                        А помните был такой IAR embedded compiler?)

                        Не под линукс и только под arm, но все же)))


                      1. 0xd34df00d
                        30.11.2021 22:50
                        +1

                        Да, и на совместимость с новыми фичами сишки или плюсов я плевки от народа слышал постоянно.


                      1. Amomum
                        01.12.2021 01:26

                        А что, IAR тоже свой компилятор дропнули?


                      1. netch80
                        30.11.2021 23:20

                        Intel C++ (ICC) уже упомянули. Ещё был open64, причём в двух инкарнациях — одна сильно подточенная AMD и одна полунезависимая.
                        Я тогда работал на HPC тематику (хоть и вскользь) и мы его там пробовали. На каких-то математических пакетах он давал код быстрее и GCC, и (тогда ещё только начинавшегося) Clang, и ICC.


                      1. cepera_ang
                        29.11.2021 23:38
                        +4

                        Можно поспорить, что Rust переживёт некоторые из таких компаний :)


                      1. amarao
                        30.11.2021 13:16
                        +1

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


                      1. cepera_ang
                        30.11.2021 13:22
                        +3

                        И поэтому код условной электростанции будет написан один раз на %vendorname% версии С, который собирается единственным компилятором от производителя использованных микроконтроллеров, который однажды засертифицировал конкретную версию, а потом скончался через 25 лет.


                        :)


                        Но требование разумное, я его вполне понимаю.


                      1. romanetz_omsk
                        30.11.2021 20:23
                        +1

                        А вы точно уверены, что некая АСУТП система сопровождается 50 лет? Насколько я знаю, к тому же самому технологическому оборудованию подключают новые шкафы, написанные на другом языке, стандартном на момент внедрения. Срок службы автоматики - 20 лет.


                      1. vkni
                        30.11.2021 04:46
                        +2

                        Это просто часть кластера болезненных точек, не причина, разумеется, но единственность ghc сильно связана с:

                        Производительность ghc - это единственный компилятор, поэтому невозможно сделать быстрый компилятор Хаскеля. (быстрый компилятор почти Хаскеля возможен - это cocl, компилятор Клина - 0.2 сек полная сборка страниц 10 текста, использующих стандартные библиотеки на Core 2 Duo T9500).

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

                        Простота разворачивания - это единственный компилятор, поэтому об "./configure; make world opt -j 40" забудьте.

                        Удаление невзлетевших фич, вроде backpack - это единственный компилятор.

                        Эксперименты с разными подходами, например в духе MLton/Stalin - забудьте. Хотя в случае ленивых языков это могло бы быть крайне интересно.

                        Где встраиваемый Хаскель, который может занять место Lua? GHC - единственный компилятор.

                        -----------------------
                        Разумеется, часть вещей реализована в том же Клине или Ocaml, которые тоже единственные компиляторы. Но к ним я могу тоже набросать массу претензий.


                      1. 0xd34df00d
                        30.11.2021 09:27
                        +2

                        Производительность ghc — это единственный компилятор, поэтому невозможно сделать быстрый компилятор Хаскеля. (быстрый компилятор почти Хаскеля возможен — это cocl, компилятор Клина — 0.2 сек полная сборка страниц 10 текста, использующих стандартные библиотеки на Core 2 Duo T9500).

                        Тормозят всякие продвинутые фичи в системе типов. Что-нибудь тупое вроде haskell98 компилируется весьма быстро — модуль с 250 функциями вида foo_k n = n + k у меня собрался за 0.8 секунд, пустой модуль — за 0.6 секунд (там линковка отчего-то долго занимает).


                        Портируемость на всякое экзотическое — это единственный компилятор, поэтому увы и ах.

                        А, по-вашему, легче портировать имеющийся компилятор, для этого предназначенный, или написать с нуля под новую платформу?


                        Простота разворачивания — это единственный компилятор, поэтому об "./configure; make world opt -j 40" забудьте.

                        Ну это увы, да, согласен.


                        Удаление невзлетевших фич, вроде backpack — это единственный компилятор.

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


                        Эксперименты с разными подходами, например в духе MLton/Stalin — забудьте. Хотя в случае ленивых языков это могло бы быть крайне интересно.

                        Есть огромная куча экспериментальных форков, а лёгкость написания плагинов означает, например, что вы можете под тот же блокчейн (cardano) писать код на более-менее обычном хаскеле, а потом отдельный плагин сгенерирует код для блокчейна. Отдельный компилятор не нужен.


                      1. vkni
                        30.11.2021 18:15
                        +1

                        А, по-вашему, легче портировать имеющийся компилятор, для этого предназначенный, или написать с нуля под новую платформу?

                        Я же говорю, что это не причина, а просто часть узла проблем. С моей точки зрения, для промышленного (не экспериментального) компилятора спецификация лучше единственной реализации.

                        В частности потому, что изменения промышленного языка очень часто болезненны.

                        а то, что надо — оказывается, что проще сделать новый язык с нуля и назвать его, например, идрисом).

                        Кмк, Хаскель давно надо было "заморозить". И работать с новыми языками.

                        Отдельный компилятор не нужен.

                        Это означает, кстати, болезненную зависимость от upstream'а.


                      1. 0xd34df00d
                        30.11.2021 20:04
                        +1

                        Кмк, Хаскель давно надо было "заморозить". И работать с новыми языками.

                        Смотря для чего. Для ресёрча — хз. Для промышленного применения — очевидно, нет, там, вопреки стереотипам, уже достаточно кода написано.


                        Это означает, кстати, болезненную зависимость от upstream'а.

                        Так в чём болезненность?


                      1. vkni
                        30.11.2021 20:54

                        Так в чём болезненность?

                        И это вопрос сейчас, после того, как SPJ ушёл? :-)


                      1. 0xd34df00d
                        30.11.2021 22:51
                        +2

                        Я за политикой слежу мало, а в чём вопрос?


                      1. vkni
                        30.11.2021 18:23
                        +1

                        Тормозят всякие продвинутые фичи в системе типов.

                        Ну нет. Компиляция сгенерированного кода для одной и той же xsd схемы на Ocaml PPX и на Template Haskell отрабатывают за секунду и 30 минут соответственно. Причём что там, что там отображения с одних и тех же алгебраических типов. Просто огромного кол-ва.

                        Ну и компиляция строк 1000 за 0.6 секунд - это тоже перебор. Я прекрасно знаю, что это именно проблема культуры группы вокруг Ghc - те же Clean/Ocaml собирают такое мгновенно.

                        Просто Ocaml'щики маниакально оптимизируют компилятор и экосистему для скорости (к той же dune масса претензий, но не скорость). А люди вокруг Ghc забивают - stack на собранном проекте секунду что-то внутри себя думает. Хотя make и dune в той же ситуации отрабатывают мгновенно.


                      1. 0xd34df00d
                        30.11.2021 20:12
                        +1

                        Компиляция сгенерированного кода для одной и той же xsd схемы на Ocaml PPX и на Template Haskell отрабатывают за секунду и 30 минут соответственно. Причём что там, что там отображения с одних и тех же алгебраических типов. Просто огромного кол-ва.

                        Так в TH небось обмазывание всякими дженериками потом и прочим в deriving, в отличие от окамля. Вот это уже будет тормозить, да, но дженерики — это уже продвинутая фича.


                        Я прекрасно знаю, что это именно проблема культуры группы вокруг Ghc — те же Clean/Ocaml собирают такое мгновенно.

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


                        И я бы сказал, что фишки в типах действительно важнее. В конце концов, есть репл, есть LSP, скорость компиляции в процессе разработки не особо важна, на самом деле.


                        А люди вокруг Ghc забивают — stack на собранном проекте секунду что-то внутри себя думает. Хотя make и dune в той же ситуации отрабатывают мгновенно.

                        stack делает далеко не то же, что make.


                      1. vkni
                        30.11.2021 20:49

                        Вот это уже будет тормозить, да, но дженерики — это уже продвинутая фича.

                        Клин с дженериками не тормозит.

                        В конце концов, есть репл, есть LSP, скорость компиляции в процессе разработки не особо важна, на самом деле.

                        До REPL тоже надо добраться. В вышеупомянутом случае я ждал эти 10 минут/пол часа.

                        stack делает далеко не то же, что make.

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

                        Это проблема приоритетов.

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


                      1. AnthonyMikh
                        30.11.2021 20:52

                        Клин с дженериками не тормозит.

                        Я что-то сомневаюсь, что вы под дженериками понимаете одно и то же. Дэдфуд под ними понимает вот это.


                      1. vkni
                        30.11.2021 20:55

                        Зря. Теперь сравните вот с этим - https://clean.cs.ru.nl/download/html_report/CleanRep.2.2_9.htm#_Toc311798067


                      1. AnthonyMikh
                        30.11.2021 21:11

                        Понял, был неправ, беру свои слова обратно.


                      1. 0xd34df00d
                        30.11.2021 23:07
                        +1

                        такое

                        Version 2.7.3, Git revision 7927a3aec32e2b2e5e4fb5be76d0d50eddcc197f x86_64 hpack-0.34.4
                        2021-11-30 14:04:03.051650: [debug] Checking for project config at: /home/d34df00d/Programming/necogda/stack.yaml
                        2021-11-30 14:04:03.051781: [debug] Loading project config file stack.yaml
                        2021-11-30 14:04:03.055321: [debug] (SQL) SELECT COUNT(*) FROM "last_performed" WHERE ("action"=?) AND ("timestamp">=?); [PersistInt64 1,PersistUTCTime 2021-11-29 20:04:03.055300791 UTC]
                        2021-11-30 14:04:03.055690: [debug] Using package location completions from a lock file
                        2021-11-30 14:04:03.057504: [debug] Loaded snapshot from Pantry database.
                        2021-11-30 14:04:03.236940: [debug] RawSnapshotLayer < тут 250 килобайт текста с текущими пакетами в снепшоте >
                        2021-11-30 14:04:03.290311: [debug] Running hpack on /home/d34df00d/Programming/necogda/package.yaml
                        2021-11-30 14:04:03.294792: [debug] hpack output unchanged in /home/d34df00d/Programming/necogda/necogda.cabal
                        2021-11-30 14:04:03.296550: [debug] Asking for a supported GHC version
                        2021-11-30 14:04:03.297149: [debug] Installed tools: 
                         - ghc-tinfo6-8.10.3
                         - ghc-tinfo6-8.10.7
                         - ghc-tinfo6-8.10.5
                         - ghc-tinfo6-8.10.4
                         - ghc-tinfo6-8.8.4
                         - ghc-tinfo6-8.10.6
                         - ghc-tinfo6-8.10.2
                         - ghc-tinfo6-8.8.3
                         - ghc-tinfo6-8.6.5
                        2021-11-30 14:04:03.297392: [debug] Run process: /sbin/ldconfig -p
                        2021-11-30 14:04:03.298620: [debug] Process finished in 1ms: /sbin/ldconfig -p
                        2021-11-30 14:04:03.299264: [debug] Did not find shared library libtinfo.so.5
                        2021-11-30 14:04:03.299299: [debug] Found shared library libtinfo.so.6 in 'ldconfig -p' output
                        2021-11-30 14:04:03.299339: [debug] Found shared library libncursesw.so.6 in 'ldconfig -p' output
                        2021-11-30 14:04:03.299378: [debug] Found shared library libgmp.so.10 in 'ldconfig -p' output
                        2021-11-30 14:04:03.299435: [debug] Did not find shared library libgmp.so.3
                        2021-11-30 14:04:03.299459: [debug] Potential GHC builds: tinfo6, ncurses6
                        2021-11-30 14:04:03.299496: [debug] Found already installed GHC builds: tinfo6
                        2021-11-30 14:04:03.299602: [debug] (SQL) SELECT "id","actual_version","arch","ghc_path","ghc_size","ghc_modified","ghc_pkg_path","runghc_path","haddock_path","cabal_version","global_db","global_db_cache_size","global_db_cache_modified","info","global_dump" FROM "compiler_cache" WHERE "ghc_path"=?; [PersistText "/home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-8.10.7"]
                        2021-11-30 14:04:03.362765: [debug] Loaded compiler information from cache
                        2021-11-30 14:04:03.362887: [debug] Asking for a supported GHC version
                        2021-11-30 14:04:03.363125: [debug] Resolving package entries
                        2021-11-30 14:04:03.363172: [debug] Parsing the targets
                        2021-11-30 14:04:03.365413: [debug] Checking flags
                        2021-11-30 14:04:03.365455: [debug] SourceMap constructed
                        2021-11-30 14:04:03.370510: [debug] Starting to execute command inside EnvConfig
                        2021-11-30 14:04:03.373375: [debug] Finding out which packages are already installed
                        2021-11-30 14:04:03.373466: [debug] Run process: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --global --no-user-package-db dump --expand-pkgroot
                        2021-11-30 14:04:03.406968: [debug] Process finished in 33ms: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --global --no-user-package-db dump --expand-pkgroot
                        2021-11-30 14:04:03.416051: [debug] Run process: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --user --no-user-package-db --package-db /home/d34df00d/.stack/snapshots/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/pkgdb dump --expand-pkgroot
                        2021-11-30 14:04:03.494927: [debug] Process finished in 79ms: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --user --no-user-package-db --package-db /home/d34df00d/.stack/snapshots/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/pkgdb dump --expand-pkgroot
                        2021-11-30 14:04:03.503268: [debug] Run process: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --user --no-user-package-db --package-db /home/d34df00d/Programming/necogda/.stack-work/install/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/pkgdb dump --expand-pkgroot
                        2021-11-30 14:04:03.522352: [debug] Process finished in 19ms: /home/d34df00d/.stack/programs/x86_64-linux/ghc-tinfo6-8.10.7/bin/ghc-pkg-8.10.7 --user --no-user-package-db --package-db /home/d34df00d/Programming/necogda/.stack-work/install/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/pkgdb dump --expand-pkgroot
                        2021-11-30 14:04:03.522887: [debug] Constructing the build plan
                        2021-11-30 14:04:03.525400: [debug] (SQL) SELECT "id","directory","type","pkg_src","active","path_env_var","haddock" FROM "config_cache" WHERE "directory"=? AND "type"=?; [PersistText "/home/d34df00d/Programming/necogda/.stack-work/install/x86_64-linux-tinfo6/13fa040522c82079f8fcb58f0649cdd5905ee24cc49d2e88e8f1a2a03c8fcec9/8.10.7/",PersistText "lib:necogda-0.1.0.0-DOEEJaDfef8BQb8WOzmppB"]
                        2021-11-30 14:04:03.525768: [debug] (SQL) SELECT "id", "config_cache_id", "index", "option" FROM "config_cache_dir_option" WHERE ("config_cache_id"=?) ORDER BY "index"; [PersistInt64 7]
                        2021-11-30 14:04:03.525934: [debug] (SQL) SELECT "id", "config_cache_id", "index", "option" FROM "config_cache_no_dir_option" WHERE ("config_cache_id"=?) ORDER BY "index"; [PersistInt64 7]
                        2021-11-30 14:04:03.526115: [debug] (SQL) SELECT "id", "config_cache_id", "ghc_pkg_id" FROM "config_cache_dep" WHERE ("config_cache_id"=?); [PersistInt64 7]
                        2021-11-30 14:04:03.526483: [debug] (SQL) SELECT "id", "config_cache_id", "component" FROM "config_cache_component" WHERE ("config_cache_id"=?); [PersistInt64 7]
                        2021-11-30 14:04:03.526877: [debug] Start: getPackageFiles /home/d34df00d/Programming/necogda/necogda.cabal
                        2021-11-30 14:04:03.539345: [debug] Finished in 12ms: getPackageFiles /home/d34df00d/Programming/necogda/necogda.cabal
                        2021-11-30 14:04:03.543139: [debug] Checking if we are going to build multiple executables with the same name
                        2021-11-30 14:04:03.543184: [debug] Executing the build plan


                      1. vkni
                        30.11.2021 23:24

                        То есть, стэку ещё надо выяснить, что там надо собирать.

                        Очевидно, это можно было как-то оптимизировать по скорости. Было бы желание. Но у сообщества вокруг Ghc этого желания нет.

                        Без nfs сейчас dune отрабатывает за

                        А так — кто мешает потратить силы на то, чтобы запилить это же в первый?

                        Это шутка?

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

                        Фичи, разумеется, не аналогичные, далеко не аналогичные. Но конкретно на сборках быстрее.


                      1. 0xd34df00d
                        30.11.2021 23:56
                        +1

                        Очевидно, это можно было как-то оптимизировать по скорости. Было бы желание. Но у сообщества вокруг Ghc этого желания нет.

                        Вообще-то есть, и stack несколько лет назад работал сильно медленнее, чем сейчас. А по сравнению с олдовым cabal, который на любой чих задумывается на пару минут со словами «Resolving dependencies…», он вообще молниеносный.


                        Без nfs сейчас dune отрабатывает за

                        Так и не понял, за сколько :]


                        Это шутка?

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


                        Кстати, сколько компиляторов у clean?


                      1. vkni
                        01.12.2021 04:17

                        Так и не понял, за сколько :]

                        Core2 Duo T9500, Linux 64-bit; 0.14 сек первый запуск на ppx (3 файла), 0.04 сек - последующие. Проект собран. А я - лох, конечно.

                        Кстати, сколько компиляторов у clean?

                        Я же уже писал, что один. Есть ещё где-то компилятор eClean,но он закрыт и точно без генериков и динамики. Это один из недостатков Клина, а их вообще немало.

                        Но Клин показывает, что можно сделать быстрый компилятор Хаскеле-подобного языка, выдающий более-менее сносный код. Точно также, как Caml Light показывал, что можно сделать хороший компилятор для языка CAML.

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


                      1. 0xd34df00d
                        01.12.2021 10:08
                        +3

                        Я же уже писал, что один.

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


                        Олсо, компиляторов хаскеля когда-то было дофига (и сейчас они всё ещё технически есть, но именно что технически). Просто оказалось, что одного ghc достаточно.


                      1. vkni
                        01.12.2021 17:14

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

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


                      1. 0xd34df00d
                        03.12.2021 01:47
                        +1

                        К слову об отсутствующем желании, ковырял тут поддержку ghc 9.2 для hls — наткнулся на такое, тут чувак очень сильно ускорил скорость HLS. Что, в принципе, разумно, фокусироваться на language server'е куда разумнее, чем на самой сборке, на удобство и удовольствие от разработки оно влияет куда больше.


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


                      1. vkni
                        30.11.2021 18:31
                        +1

                        Так Linux был далеко не монопольной OS.


                      1. Mingun
                        29.11.2021 23:47
                        +1

                        A Watcom C был open-source? Если вдруг (хотя это очень-очень-очень маловероятно) развитие раста повернет куда-то не туда, не проще ли форкнуть уже существующий отлаженный компилятор, чем писать собственный? Хотя я все равно не понимаю, зачем вам иметь зоопарк компиляторов, чтобы потом мучится и писать проекты, которые должны будут компилироваться под всеми из них и в итоге не использовать ни один из них на полную катушку, зато иметь лес затычек багов то одного, то другого.


                      1. nick758
                        30.11.2021 01:20
                        -1

                        Watcom C не был opensource, но в 2003 году он стал OpenWatcom, некоторое время даже развивался. Последний стабильный релиз от 2010 года.


                      1. 0xd34df00d
                        30.11.2021 03:56
                        +8

                        А Вы сами ещё не устали от перманентных обновлений языков и необходимости обновлять кодовую базу по временным веяниям?

                        Зависит от языка, конечно, но в среднем не устал. Напротив, нередко с нетерпением жду релиза новых версий компиляторов с новыми фичами.


                        Плевать на ухудшающийся функционал и производительность

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


                        Новые оптимизации компилятора завозят, оптимизации рантайма завозят, и так далее.


                      1. PsyHaSTe
                        01.12.2021 03:29

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


                      1. PsyHaSTe
                        01.12.2021 04:07
                        +2

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


                      1. netch80
                        01.12.2021 10:29
                        -1

                        > Если не видите необходимости не пользуйте новые плюшки

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

                        > Все просто же

                        Нет.


                      1. PsyHaSTe
                        01.12.2021 13:46
                        +4

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

                        Желание собирать под новое целевое устройство и есть "видеть необходимость в обновлении"


                      1. netch80
                        01.12.2021 14:18

                        > Желание собирать под новое целевое устройство и есть «видеть необходимость в обновлении»

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

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


                      1. amarao
                        30.11.2021 13:18

                        Нет. PL/I с нами на веки.


                      1. vkni
                        30.11.2021 18:33

                        "Кресты" вот в области стандартов и обновлений очень хороши. У них море недостатков, но вот конкретно стандарты, обратная совместимость на уровне.


                      1. netch80
                        30.11.2021 23:31

                        > но вот конкретно стандарты, обратная совместимость на уровне.

                        Хорошая шутка, спасибо.

                        Можно начинать смотреть отсюда (всю ветку, там много весёлых гитик).

                        Или на cppreference.com отменённые совсем в каких-то ревизиях возможности (например, auto_ptr в C++17 уже нет — ну да, GCC рисует для совместимости).


                      1. vkni
                        30.11.2021 18:31

                        Я сформулировал таки требование, которое у меня есть - простая для реализации стабильная (на ближайшие 5-10 лет) спецификация языка и ядра системной библиотеки. Если это системный язык, то, кмк, это вполне обосновано.

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


                      1. amarao
                        30.11.2021 20:33

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

                        Вот llvm вопрос открытый. Уж очень удобно иметь единый бэкэнд.


                      1. vkni
                        30.11.2021 20:44
                        -2

                        Вот llvm вопрос открытый. Уж очень удобно иметь единый бэкэнд.

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

                        У языка есть поддержка стабильных редакций. Условно говоря, в 2021 вы как писали на Rust-2018

                        Можете называть это эстетическим чувством, но мне не нравится, когда экосистема превращается в кубик-рубик-монолит. С единым компилятором, большим кол-вом пакетов в cargo, и llvm оно неизбежно превратится в нечто такое необозримое и неизменное.


                      1. cepera_ang
                        30.11.2021 20:49

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

                        И какая цена отказа от того, во что вложены миллионы человеко-часов? :)


                    1. domix32
                      01.12.2021 19:31

                      Это создает некоторую конкуренцию, что лучше чем монополия.


                1. AnthonyMikh
                  30.11.2021 19:26

                  Я сильно сомневаюсь, что простому системному язычку нужно 5 (пять) видов макросов

                  Как вы их столько насчитали? В Rust есть только две разновидности макросов: декларативные и процедурные.


                  1. Cerberuser
                    01.12.2021 06:03

                    Декларативные двух видов (macro_rules! и нестабильный macro), процедурные - трёх (derive, attribute, function-like).


                    1. AnthonyMikh
                      01.12.2021 15:43

                      С точки зрения писателя макроса — два с половиной вида, с точки зрения пользователя — один (function-like, а как именно реализован — неважно).


            1. cepera_ang
              29.11.2021 15:56
              +35

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


              1. indestructable
                29.11.2021 18:34
                +8

                Вот именно. Си намного сложнее Раста, не в плане синтаксиса, а в понимании того, как на самом деле будет работать программа


            1. inferrna
              30.11.2021 10:57
              +1

              >сложность языка имеет свойство перекладываться на сложность программ

              1. В расте очень много сахара, который частично компенсирует сложность bc.

              2. Зависит от масштаба. Мелкая утилита/библиотека на расте будет чуть сложнее. Зато на крупном проекте будет меньше граблей и головной боли с отладкой сегфолтов.


            1. PsyHaSTe
              01.12.2021 03:19
              +6

              Ключевое слово — кажется. Раст просто в язык добавляет явно некоторые концепции (вроде времени жизни объекта) которые и в Си и в Сипипи существуют, но на уровне "в голове у разработчика". Шаг же от динамики (в голове у разработчика) к статике (записано в типах) всегда же должен приветстоваться, тем более в таких важных вещах как системное ПО.


          1. artemisia_borealis
            29.11.2021 15:51
            +3

            Ну, справедливости ради, самое близкое это всё же режим Better C в Dlang. Там часть самострелов в ногу устранено. Но главный плюс это то, что перетекать на этот компилятор можно начать плавно с уже имеющейся кодобазой.


            1. amarao
              29.11.2021 15:53
              +6

              Имеющаяся codebase, которая основна на цементировании UB, это, скорее, минус. Собственно, поговорка, что Rust - это C++ из которого убрали С.


          1. bm13kk
            29.11.2021 17:17

            Чисто ради интереса. А как оно происходит в Си?
            Программа ождидает что здесь по адресу лежит положительное число. Пришло отрицательное. Код не упал и продолжил работать с отрицательным числом.

            Как это работает?


            1. amarao
              29.11.2021 17:27
              +1

              Если это энкапсулированное число (т.е. код его не использует), то просто оставляет как есть. Если использует, но в виде "просто данных" (например, делает +1), то из -1 становится 0. Если это смещение к указателю, то UB. Что будет, то будет, а CVE не миновать.

              (Это в отсутствие явной проверки в коде)


            1. netch80
              29.11.2021 17:28
              +2

              > Программа ождидает что здесь по адресу лежит положительное число. Пришло отрицательное. Код не упал и продолжил работать с отрицательным числом.

              А общего рецепта не будет, всё зависит от конкретного кода.

              Ну например вы пишете

              for (int i = 0; i < n_elems; ++i) {
                что-то делаем
              }
              


              будет пустой цикл.

              А если вы сделаете

              for (int i = 0; i != n_elems; ++i) {
                что-то делаем
              }
              


              то оно пойдёт выполняться по всем положительным, заврапится и на отрицательные числа (4 миллиарда проходов не хотите?)

              А если компилятор опознает, что там отрицательное — то может вообще выкинуть цикл как неисполнимый. Вопрос, дадут ли ему это опознать — зависит от погоды на Юпитере и фазы лун Альдебарана (точнее, от всего входного кода и версии компилятора).

              Но специфика оптимизации тут только в этом выкидывании, код и так был некорректный. В идеале надо было бы какой-нибудь assert на предусловие n_elems >= 0… хотя <= и так помогает неплохо. Поэтому при любой возможности тут лучше делать такие проверки.

              (GCC, что характерно, обычно превращает такой цикл в сначала проверку что n_elems >= 1, а затем уже в теле цикла меняет <= на !=. Вот удобнее ему так. Но имеет право. А вот если бы совсем выкидывал проверку — был бы неправ.)


              1. bm13kk
                29.11.2021 17:44
                +2

                cc @amarao@vanxant

                Я не, видимо, не правильно задал вопрос что его смысл потерялся.

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

                Вопрос именно в контексте аргументов Торвальдса. Как пишут драйверы, что они устойчевы в повреждению памяти? Весь мой опыт (10 лет, 3 основных и десяток вспомогательных языков) - говорит что оно все равно упадет. Что даже, если каким-то чудом, драйвер продолжит работу - вероятность поломки/потери чего-то ценного огромна.

                Можно, например, написать какой-то глобальный цикл (или демона де-факто) на 10 строк. Он никогда принципиально не упадет. И любую логику делать внутри цикла и трая. Тогда ошибки перехватываются и *формально* драйвер продолжает работать. Но этот паттерн применим к любому языку.

                Я уже молчу про контраргумент в дргом сообщении - из С тоже можно вызвать аналог паники в любом месте.


                1. netch80
                  29.11.2021 19:17

                  > Вопрос именно в контексте аргументов Торвальдса. Как пишут драйверы, что они устойчевы в повреждению памяти?

                  Они не устойчивы. Но в Linux есть защита, что если драйвер повредил только собственные структуры, то ядро хотя бы успеет сказать «мяу» и скинуть лог проблемы. Срабатывает не всегда, но достаточно часто, чтобы собирать трейсы, логи и прочие данные, по которым можно понять, что случилось.
                  А в остальном — повезёт или не повезёт.
                  Проблема с Rust, насколько я вижу, в интеграции его рантайма. Пока что есть с этим проблемы — в отличие от C, которому в варианте ядра Linux достаточно было настроить стек на кусок RAM, argc+argv и какой-то простейший malloc, дальше он уже работал.
                  Но, думаю, их решат без особых затяжек.

                  Если я всё ещё не на то отвечаю — переформулируйте.


                  1. bm13kk
                    29.11.2021 20:34

                    Нет, теперь понятно, спасибо.

                    Но опять же получается что это не проблема Раста - а лок линукса на си


                    1. b-s-a
                      29.11.2021 23:44
                      +1

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


                      1. bm13kk
                        30.11.2021 05:27

                        "Лок" не всмысле управления памятью и процессами. А в смысле кода захардкодженного на определенное поведение определенной реализации. Которое, как я понимаю, еще и не является частью стандарта.


                      1. b-s-a
                        30.11.2021 14:26
                        +1

                        memset - это стандартная функция. Она есть в стандарте. Более того, сам Линукс к подобным функция не привязан. Более того, если не ошибаюсь, там вообще стандартная библиотека в ядре отсутствует.

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


                      1. bm13kk
                        30.11.2021 17:15

                        Мне (и возможно не только) очень интересно за что минус. Потому что аргументация в комментарии мне понятна (хотя я все еще не согласен с Торвальдсом).

                        Но я не сишник, чтобы самому знать что в нем не верно.


                      1. b-s-a
                        30.11.2021 17:17

                        Тут кто-то очень щедро минусы расставляет. У меня почти все комментарии с минусами тут. :-)


                    1. netch80
                      30.11.2021 09:00

                      Ну скорее не на C, а на конкретные ABI его реализации. Но C тут тем хорош, что его рантайм имеет очень ограниченные требования — фактически, только обеспечение стеком, правила передачи аргументов/результата и явно вызванные функции библиотеки. И под конкретный ABI на конкретной платформе можно уже и подстроить заточку. Уже с C++ так легко не получилось (хотя в причинах я не уверен — можно было бы уточнить в режиме без исключений и RTTI).


                1. codelock
                  29.11.2021 22:10

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


                  1. kmeaw
                    30.11.2021 00:52

                    Но ведь такие обстоятельства существуют и для C, например UB. Почему вызов паники из C-кода ядра - хорошо, наличие UB в C разработчики ядра терпеть готовы, а панику рантайма Rust - нет?


                    1. lorc
                      30.11.2021 02:44
                      +1

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

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

                      Но на самом деле проблема с паниками - это не самое страшное. Я тут недавно читал цикл постов про различия в моделях памяти между растом и ядром. Там все куда сложнее. Хотя и менее очевидно.


                    1. codelock
                      30.11.2021 13:13

                      Эти штуки из разных плоскостей. Undefined Behavior не означает аварийное завершение работы, тем более в пространстве ядра, а является формальным описанием области значений, при нарушении инварианта, которые будут отличаться от ожидаемых. Например, если Вы нарушаете закон, то Вас могут найти правоохранители и наказать, а могут не найти и всё будет ок.

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


            1. vanxant
              29.11.2021 17:29
              +4

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

              Ну а так, мусор на входе - мусор на выходе.

              Потом, правда, ракеты падают и десятичная точка в сумме при банковском переводе сдвигается.


              1. netch80
                29.11.2021 19:19
                +2

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


              1. b-s-a
                29.11.2021 23:54
                +1

                Процессору пофиг. А вот компилятору нет. Сегодня ты компилмруешь для процессора с int=int32, а завтра int=int64, и твоя кривая проверка работать перестанет. Поэтому разработчики стандарта языка и компиляторов делают такие вещи. По мне, если ты делаешь какую-то дичь, которая не соответствует стандарту языка, ты обязан обложить ее проверками на архитектуру системы и написать кучу комментариев, почему ты это сделал, и как оно вообще работает. По-хорошему, компилятор должен ругаться, когда выкидывает часть кода. Ну, ненормально это в 99% случаев.


                1. vanxant
                  30.11.2021 03:18

                  твоя кривая проверка работать перестанет

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

                  При том что спор, тащем-та, о трёхстрочных функциях. Допустим, у нас есть

                  int a, int b;

                  Для проверки на то, не было ли переполнения при выполнении условной операции ⊕, достаточно написать

                  (long)(a ⊕ b) == ((long) a) ⊕ ((long) b)


                  1. netch80
                    30.11.2021 09:14
                    +2

                    > Для проверки на то, не было ли переполнения при выполнении условной операции ⊕, достаточно написать

                    > (long)(a ⊕ b) == ((long) a) ⊕ ((long) b)

                    А теперь повторите этот фокус, например, с 64-битным long при отсутствии поддержки 128-битного типа.

                    А потом вспомните цену поддержки 64-битного long long на 32-битной платформе и, соответственно, цену такой проверки, по сравнению с более близкой к возможностям машины (которые обычно делают проще).


                    1. vanxant
                      30.11.2021 10:11
                      -2

                      Заметьте, я сказал написать (на Си), а не скомпилировать под конкретный процессор. В комитете не совсем идиоты сидят, наверное, что столько лет не пускают это в стандарт.


                      1. netch80
                        30.11.2021 10:16
                        +3

                        > Заметьте, я сказал написать (на Си), а не скомпилировать под конкретный процессор.

                        Отлично, и как вы обеспечите этот фокус на long значениях, если вдвое более широкого типа данных просто не дают (по стандарту)?

                        > В комитете не совсем идиоты сидят, наверное, что столько лет не пускают это в стандарт.

                        В комитете сами по себе — безусловно, не идиоты. А люди, защищающие бизнес-интересы конкретного представителя (чаще всего — компиляторов) так, как эти интересы понимают владельцы их фирм. И эти интересы не совпадают в подавляющем большинстве случаев.


                      1. vanxant
                        30.11.2021 10:21
                        -1

                        Отлично, и как вы обеспечите этот фокус на long значениях, если вдвое более широкого типа данных просто не дают (по стандарту)?

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


                      1. netch80
                        30.11.2021 10:27
                        +3

                        > Все реально используемые процессоры умеют в произвольное удлинение целых.

                        1. Это сильно дороже.

                        2. Вы сказали «все реально используемые процессоры», но не сказали, что это обеспечено библиотекой. Это значит, что тот, кто заботится о проблеме, должен сам писать такой код? Ещё и продираясь через то, что некоторые фишки процессора (типа флагов CF, OF) недоступны в C и их функциональность надо эмулировать?

                        Ладно, со сложением или вычитанием — там достаточно просто написать что-то типа

                        if ((b > 0 && a > INT_MAX - b)
                         || (b < 0 && a < INT_MIN -b))
                        {
                         ... 
                        }
                        


                        а с умножением как? Приведите тут (хотя бы для себя), как это будет выглядеть в коде. Можно в отдельную функцию.

                        > Бэкэнд компилятора должен в такое уметь, даже если на его фронте таких типов нет.

                        Вы сами сказали в своём предыдущем комментарии:

                        > Заметьте, я сказал написать (на Си)

                        Какой бэкэнд? Какой фронтэнд? Вы или крестик снимите, или трусы наденьте ©, или мы говорим про язык, или про свойства и возможности конкретных компиляторов.


                      1. vanxant
                        30.11.2021 11:27
                        -2

                        Воу-воу, палехчи. Я, похоже, в отличие от вас, читал исходники gcc.

                        там достаточно просто написать что-то типа

                        Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например a = b = INT_MIN. Вы забываете, что инструкция сравнения это "вычитание без запоминания результата". Если в процессоре нет флагов, как в RISC-V, то ваш код не взлетит (а если флаги есть, то он не нужен).

                        а с умножением как?

                        умножение двух интов сразу даёт long (и гарантированно в этот long влезает что для знаковых, что для беззнаковых). Тут даже делать особо ничего не требуется, просто не упаковывать long обратно в int.


                      1. netch80
                        30.11.2021 11:33
                        -1

                        > Воу-воу, палехчи. Я, похоже, в отличие от вас, читал исходники gcc.

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

                        (Не вспоминаю пока, зачем тут вообще нужны исходники gcc к данной дискуссии. Но это мы обсудим после ваших извинений.)


                      1. vanxant
                        30.11.2021 12:35

                        вы утверждаете, что я не читал их.

                        Слово "похоже" вам ни на что не намекает?

                        Ну раз читали, освежите память. Исходники gcc вот: https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/gcc/internal-fn.c

                        начиная с 686 строки.

                        Конкретно для сложения и вычитания int-ы и вообще signed типы достаточно расширять до соответствующего unsigned (что само по себе CWE-195, но компилятору можно :)


                      1. netch80
                        30.11.2021 13:23

                        Мой пример на проверку до собственно сложения. Вы сказали:

                        > Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например a = b = INT_MIN.

                        OK, смотрим. b == INT_MIN даёт ветку b < 0. INT_MIN — b == 0. a == INT_MIN, значит, < 0. Проверка сработала (вторая ветка), и мы ушли в обработку переполнения. К чему были ваши претензии? Вы можете показать, с какими именно аргументами для сложения такая проверка не сработает?

                        Ну а что с умножением сильно сложнее — я говорил открытым текстом.

                        И вот это вот:

                        > Если в процессоре нет флагов, как в RISC-V, то ваш код не взлетит (а если флаги есть, то он не нужен).

                        Почему это он не взлетит, если он полностью корректен?

                        > Вы забываете, что инструкция сравнения это «вычитание без запоминания результата».

                        Не забываю. А проверки такого рода корректны и выполняются и на машинах без флагов условий. Да, они могут быть дороже, чем явный вызов профильного интринсика, но результат они дают — и они используются именно в таком виде в соответствующих врапперах.
                        Можете сами взять эмулятор того же RISC-V и проверить все ключевые случаи.

                        > Исходники gcc вот:

                        Обработка __builtin_{add,sub}_overflow? Ну хорошо, и при чём тут это к моему описанию, как можно обойтись без такого интринсика для конкретной операции?

                        И какая связь этого с предыдущим обсуждением, где мой основной пункт был, что самый широкий тип (как long long) расширять уже бесполезно — до такой степени, что без такого интринсика в Safeint3 вообще сделали проверку обратным делением (с соотв. ценой)?

                        > Конкретно для сложения и вычитания int-ы и вообще signed типы достаточно расширять до соответствующего unsigned (что само по себе CWE-195, но компилятору можно :)

                        1. Это не расширение. Просто расширить signed до unsigned нельзя: -1 уже ни во что не преобразуется. Можно переинтерпретировать битовую последовательность, что они и делают, насколько я понял (код сильно путаный).
                        2. Там же на строках 724-726 написано дословно то же, что я предлагал для сложения. Вы это читали?
                        3. И повторю в который раз, что фишки конкретного компилятора тут не относятся к стандарту аж никак.

                        Но, попробуйте сделать ассемблерный выхлоп __builtin_mul_overflow() на самых распространённых архитектурах. Вы получите разные варианты игр с получением старшей части «косого» умножения средствами процессора, но не процессоро-независимыми средствами. И даже понятно, почему :)

                        UPD: скосил нетехническую часть, так лучше для долгой истории. Всё остальное осталось в личке.


                      1. mayorovp
                        30.11.2021 12:50

                        Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например a = b = INT_MIN.

                        Посмотрите на код ещё раз — этот вариант там обрабатывается.


                      1. vanxant
                        30.11.2021 13:06

                        Да, посмотрел ещё раз - этот код работает. Хотя и использует 2 ветвления вместо одного.


                      1. ainoneko
                        01.12.2021 07:22
                        +5

                        умножение двух интов сразу даёт long (и гарантированно в этот long влезает что для знаковых, что для беззнаковых).
                        А разве лонг не может быть иногда той же длины, что инт?


                      1. b-s-a
                        30.11.2021 14:42
                        +2

                        Речь про процессоры, а не про языки программирования. Если вам нужно сложить два числа разрядностью 32 бита на 8-ми битном Z80, например, то вы делает это так:

                        LD  A,(DE)
                        ADD A,(HL)
                        LD  (DE),A
                        INC DE
                        INC HL
                        LD  A,(DE)
                        ADC A,(HL)
                        LD  (DE),A
                        INC DE
                        INC HL
                        LD  A,(DE)
                        ADC A,(HL)
                        LD  (DE),A
                        INC DE
                        INC HL
                        LD  A,(DE)
                        ADC A,(HL)
                        LD  (DE),A

                        Как не трудно заметить, сложность тут чисто линейная, чуть дороже, чем сложение 4-х 8-ми битовых чисел. И никакие библиотеки тут не нужны. А вот на языках высокого уровня сделать это значительно сложней, так как ни один язык, что я знаю, не предоставляет информацию о переполнении.


                      1. mayorovp
                        30.11.2021 15:26

                        Речь именно про различие процессоров и языка программирования Си.


                        Отлично, вы написали код сложения двух "длинных" чисел на ассемблере. А теперь попробуйте повторить тот же самый способ сложения на Си.


                      1. b-s-a
                        30.11.2021 15:38

                        Согласен, это только на ассемблере можно так просто сделать. На Си будет чуть сложней.

                        int32_t num1[8];
                        int32_t num2[8];
                        ...
                        int64_t t = 0;
                        for (int i = 0; i < 8; i++) {
                           t += int64_t(num1[i]) + num2[i];
                           num1[i] = int32_t(t);
                           t >>= 32; //result: -1, 0 or 1
                        }
                        if (t != 0) {} //overflow

                        Да, это несколько дороже... Возможно, компилятор при оптимизации сможет что-то сделать, но не уверен.


                      1. mayorovp
                        30.11.2021 15:51

                        А теперь сложите не 8 по 32 разряда, а 4 по 64.


                      1. b-s-a
                        30.11.2021 15:57
                        +1

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

                        ЗЫ: Я знаю, как будет выглядеть код, если пытаться складывать 64-х битные числа.


                      1. mayorovp
                        30.11.2021 16:08
                        +5

                        Вот-вот, "на процессоре" можно, как правило, складывать числа максимальной разрядности, а в языке Си — на 1 шаг меньше.


                        И после этого кто-то ещё пытается утверждать что язык Си к железу близок!


                      1. vanxant
                        30.11.2021 15:56
                        +1

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

                        FYI, в питоне целые сразу произвольной длины "из коробки". Типа как строки. Поэтому для вычислений там все юзают numpy и прочие написанные на С либы:)


                      1. b-s-a
                        30.11.2021 15:59
                        +1

                        Я вот не знаю Питона :-)


                      1. cepera_ang
                        30.11.2021 16:00
                        +1

                        Поэтому для вычислений там все юзают numpy и прочие написанные на С либы:)

                        Не поэтому :)


                      1. netch80
                        30.11.2021 16:00

                        > Речь про процессоры, а не про языки программирования.

                        Процессоры ой разные бывают. Вот смотрим на GMP код для MIPS, со строки 64 — метод может быть повторен 1:1 с использованием беззнаковых целых на каждом шагу, типа такого:

                        void sum(unsigned *a, unsigned *b, unsigned *result, size_t length) {
                          unsigned carry = 0;
                          for (int i = 0; i < length; ++i) {
                            unsigned tmp1 = b[i] + carry;
                            unsigned carry1 = tmp1 < b[i];
                            unsigned r = a[i] + tmp1;
                            unsigned carry2 = r < a[i];
                            result[i] = r;
                            carry = carry1 | carry2;
                          }
                        }
                        


                        Проще никак — флагов нету, всё съели. RISC-V — точно так же.

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

                        Увы, придётся повторить тот же метод. Но это сработает.
                        Для знаковых — сравнить знаки аргументов со знаком результата, тоже легко.

                        Несколько неформальным тут может быть побитовая конверсия между знаковым и беззнаковым одинаковой ширины, но это сейчас реально всеми допускается, а с учётом обязательного дополнительного кода в C++20 (и ожидаемом таком же в C) — проблемы не составит.


                      1. flx0
                        01.12.2021 00:13

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

                        __builtin_add_overflow есть в gcc и clang. Да, этой фичи нет в стандарте, но она есть в реальном мире.


                      1. adeshere
                        01.12.2021 02:29

                        Я не настоящий сварщик, но в Intel Forntran есть возможность получать ошибку при целом переполнении:

                        severe(165): Program Exception - integer overflow

                        FOR$IOS_PGM_INTOVF. During an arithmetic operation, an integer value exceeded the largest representable value for that data type. See Data Representation Overview for ranges for INTEGER types.


                      1. AnthonyMikh
                        01.12.2021 15:45
                        +2

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

                        Rust предоставляет.


                      1. b-s-a
                        01.12.2021 15:58
                        -1

                        Тогда понятно, почему продвигают Rust для разработки ядра.


                    1. b-s-a
                      30.11.2021 14:35

                      Цена 64-битнго сложения на 32-битной машине равна двум 32-битным сложениям: сложение и сложение с переносом. Ничего тут страшного нет. Другое дело, что неправильно так делать для проверки, с учетом того, что сам процессор, обычно, знает, когда произошло переполнение.


                  1. b-s-a
                    30.11.2021 14:50
                    +1

                    Согласен с тем, что в Си нет проверки на переполнение. Было бы очень здорово, если бы оно было в виде int add_int(int *a, int b, int с) { ... *a += b + c; ... } , где возвращаемое значение 0 - если все в порядке, -1 если переполнение в отрицательную сторону, 1 - в положительную. В этом случае очень удобно бы было делать операции с многоразрядными числами.


                    1. netch80
                      30.11.2021 16:04

                      > Было бы очень здорово, если бы оно было в виде int add_int(int *a, int b, int с) {… *a += b + c;… }

                      Почти то же (без знака переполнения) есть в GCC, Clang и вслед за ними ICC. Осталось протолкнуть в стандарт :)
                      Знак можно вычислить по другим признакам (например, если вообще произошло переполнение, то в ваших терминах b!=0 и знак b соответствует направлению переполнения). Этого достаточно.


          1. mva
            29.11.2021 19:01
            -2

            Вот бы ещё у раста с портабельностью (в т.ч. на "старое" железо) было получше...

            А ещё со временем сборки. Как самого тулчейна, так и программ на нём...

            А ещё с весом конечных бинарников...

            INB4: "в 21 веке считать ресурсы".


            1. amarao
              29.11.2021 20:25
              +5

              Размер бинарников во многом зависит от runtime'а (который предоставляет много сервиса, типа разворачивания стека при паниках и т.д.). Рантайм в расте опциональный, и люди вполне пишут под embedded без него.

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

              В целом, я не видел, чтобы rust генерировал код существенно больше, чем Си (для этого надо смотреть на размер кода функций, а не итогового бинаря, потому что, повторю, опциональный рантайм толстоват).

              А вот с поддержкой странных систем... Это да.

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


            1. PsyHaSTe
              01.12.2021 03:34

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


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


          1. Jipok
            29.11.2021 19:49

            А zig?


      1. netch80
        29.11.2021 15:08

        В GCC/Clang это уже частично решили через __builtin_${op}_overflow(). Пока только в избранных местах, без левого сдвига, но это уже хоть какая-то поддержка (можно написать хотя бы переносимый между процессорами враппер).

        Теперь ждём ещё 30 лет для пробивания этого в стандарт.


        1. amarao
          29.11.2021 15:26

          Я не настолько глубоко в gcc, чтобы понять о чём речь. Грубо говоря, если я сделаю int(max_int)+1 - оно такое отловит? А int(max_int/2)*2? А int (min_int)*int(min_int)?


          1. netch80
            29.11.2021 15:32
            +7

            > Я не настолько глубоко в gcc, чтобы понять о чём речь. Грубо говоря, если я сделаю int(max_int)+1 — оно такое отловит?

            Да, отловит. Если вы сделаете, например,

            int a = INT_MAX;
            int c;
            int ovf = __builtin_add_overflow(a, 1, &c);
            
            

            ovf будет установлено в 1 — случилось переполнение.

            > А int(max_int/2)*2? А int (min_int)*int(min_int)?

            Точно так же, через __builtin_mul_overflow().

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

            int32_t a = 999;
            int8_t b;
            int ovf = __builtin_add_overflow(a, 1, &b);
            


            поставит 1 — потому что 999 не помещается в int8_t.

            Эти же интринсики есть в Clang. В GCC есть ещё _p вариант, который позволяет проверить, например, что значение влезло в битовое поле (Clang это себе ещё не перенёс).


            1. amarao
              29.11.2021 15:43

              Спасибо.


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


    1. aversey
      29.11.2021 14:58
      +12

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


      1. ibrin
        29.11.2021 15:17
        +3

        Я понял как решить проблему с UB для знакового! Надо внести в стандарт поведение при переполнении и UB исчезнет, переполение станет предсказуемым!


        1. aversey
          29.11.2021 15:19
          +1

          Именно так. =)


        1. netch80
          29.11.2021 15:34
          +1

          И какое именно поведение будем вносить?

          То, что для беззнаковых производится _молчаливое_ усечение (wrapping, truncating) до младших бит (и это жёстко в стандарте) — это тоже не лучший вариант. Да, проверку на переполнение в беззнаковых операциях делать чуть легче, но всё равно про неё надо явно помнить.

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


          1. BigBeaver
            30.11.2021 11:25

            Честно говоря, проблема в принципе не очень понятна.

            производится _молчаливое_ усечение (wrapping, truncating) до младших бит (и это жёстко в стандарте) — это тоже не лучший вариант
            Это как раз хорошо и логично и часто используется на малопроизводительных железках с высокими требованиями к реалтайму (экономим память и такты на обработку счетчиков в циклических процессах типа генераторов ШИМ, сигнала развертки и тд). При чем, дело тут больше даже не в стандарте, а в том, что оно почти всегда так работает на уровне процессора (если не обрабатывать флаг переноса, если он есть вообще на платформе). То есть скорее просто протечка абстракций.

            Разумеется, при это иподразумевается, что разработчик знает платформу, под которую пишет.


            1. netch80
              30.11.2021 11:37

              > То есть скорее просто протечка абстракций.

              Так в том и дело, что абстракции нижнего уровня становятся неадекватными и вредными на верхних уровнях.
              В идеале, конечно, надо было бы иметь какой-то «безразмерный» int, который везде, где компилятор способен опознать реальный диапазон значений, сокращается до удобного машинного типа. Но если мы не можем себе это позволить, что делать в случае переполнения, то есть несоответсвии сгенерированного результата ожидаемому? Как и при любой ошибке программирования, jIMHO надо генерировать ошибку явно и вышибать выполнение (или просто ставить флаг, если явно попросили, или механизм немедленной реакции не работает).
              (А если компилятор уверен, что переполнения не будет — он выкинет проверку, это законно и желательно.)

              Ну а «на малопроизводительных железках с высокими требованиями к реалтайму» всё равно особая среда, которую можно явно пометить.


              1. BigBeaver
                30.11.2021 12:48

                В целом, я согласен с каждым пунктом.
                Но с другой стороны, мне кажется, что это решается некими coding policy. Либо можно вообще не писать «верхние» уровни на си.


    1. exegete Автор
      29.11.2021 15:06
      +2

      Да, по сути верно. Единственная загвоздка тут в том, что знаковое переполнение, ровно как и многие другие случаи неопределенного поведения, большинством программистов считалось правомерным. В этой позиции есть смысл, если думать о Си как языке низкого уровня. В действительности это не так. Отсюда и огромное количество сломанного кода, и требование откатить изменения компилятора.


      1. vsb
        29.11.2021 17:18
        +7

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


        1. netch80
          29.11.2021 17:21
          +1

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

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


        1. eyudkin
          29.11.2021 17:35
          +14

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


          1. Travisw
            29.11.2021 20:01

            А вот этот комментарий сделал мой день :-)


        1. exegete Автор
          29.11.2021 23:41

          Я этого не говорил да и полагаю, что они оба просто высокоуровневые. Ни один из них не более или не менее высокоуровневый, чем другой, так как нормальных критериев я придумать не могу. Да и какой смысл? Вообще я имел в виду немного другое - большое количество программистов предполагают, что операции в Си будут имеют ровно такое же поведение, как и соответствующие им инструкции на целевой архитектуре. Т.е. если вы скомпилировали код под x86, то битовый сдвиг на размер переменной должен оставить ее нетронутой. В теории и на практике это так не работает. Поэтому Си - это не язык низкого уровня. То, как в стандарте языка определяется битовый сдвиг или знаковое переполнение не влияет на то, станет он от этого более высокого или низкого уровня (если только прямо не сказано, какую инструкцию должен использовать компилятор при трансляции). Да, неопределенное поведение позволяет совершать более агрессивные оптимизации, но всему есть предел. Си просто переполнен UB, приличная часть которого вообще имеет мало смысла. Конкретно проверять знаковое переполнение перед каждой потенциально опасной операцией гораздо неудобнее, чем это делать после.


          1. paluke
            30.11.2021 08:39

            А как переносимо проверять переполнение после? Например, если на какой-то платформе переполнение вызывает прерывание и аварийное завершение программы?
            В каком-то языке (не помню каком) так и было определено поведение при переполнении, и на x86 компилятор инструкции into вставлял после арифметических операций.


            1. netch80
              30.11.2021 09:21
              +1

              > В каком-то языке (не помню каком) так и было определено поведение при переполнении, и на x86 компилятор инструкции into вставлял после арифметических операций.

              TurboPascal при соответствующем режиме компиляции (директива компилятора {$Q+}). Можно было выключать в определённых местах.


            1. exegete Автор
              30.11.2021 17:15
              -1

              Если бы в стандарте языка было указано, что вся целочисленная арифметика явным образом использует дополнительный код, то для тех архитектур, которые ее не поддерживают, пришлось бы имитировать такое поведение. Впрочем, тут нет ничего чрезвычайно страшного - компиляторам языка Си и без этого приходится программно реализовывать те операции, которые целевые архитектуры также не поддерживают. Так, например, для тех машин, которые не содержат инструкций для целочисленного деления и не только, gcc использует специальную библиотеку libgcc. Подробнее можно почитать здесь:

              https://gcc.gnu.org/onlinedocs/gccint/Libgcc.html


        1. vkni
          30.11.2021 04:49
          +1

          Есть и противоположная точка зрения - https://dreamsongs.com/WorseIsBetter.html


    1. netch80
      29.11.2021 15:06
      +17

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

      Нет, всё строго наоборот, насколько я понял вашу метафору.

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

      Разработчики компилятора говорят, что предохранитель утяжеляет конструкцию, и если пользователь не может предусмотреть все ситуации, то он сам виноват, а падать с лошади не надо. (Скрытым намёком идёт, что кто хочет безопасности — пусть берёт пистолеты от Ada.)

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


    1. asor
      29.11.2021 20:47
      +4

      Вообще-то, возможность выстрелить себе в ногу лежит в основе философии Си и вообще Unix. "Unix was not designed to stop its users from doing stupid things, as that would also stop them from doing clever things." (Doug Gwyn)


      1. netch80
        30.11.2021 09:55
        +4

        > Вообще-то, возможность выстрелить себе в ногу лежит в основе философии Си и вообще Unix.

        Возможность намеренно выстрелить себе в ногу. А не неявно и случайно. Это принципиальная разница.

        Вы можете написать rm -rf / и оно удалит все файлы (если от рута). Но надо было его вызвать с -r и -f. Просто «rm /» такого не даёт, и с какого-то раннего момента по одной из этих опций — тоже (причём Bell V6 давало это по rm -r, а System III и ранние BSD, насколько помню, уже нет). «rmdir» тоже, оно удаляет только пустой каталог.

        И это принципиально. Обсуждаемые тут фишки это неожиданный выстрел в ногу от действий, которые такого не предполагали.


        1. asor
          30.11.2021 17:10
          +2

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


          1. demp
            30.11.2021 22:07

            Мне вот интересно стало, если Линусу не нравится реализация GCC, то где можно увидеть ядро, собранное альтернативным компилятором? И легко ли это сделать в принципе?

            Проект, на котором я сейчас работаю, успешно собирается на С++ компиляторах от 3-х вендоров. Этот проект бесконечно мал, по сравнению с ядром Линукса, но и ресурсы на его разработку также бесконечно малы.

            Вопрос: если новый компилятор GCC так не устраивает Торвальдса, то почему он ест этот кактус?


            1. mpa4b
              30.11.2021 22:28
              +1

              Ну вообще-то прямо сейчас ведутся работы для возможности компиляции ядра линукса также и на clang.


              1. asor
                01.12.2021 00:35
                +1

                Не то что "ведутся работы для возможности", а Андроид собирают clang-ом! https://source.android.com/setup/build/building-kernels#customize-build


            1. asor
              01.12.2021 00:29
              +1

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

              https://www.kernel.org/doc/html/latest/kbuild/llvm.html
              https://www.linuxjournal.com/content/linuxdna-supercharges-linux-intel-cc-compiler

              Причём, icc - аж с 2009 года!


              1. demp
                01.12.2021 15:53

                Да, действительно, только что clang-13 без проблем собрал ядро 5.15.6

                Видимо clang стал еще более совместим с gcc и/или из ядра таки выпилили gcc-only код.


        1. PsyHaSTe
          01.12.2021 03:38

          Просто это хреновая философия. У вас же перила на лестнице например стоят и двери у лифта закрываются — зачем? Нормальный человек ведь в шахту не упадет. Надо было специально подойти к двери (-r) и шагнуть вперед (-f), любое одно действие из этих не дают такого результата.


          Кому от этого легче только.


          1. netch80
            01.12.2021 10:34
            +1

            > Нормальный человек ведь в шахту не упадет. Надо было специально подойти к двери (-r) и шагнуть вперед (-f)

            Нет, -r это означает перелезть через перила, если они есть, а -f — проигнорировать отсутствие лифта. Такая аналогия сильно точнее вашей (полного соответствия, естественно, не будет, но и не требуется).

            > Просто это хреновая философия.

            При таких интерпретациях — да.

            > Кому от этого легче только.

            Тому, кто таки обращает внимание на то, что делает.

            Сейчас вон мода в UX отменять запросы подтверждений — мол, их никто не читает и всегда жмёт «да»… но мне они реально помогают, иногда остановиться «перед краем» как раз подумав, что тут может быть что-то не так.


            1. PsyHaSTe
              01.12.2021 13:49

              Нет, -r это означает перелезть через перила, если они есть, а -f — проигнорировать отсутствие лифта. Такая аналогия сильно точнее вашей (полного соответствия, естественно, не будет, но и не требуется).

              То есть -r это что-то плохое чем никто не пользуется? Или это нормальный флаг без которого папочку не удалить если в ней что-то есть?




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


              1. netch80
                01.12.2021 14:25

                > То есть -r это что-то плохое чем никто не пользуется?

                Это флаг, который явно говорит «удаляй каталог несмотря на наличие содержимого», и если он указан — то это значит преодолеть соответствующую защиту.

                > Или это нормальный флаг без которого папочку не удалить если в ней что-то есть?

                Если вы уверены, что надо удалить с содержимым — да. Но надо вначале стать уверенным (или слишком уверенным).
                Для удаления пустого каталога отдельно есть rmdir, и я его регулярно зову в случае, если хочу получить эффект «непустое не удалять!»

                > И если человек влез и погиб то виноват будет не он, а тот кто замок сломал.

                В этой аналогии, как вы представляете себе, чтобы один сломал замок, а другой влез?
                Админ ходит по системам и по ночам пишет `alias rm='rm -rf'`, или как?
                Я, наоборот, часто вижу `alias rm='rm -i'` как защитное средство, и поддерживаю (в интерактивном режиме).

                > У вас же почему-то «ты че дурак не прочитал что написано» — ну ок

                И опять же хромая аналогия. Пусть есть на двери обычная ручка, которая чтобы войти (это как вообще вызвать rm), а есть особая, которую ещё надо нажать несмотря на красный цвет и предупреждение. Кто захочет это делать просто так? Ну да, может, 0.1% захочет. Ну так и файлы себе постоянно удаляют даже с предупреждением.
                Цель всех этих средств — не тотальная защита от всей глупости, это невозможно — полный дурак или самоубийца всегда найдёт метод — а защитить от ненамеренных ошибок. А для них опций типа -r, -f достаточно (при прочих равных).


  1. anonymous
    00.00.0000 00:00


    1. Kircore
      30.11.2021 08:19

      Неправильно.


    1. Nehc
      30.11.2021 10:33
      +9

      Нет, не правильно.

      Суд по листингу, который привел felix-gcc, раньше assert(a + 100 > a) прерывало программу в случае переполнения (что в общем довольно логично — отрицательное число явно не больше положительного), а после очередного обновления — перестало… Типа на основании того, что такое поведение является «неопределенным», а потому — нарушением стандарта и не хрен такой код выводить в прод… ;)

      В вашей аналогии люди знали, что можно случайно выстрелить в ногу, сооружали кастомные предохранители, но в очередной версии кольта компилятора эти предохранители перестали работать… НА основании того, что в инструкции по эксплуатации черном по белому написано: «ЗАПРЕЩЕНО стрелять в ногу!»


    1. norn
      01.12.2021 05:29
      -1

      Простите, что пользуюсь Вашим комментарием, чтобы выразить своё мнение. В своё оправдание скажу, что темы схожи. Если я правильно понял статью, Си должен умереть потому что:

      1. стандарт языка - фуфло;

      2. разработчик (ОДИН!) средства разработки (ОДНОГО!) этого не понимает и отказывается использовать СУЩЕСТВУЮЩУЮ ОПЦИЮ по умолчанию;

      3. в качестве замены, как системного языка, появился юный Rust.

      Я всё верно понял? Т.е., вопрос не к выразительным средствам языка, не к тому, что символ "*" в Си без контекста не понять (что есть правда)?

      Я не защищаю Си и не обвиняю Rust. Я лишь хочу указать на сомнительность аргументации и использование цитат "больших программистов" в своих целях. Как пример последнего, отсылаю к цитате из переписки Торвальдса, который говорит (ИМХО) верно: если документ мешает писать, то к чёрту документ! Из цитаты Торвальдса "Это то, почему мы используем "-fwrapv", "-fno-strict-aliasing" и другие флаги." я делаю вывод, что у него претензии-таки не к Си. Хао! А теперь минусуем. :)


  1. mayorovp
    29.11.2021 14:43
    +2

    В примере с check_password есть ошибка: поскольку буфер pwd — внешний, выкидывать memset для него компилятор не может (по крайней мере, до тех пор пока не заинлайнит тело функции на уровень выше). Да и изменение памяти по указателю на константу является ошибкой само по себе, независимо от оптимизаций.


    Вот какой вызов на самом деле не помешал бы и какой компилятор имеет полное право выкинуть — это очистка буфера real_pwd.


    1. exegete Автор
      29.11.2021 14:47
      +2

      Да, вы совершенно верно отметили, это опечатка. Имелся в виду real_pwd. Спасибо большое, исправлено!


  1. Stronix
    29.11.2021 14:43
    +1

    
    int main()
    {
        int x;
        int y;
        int *p = &x + 1;
        int *q = &y;
        printf("%p %p %d\n", (void *) p, (void *) q, p == q);
        return 0;
    }

    Небольшая поправка, должно быть

    int *p = &x - 1;


    1. aversey
      29.11.2021 14:45

      Бывает по-разному, можно написать -1, можно +1, у меня при использовании clang работает один вариант, а при gcc -- другой.


    1. exegete Автор
      29.11.2021 14:54
      +3

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

          int *p1 = &x + 1;
          int *p2 = &x - 1;
          int *q = &y;
          printf("%p %p %p\n", (void *) p1, (void *) p2, (void *) q);
          printf("%d %d\n", p1 == q, p2 == q); /* 0 0 /*


      1. ainoneko
        29.11.2021 18:45
        +3

        Два компилятора (слишком старых?) -- три мнения ^_^

        ❯ gcc  p_2.c && ./a.out
        0x7ffc001efefc 0x7ffc001efef4 0x7ffc001efefc
        1 0
        
        ❯ gcc -O2  p_2.c && ./a.out
        0x7ffc7ef73664 0x7ffc7ef7365c 0x7ffc7ef73664
        0 0
        
        ❯ clang  p_2.c && ./a.out
        0x7ffccdefd07c 0x7ffccdefd074 0x7ffccdefd074
        0 1
        
        ❯ clang -O2  p_2.c && ./a.out
        0x7ffc1afeabdc 0x7ffc1afeabd4 0x7ffc1afeabdc
        1 0
        
        ❯ gcc --version
        gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
        
        ❯ clang --version
        clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
        


        1. exegete Автор
          30.11.2021 17:46

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


  1. netch80
    29.11.2021 14:58
    +6

    Хорошая сводка тезисов на эту тему, положил в закладки. Ещё бы хорошо добавить ссылки на другие примеры неожиданных оптимизаций и их последствий, типа такого или такого (вообще их штук 20, наверно), блог regehrʼа, аналогичные ресурсы.

    Но, с другой стороны, мне кажется, что именно смерти C (и C++) призывать тут не стоит. А вот что стоит (и об этом говорил много раз) — предлагать формализацию допустимых оптимизаций и обхода таких граблей.
    Сейчас это полностью implementation-defined (отдано на откуп компиляторам). Но уже есть механизм атрибутов и прагм. Чтобы устранить возможные проблемы, надо ввести в стандарт регулирование этих возможностей, например:

    #pragma STDC optimize strict_aliasing off
    

    и до конца блока (исходного файла, если на верхнем уровне) стоит запрет. Или:
    #pragma STDC optimize("Debug") strict_aliasing off
    


    отключено при сборке в Debug-режиме (требуется передача режима команде компиляции).

    Аналогично и для переполнения, проверки на null, и прочих диверсий.

    Ну и вариант типа

    c = [[int_arith(wrapping)]] (a + b);
    


    для атрибутной пометки.

    Для ~95% кода достаточным будет режим с минимумом оптимизаций. Только определённые участки (hot/critical path) заслуживают повышенного уровня.

    Дополнительно замечания по переполнению: 1) начиная с GCC5 есть проверки типа __builtin_${op}_overflow[_p], частично они есть и у Clang, все проблемные места можно уже пометить и явно проверить. 2) так как в C нет исключений, лучше всего делать проверку переполнений при синтаксисе типа a+b через thread-local переменную, аналогично errno.

    Главное — таки продавить это в стандарт (не верю ещё лет ближайших 20).


    1. aversey
      29.11.2021 15:17
      +7

      Спасибо!

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

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

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

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


      1. netch80
        29.11.2021 15:45
        +2

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

        Не нужно такое делать на все оптимизации, мне кажется. Нужно на основные (все — тоже вряд ли получится) случаи UdB в стандарте, которые могут возникать именно из-за ограничений человеческого разума. Два первых кандидата — как раз переполнения и алиасинг. Разыменование пустого указателя — возможно, следующее, но его статический анализатор способен (обычно) опознать анализом кода (хотя был пример неожиданного для человека проявления, сейчас не найду ссылки). В общем случае, да, пересмотреть набор всех случаев (в C++ их несколько сотен, в C должно быть поменьше).

        > не считая необходимости постоянно осекаться и спрашивать «а включил ли я все необходимые свойства сборки?»

        Полиси кода со стандартной шапкой в каждом исходном файле решает это. Потом — и умолчания компилятора (хотя на это вряд ли пойдут).

        > Но кажется что этот язык по сути свернул куда-то не туда и шансов на улучшение у него мало. Но всё может быть, успехов вам! =)

        Вряд ли я лично буду этим заниматься, но всё равно спасибо :)


      1. 0xd34df00d
        29.11.2021 19:23
        +7

        на нём вполне можно писать рабочий код

        Только очень сложно. Локально работающий - да. Код, которому можно доверять - неа.


    1. exegete Автор
      29.11.2021 15:41
      +7

      Спасибо большое за ссылки и комментарий! К сожалению, в текст одной статьи невозможно уместить все сразу, тем более, что бездна UB неисчерпаема. Идея была в том, чтобы провести неискушенного в тонкостях стандарта читателя от достаточно тривиальных ошибок до гораздо более изощренных и опасных случаев неопределенного поведения. Безусловно, говоря о том, что Си должен умереть, я сам не надеюсь на то, что это произойдет. Смысл в том, что вокруг этого языка существует огромное количество серьезных заблуждений, и вызваны они отнюдь не только непрофессионализмом тех, кто на нем пишет. И учитывая историю Си, и то, как написан стандарт, неудивительно, что ошибки неопределенного поведения стали обыденностью. Я сам долгое время считал, что Си - это своего рода кроссплатформенный ассемблер. Потому я полагаю, что концепция языка, который создает столь опасную иллюзию , является по меньшей мере неудачной. И в том случае, если Си остается с нами надолго (а у меня нет оснований считать иначе), очень важно, чтобы программисты четко осознавали то, какой именно инструмент они используют.


    1. 0xd34df00d
      29.11.2021 19:21
      +5

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

      Короче, я пессимистичнее смотрю.


    1. permeakra
      30.11.2021 16:03
      +7

      Я вот считаю, что тут нужно призывать именно к смерти Си. Для низкоуровневого языка он делает слишком много предположений о том, как нам жить. А для современного высокоуровневого языка он не дает достаточной изоляции от платформы.


  1. sys_adm1n
    29.11.2021 15:13
    +1

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

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

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

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

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


    1. aversey
      29.11.2021 15:37
      +1

      Спасибо, мы старались! =)


  1. isadora-6th
    29.11.2021 15:14
    +21

    Автор привел в тексте огромное количество примеров о которых знает каждый новичек в Си. Спасибо, перебрали немного граблей новичков.

    Си это не язык в вакууме -- это текст который транслируется в бинарный вид (LLVM или в машинный) так, как описано в стандарте. Остальное является неопределенным поведением, зависящим от твоей платформы, фазы луны, версии компилятора или стандарта.

    Гарантии не даются не по причине "мы хотим натворить ЗЛО", а потому, что никто не может их дать. Ты пишешь стандарт сегодня, а завтра intel выпустит очередной i7 в котором переполнение int будет работать по другому, а ты завязался на логику, которая является платформа-зависимой. Это абсолютно адекватно, а автор приводит срачи 20 летней давности.

    На тему поломки старого кода -- у Си есть 2 пути: умереть или стать лучше. Си выбрал вариант похоронить старый код, и сделать обычные случаи применения на 10% быстрей. Переезд на новый стандарт для старого кода означает ускорение его работы, и перепись особо кривых мест. Ребята которые используют грязные хаки за границами стандарта прекрасно понимают "что они делают" и почему они это делают. Спойлер: ради скорости.

    У староверов, есть выход: отвязаться от платформы либо привязаться к конкретной версии стандарта и компилятора.

     Благодаря нему у нас есть ядро Linux и тысячи уязвимостей в нём же в придачу.

    Благодаря Си Linux самая популярная серверная ОС. А еще мы не пишем на ASM для МК в основном благодаря стандарту.

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

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

    К языку Си есть ряд требований. Первое из которых -- быть быстрым, компактным, работать везде (самый переносимый язык кстати, один раз написал (если в пределах стандарта) собрал для любой платформы). Говорить, что Си плохой потому, что легко прострелить ногу, это как жаловаться, что ножем можно порезаться.

    Особенно меня радует истории про "-fstrict-aliasing", который при включении хоронит твою производительность, т.к. нельзя теперь применять жесткие оптимизации зато работает "не стандартный код". Если ты лично решил, что умней компилятора и оптимизатора, ты можешь собрать свою 1 функцию в отдельном файле с "-fstrict-aliasing", проблема высосана из пальца.

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

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


    1. netch80
      29.11.2021 15:26
      +22

      > Автор привел в тексте огромное количество примеров о которых знает каждый новичек в Си.

      Курсы для новичков про это обычно не рассказывают. Затем и нужны статьи, чтобы потом объяснять, что реально здесь водится целая стая голодных драконов.

      > Гарантии не даются не по причине «мы хотим натворить ЗЛО», а потому, что никто не может их дать. Ты пишешь стандарт сегодня, а завтра intel выпустит очередной i7 в котором переполнение int будет работать по другому, а ты завязался на логику, которая является платформа-зависимой.

      Нет и ещё раз нет.
      Во-первых, вы почему-то принципиально думаете только о варианте, когда переполнение замалчивается. Да, -fwrapv делает это. Но нормальная реализация должна давать оповещение, а не замалчивание. Для C++ этим было бы throw. Для C такого нет, но в стандартной библиотеке есть errno, которое уже показывает пример, что thread-local переменная может быть использована для оповещения об ошибке. Или не thread-local, а указанная контекстным атрибутом.

      Во-вторых, если вы не в курсе, C++20 уже постановил, что кроме дополнительного кода варианта нет. C в следующем стандарте, вероятно, последует за ним. Вероятность отката от этого уже нулевая. Так что предположения про «а завтра intel выпустит очередной i7 в котором переполнение int будет работать по другому» это не более чем голые фантазии.

      А так — C изначально завязан на кучу вещей: например,
      1) само по себе двоичное (не троичное, не какое-то другое) представление
      2) битовое представление числа в обычном счислении (а не в каком-нибудь коде Грея)
      3) ячейки-«байты» не менее чем по 8 бит
      4) память с последовательной нумерацией ячеек (не может быть, что в массиве от 1000 до 2000 — 1001 пропущено, а 1002 есть)

      и 100500 других признаков типовых современных платформ. И что, кто-то их собрался отменять?

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

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

      А именно такая механическая помощь человеку — задача компилятора. Если надо проверять переполнение, это должен делать компилятор, а не человек. Человек должен думать над задачами для человека. Сейчас же компилятор только мешает, и каждая такая новая оптимизация мешает чем дальше, тем резче и неожиданнее.

      > Если ты лично решил, что умней компилятора и оптимизатора, ты можешь собрать свою 1 функцию в отдельном файле с "-fstrict-aliasing", проблема высосана из пальца.

      Нет, не высосана, потому что 1) требуется отдельный файл (include-only уже не годится, инлайнинг не сработает), 2) опции компиляторо-зависимы и присутствуют не везде.


      1. 0xd34df00d
        29.11.2021 19:26
        +6

        Но нормальная реализация должна давать оповещение, а не замалчивание. 

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


        1. netch80
          29.11.2021 19:54
          +3

          > Идеальная нормальная реализация требует доказательства отсутствия переполнения во время компиляции,

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


        1. Nehc
          30.11.2021 10:44

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

          Наверное плохо, что код был написан так. Наверное автор кода не прав в том смысле, что использовал в коде неопределенное поведение относительно которого было отдельное предупреждение. Но плохо, что поведение компилятора изменилось в моменте, а кодовая база осталась прежней… Это реально чревато проблемами в самых неожиданных местах. Причем «тихими» проблемами. Было бы лучше, если бы переполнение стало везде приводить к ошибке…

          Да и… Если честно я не понял логики разработчика… Почему при сравнении отрицательного числа с положительным оно внезапно не меньше?! Компилятор не производит вычислений? Типа видит что слева Х+константа, а справа просто Х и без вычисления понимает что справа слева больше, так что ли? "Хороший тамада и конкурсы интересные"(с)


      1. Kircore
        30.11.2021 08:48
        -4

        1) само по себе двоичное (не троичное, не какое-то другое) представление
        2) битовое представление числа в обычном счислении (а не в каком-нибудь коде Грея)
        3) ячейки-«байты» не менее чем по 8 бит

        Это точно проблема языка, а не существующего железа? Для последнего пункта есть bit fields.

        4) память с последовательной нумерацией ячеек (не может быть, что в массиве от 1000 до 2000 — 1001 пропущено, а 1002 есть)

        Ты уверен, что в Си есть массивы? Точно знаком с языком?

        и 100500 других признаков типовых современных платформ

        Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.


        1. netch80
          30.11.2021 10:14
          +2

          > Это точно проблема языка, а не существующего железа? Для последнего пункта есть bit fields.

          Вы вообще смотрите, на что отвечаете? Я говорю, что это требование языка к среде реализации кода: память состоит из сущностей, именуемых «байт» и имеющих не менее 8 бит. При чём тут битовые поля?
          Если машина, например, с 6-битными байтами (как было типовым решением в 1950-60-х), то «байт» C будет состоять из двух байтов этой машины.

          > Ты уверен, что в Си есть массивы? Точно знаком с языком?

          Какие возражения против сказанного? Вы не в курсе, что массивы располагаются в памяти в последовательных адресах?

          > Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.

          В чём возражение? Или вы не попытались понять тезис?


          1. Kircore
            30.11.2021 23:43
            -2

            Если машина, например, с 6-битными байтами (как было типовым решением в 1950-60-х)

            А напомни, когда разработан Си и сколько таких машин было с того времени? Не было надобности - нет реализации.

            Какие возражения против сказанного?

            Утверждение, что в Си есть массивы.

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

            Элементы массивов? В каком языке?

            В чём возражение?

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


            1. netch80
              01.12.2021 00:53
              +3

              > А напомни, когда разработан Си и сколько таких машин было с того времени? Не было надобности — нет реализации.

              Поищите «comp.lang.c FAQ» с примерами странных архитектур.
              Машины с 18- и 36-битными словами дожили до начала 90-х. C на них был.
              Скажете, это всё легаси? Собственные разработки Cray имели 32-битные слова без возможности байтовой адресации, на них и CHAR_BIT сишный был равен 32. А это уже конец 70-х, начало 80-х.
              Ранние Alpha могли адресовать память только словами, байты там эмулировались (до появления BWX extensions), это 1995.
              Ну ладно про всякие PDP-1 и Cray вы могли не знать, но как мимо вас прошла Alpha?

              > Утверждение, что в Си есть массивы.

              Я честно не хочу влазить в режим language lawyerʼа плохого пошиба, поэтому скажу так: если я вижу определение типа `int a[1000];`, то я вместе со 100500 других источников, включая учебники C, называю это массивом, как бы это кому-то ни не нравилось.

              > В требовании абсурдной ерунды, которой ни в одном языке нет за ненадобностью.

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


              1. Kircore
                01.12.2021 03:09
                -1

                Машины с 18- и 36-битными словами дожили до начала 90-х. C на них был.

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

                Собственные разработки Cray имели 32-битные слова без возможности байтовой адресации, на них и CHAR_BIT сишный был равен 32.

                И сейчас такие системы есть, но в твоей реальности они не существуют.

                Я честно не хочу влазить в режим language lawyerʼа плохого пошиба, поэтому скажу так: если я вижу определение типа int a[1000];, то я вместе со 100500 других источников, включая учебники C, называю это массивом, как бы это кому-то ни не нравилось.

                Называй как хочешь, устройство языка это не меняет.


                1. PsyHaSTe
                  01.12.2021 03:46

                  https://queue.acm.org/detail.cfm?id=3212479


                  cc netch80 полезный референс на тему


                  1. Kircore
                    01.12.2021 04:26
                    -2

                    И к чему эта простыня?


                  1. netch80
                    01.12.2021 11:18

                    Да, оно у меня в закладках. Проблема в том, что C одновременно и такой высокоуровневый язык, как там описывают, и низкоуровневый, и вот это сочетание даёт как минимум заметную часть текущих проблем. С C++ ещё хуже — сохраняя всё это, он добавляет ещё несколько слоёв вплоть до отдельного compile-time языка темплетов. Понимать и поддерживать всю эту кашу можно только думая на нескольких уровнях.
                    Исходная же статья частично поднимает вопрос — а как всё это получилось? Абьюзинг всех изначальных UdB только часть этой проблемы (хотя и самая очевидная).


                1. netch80
                  01.12.2021 10:44

                  > Не, ты мне реализацию шестибитных байтов обоснуй.

                  Ну я тут не знаю… попробуйте историю вычислительной техники почитать, что ли? Там много интересного есть. Например, что в 50-60-х 6-битные байты были большей нормой, чем 8-битные. Что при проектировании знаменитой System 360 большинство голосов начальства были за 6-битность, и то, что Gene Amdahl отстоял 8-битность, было великим подвигом с его стороны (всупереч голосам про бессмысленную трату ресурсов), что на него ещё несколько лет шипели, пока окончательно не утвердилось преимущество 8-битки. Что PDP-1 была в 1958, а знаменитая PDP-11 аж в 1970, а в промежутке между ними было множество моделей со всякими словами по 18 бит. Что линия PDP в заметной мере разрабатывалась под нужды армии (PDP, в отличие от S/360, были мобильными! в переводе с армейского — могли переезжать на грузовиках), а там своя специфика. Ну и прочее…

                  > И сейчас такие системы есть, но в твоей реальности они не существуют.

                  В моей реальности _под которую пишу_ я и под которую пишет 99% тут присутствующих — да, не существуют.
                  За пределами её есть 8-битные процессоры, есть секвенсоры, есть много чего, но это другой слой.

                  > Называй как хочешь, устройство языка это не меняет.

                  Верно. И в этом устройстве массивы есть, но вы об этом не хотите знать, судя по данному ответу.


              1. permeakra
                01.12.2021 11:49

                18-битная и другая странная арифметика до сих пор встречается в DSP, AFAIK. Да, программируют их обычно не на С, но как явление их упомянуть надо.


                1. netch80
                  01.12.2021 13:15

                  > 18-битная и другая странная арифметика до сих пор встречается в DSP, AFAIK. Да, программируют их обычно не на С, но как явление их упомянуть надо.

                  Ну некоторые DSP вроде и на C программируют (с интринсиками везде где можно). Но для таких, где 18 бит, да, это уже кажется вряд ли — не по факту, а по тому, что современным программистам непривычно.

                  А так — я могу и на amd64 применить int16_t, int18_t и всё такое, если их предоставит компилятор. Правда, все операции с ним будут получать автоматический integer promotion до int32_t (равному int), но при укладке в переменные урежется обратно (и это ещё одна проблема на подумать).

                  LLVM в этом смысле сильно более современен — и появляются языки, которые начинают это напрямую использовать (Rust, Swift...): integer promotion нет, операции выполняются в естественном размере типов. Более того, int может быть шириной любой от 1 до 2**23 бит (не представляю себе, зачем такая безумная ширина, но сложение-вычитание-умножение делается тривиально). Используя это, уже можно выполнить адекватную укладку на размеры необходимого. Если мы знаем, что получили определение типа

                  type Temperature = new Integer(-90...99); // даже в Оймяконе ниже не бывало


                  то дальше компилятор получит полную возможность укладывать его в i7 и делать операции в соответствующем пространстве (и избавляться от явных проверок, где не надо). Помощь человека в виде «а int8_t достаточно или нет?» тут уже не нужна.

                  Ещё это полезно в плане укладки данных в битовые поля (там, где сейчас ширина поля отделена от типа, на самом деле лучше подходит явное указание в виде int<3>, uint<11> — это автоматически позволит и проверять границы при укладке в значение такого типа). Сейчас в GCC чтобы проверить влезание данных в битовое поле пишется что-то вроде `__builtin_add_overflow_p(val, 0, dest_field)` (это проверка без собственно размещения), это даже Clang ещё не скопировал.

                  LLVM даже проверки влезания в такой размер делает без проблем — при переносе на реальную платформу типа x86 используя хранилище достаточной ширины (32, 64 бита) и добавляя проверки на границы.

                  Но чем более высокоуровневое программирование нужно, тем ценнее возможность проверки границ и указания множества значений (диапазонами или как-то иначе), а не просто какой-то [u]int${N}_t.


                  1. permeakra
                    01.12.2021 13:26

                    Вопрос производительной и/или тонковывернутой арифметики сильно шире вопросов ширины арифметики как таковой, имхо. Там очень много тонкостей семантики, какие-то из которых важны для корректности результата, а какие-то для производительности. И на уровне языка системного программирования и тем более llvm asm эти тонкости теряются.


                    1. netch80
                      01.12.2021 13:40

                      > Вопрос производительной и/или тонковывернутой арифметики сильно шире вопросов ширины арифметики как таковой, имхо.

                      «Тонковывернутой»… это о чём? Какие-то особые операции типа ДПФ в целых числах? Ну если их на C не описать, то по крайней мере интринсики уже способны их задать, а дальше дело компилятора, что он и как вызовет.

                      > И на уровне языка системного программирования и тем более llvm asm эти тонкости теряются.

                      На уровне LLVM IR таки, после укладки на дополнительный код и с дополнительными всякими nuw/nsw, оно достаточно неплохо отражается — можно восстановить и что это было вначале. Диапазоны, да, не сохраняются, но это там уже, наверно, и не нужно. Хотя тут могу и пропустить что-то важное.


                      1. permeakra
                        01.12.2021 14:24

                        Не только и даже не столько, хоть и близко. Операции типа DFT пишут специально обученные люди, обложившись мануалами, и пишут как правило под конкретное железо и один раз. Поэтому там вполне можно писать непосредственно в асме, если уж надо (см библиотеки типа openblas, где внутренние циклы натурально написаны на асме целевой платформы).

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

                        Если нужна производительность, то в качестве примера можно рассмотреть агрегацию значений сложной в вычислении функции на целочисленной сетке, причем распределение значений на этой сетке имеют нетривиальную симметрию и про некоторые диапазоны заранее известно, что их обходить не нужно. (см wikipedia://polytope+modell)

                        В любом случае, речь идет о том, что вопрос оптимизации и/или корректности вычислений часто оказывается завязан на неочевидную и нетривиальную информацию о входных данных, которые в модели вычислений на уровне С/llvm asm не отображается. Некоторые современные компиляторы достаточно умны, чтобы её из кода реконструировать (напр. gcc/graphite) но то такое.


        1. unC0Rr
          30.11.2021 12:29
          +1

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


          1. Kircore
            30.11.2021 23:49
            -1

            Конечно, массивы в Си есть

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

            стоит положить массив в структуру

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

            В Си нет массивов, они введены в синтаксис добавлением смещения к указателю.


            1. netch80
              01.12.2021 00:56
              +5

              > Наверняка для них размерность и тип задаются, да? Или исключительно указатель на начало даже без типа?

              Ага, вы таки перепутали хрен с пальцем.

              Когда написано: `int a[1000];` — это массив.

              Более того, sizeof(a) выдаст в этом случае 4000, а не 4 (считая, что int — 4 байта).

              А то, что в ряде случаев указание на массив превращается в указатель на первый элемент (индекс 0), не устраняет факт наличия такого типа.


              1. Kircore
                01.12.2021 02:46
                -6

                Когда написано: int a[1000]; — это массив.

                А когда после используешь a или *a - это указатель. Сам ты хрен с пальцем.

                Более того, sizeof(a) выдаст в этом случае 4000, а не 4 (считая, что int — 4 байта).

                У тебя вызов sizeof() заменится константой на этапе компиляции.

                Очень удобно критиковать язык, не зная его.


        1. AnthonyMikh
          30.11.2021 20:17

          Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.

          Таки машины на троичных кодах были.


          1. Kircore
            30.11.2021 23:51

            Были, не спорю. Но для них не нужен был язык Си.


    1. aversey
      29.11.2021 15:49
      +3

      Дополню ответ @netch80с которым согласен. Вы описываете Си как язык высокого уровня, и конечно тогда он имеет право давать любые гарантии и любую модель исполнения. Вот только как язык высокого уровня он плох и даёт сложные для понимания и контроля гарантии (если для вас это не так -- поздравляю, вы в сотни раз умнее меня =) ) -- и тогда вопрос -- а зачем он нужен? Ведь когда мы пишем программы мы хотим что бы они работали -- а в силу сложности Си гарантировать это ... сложно. И при этом не важно насколько быстро они работают -- если они работают с ошибками, неправильно -- то они просто не работают.


      1. Ritan
        29.11.2021 16:14

        Только вот проблема, что ни один язык не может дать таких же гарантий производительности. Переписал тут недавно небольшую ВМ на rust - замучался выпиливать проверки границ( при помощи того самого страшного unsafe кода ), т.к. с ними было медленее на 10-30% в зависимости от семпла. И даже после этого только приблизился к результату с++


        1. 0xd34df00d
          29.11.2021 19:41
          +2

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


          Можно было бы сказать, что ассемблер даёт какие-то гарантии, но там тоже всё неочевидно.


    1. Mingun
      29.11.2021 20:03

      Программисту, стоит указать, что этот код оптимизировать не нужно, и бац проблемы не существует.

      Так как, как указать, что код оптимизировать не нужно!?


      1. Kircore
        30.11.2021 08:53
        -1

        Зависит от компилятора. Должно быть что-то вида:

        #pragma NO_OPTIMIZE_START
        ...
        #pragma NO_OPTIMIZE_END


        1. netch80
          30.11.2021 09:06

          А не должно зависеть от компилятора, в чём и основной вопрос…


          1. Kircore
            30.11.2021 23:51
            -2

            Кому не должно?


        1. Mingun
          30.11.2021 18:07

          История с gcc в статье говорит, что это ничего не гарантирует.



    1. AnthonyMikh
      30.11.2021 20:13
      +4

      Благодаря Си Linux самая популярная серверная ОС

      Не-а. Я бы даже сказал, что Linux — это великолепная иллюстрация тому факту, что крупные проекты на C писать невозможно. Почему? Да потому что Linux написан не на C, а на C с расширениями языка от GCC — вообще говоря, другой язык.


      (самый переносимый язык кстати, один раз написал (если в пределах стандарта) собрал для любой платформы)

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


    1. PsyHaSTe
      01.12.2021 03:44
      +3

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


      Фишка в том, что из двух извечных вопросов важный "что делать", а виноватых искать — последнее дело. Человеки — ошибаются и им нужна помощь машины в виде компилятора чтобы писать сложные программы. И чем больше этой помощи тем лучше. В си из-за особенностей языка такой помощи дождаться тяжело, и изменить это не меняя самого языка не выйдет.


  1. MentalBlood
    29.11.2021 15:39
    -1

    Относится к действительно неопределенному поведению как к неопределенному — разумно
    Относится к действительно неопределенному поведению как к определенному — рискованно, если не глупо


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


    1. MrDEX123
      29.11.2021 16:56
      -1

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


      1. Mingun
        29.11.2021 20:07
        +3

        Тем не менее, компилятор почему-то выбрал не спустится на уровень своей платформы и не реализовать "неопределенное поведение" определенным именно для этой платформы образом (для чего этот термин изначально и вводился!), а сделать так, как ни одна платформа не делает и молча. Это свинство.


        1. kmeaw
          30.11.2021 01:04
          +1

          Для таких случаев используются термины unspecified behavior и implementation-specified behavior.

          Это свинство.

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


          1. Physmatik
            30.11.2021 02:49
            +4

            Скажите, пожалуйста, сколько людей на планете физически способны писать без UB (опуская вопрос "нужно ли писать без UB", всё-таки UB — это стандарт, а мнения по его адекватности расходятся)? И если ответ "бесконечно мало", то не является ли свинством делать то, что делают современные компиляторы?


            1. kmeaw
              30.11.2021 03:11
              +1

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

              Чем же тогда контракт "я не буду писать UB-код" хуже контракта "я не буду совершать off-by-one errors" или "я не буду допускать логических ошибок при инвалидации кэша"?

              Современные процессоры при обработке данных делают не в точности то, что написано в исполняемом коде, а могут сделать что-нибудь другое, но так, чтобы результат был неотличим от непосредственного исполнения каждой инструкции наивным образом при условии, что программа не содержит data races — и большинство пользователей процессора довольны тем, ведь это позволяет ему работать быстрее, хотя очень мало людей могут никогда не допускать гонок в своих программах. Оптимизирующие компиляторы C делают нечто похожее — они генерируют из исходника такой код, результат работы которого неотличим от интерпретации исходника абстрактной C-машиной при условии, что программа не содержит UB.


              1. Mingun
                30.11.2021 07:50

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

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


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


                1. svr_91
                  30.11.2021 10:38

                  Но всеже программы мы пишем для людей, а не для машин. Если программа работает неправильно, то она работает неправильно


              1. netch80
                30.11.2021 11:16

                > Чем же тогда контракт «я не буду писать UB-код» хуже контракта «я не буду совершать off-by-one errors» или «я не буду допускать логических ошибок при инвалидации кэша»?

                Off-by-one практически всегда ловятся в пределах одного экрана (ну, можно сделать, чтобы не поймались так, но это надо постараться). В случае кэша есть гарантированный метод — не идти на всякие lock-free, а ограничиться мьютексами (или вообще обменом сообщениями): дороже, но надёжно. В случае описанных ошибок C защититься без полного контекста (включая всякие библиотечные функции, которые могут обновляться независимо от основного кода) нереально, и вопрос не в производительности — если бы было только в ней, большинство бы использовало безопасный режим по умолчанию (как я и предлагаю в десятке соседних комментариев).


          1. Mingun
            30.11.2021 07:35
            +2

            Хорошие системы предполагают, что кроме известных правил вас предупредят о том, что вы близки к тому, чтобы их нарушить. Пора очнуться от ощущения, что разработчик способен уместить в своей голове всю программу — это не так уже лет 20. Это забота компилятора. Собственно, основная претензия к C/C++ компиляторам именно в том, что они делают преобразования молча. И не говорите мне, что контекст к тому времени уже потерян. Просто и нет попыток его сохранить.


          1. netch80
            30.11.2021 10:21
            +3

            > Разрабатывая программу на стандартном C, разработчик принимает контракт,
            > Эти правила известны разработчику заранее.

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

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


        1. MrDEX123
          03.12.2021 15:53

          А как спуститься на уровень платформы, если на 2х разных платформах подразумевается совершенно разное поведение с разными результатами? Тот же пример со знаковым переполнением, понимаю что заезженно и уже неактуально после с++20, но гипотетически на платформе Winux отрицательные числа представлены не не доп кодом, то есть невозможно один и тот же код допускающий ub скомпилить, чтобы он выполнял одну и ту же работы для этой платформы и, например, linux


  1. homm
    29.11.2021 15:49
    +14

    Говоря об упомянутом в письме ядре Linux, его автор Линус Торвальдс также дал свою оценку strict aliasing в частности и работе комитета в целом.

    Когда знаешь, что следующие три минуты чтения пройдут очень весело.


  1. uuuuuuuu
    29.11.2021 15:50
    +24

    Си никому ничего не должен. Считаете, что он не нужен — просто не используйте его.


  1. gameplayer55055
    29.11.2021 15:51
    +1

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


    1. le2
      29.11.2021 19:58
      +5

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

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

      Требуется радикальная смена парадигмы. Такой вычислитель, который отправит в каменный век Unix.


      1. MinimumLaw
        30.11.2021 07:35

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

        Прям не смерть, а буддистское перерождение. "А после из прораба до министра дорастешь" (с)


  1. MinimumLaw
    29.11.2021 15:55
    -2

    Хорошая статья. Плюсанул везде, где мог.

    От себя скажу лишь одно - умрет. Обязательно умрет. Как умер Кобол, как умер Фортран, как умер Ада, как умер Паскаль... Так устроена жизнь - старички умирают уступая место молодым. Самые легендарные продолжают жить в мифах и легендах. Си уже более чем седовласый старичок. За время его жизни сменилась не одна парадигма программирования и не одно поколение вычислительных машин. Потому даже не сомневаюсь - умрет.

    Вопрос ведь не в том умрет ли. И даже не в том когда именно. Вопрос в том, что будет потом. Появится ли тот самый "квантовый компьютер" - единственный и неповторимый, под который получится написать сразу и бесповоротно единственный и непротиворечивый стандарт без костылей в виде неопределенного поведения в бесконечном будущем? Помнится JAVA VM в свое время пыталась стать таким вот "универсальным компьютером"... И не учит ли нас та же JAVA VM тому, что у такого пути есть свои недостатки? Или у нас будет зоопарк архитектур, в которых подсчет бит в виде BE/LE и особенности работы со знаковыми и беззнаковыми числами это минимальное из зол? И кто возьмется написать что-то, что накроет все прошлые и еще живые архитектуры в купе с настоящими и будущими?

    У меня нет ответов ни на один из вопросов.


    1. aversey
      29.11.2021 16:02
      -1

      Всё течёт, всё меняется. =)

      К сожалению я тут немного пессимистичен, Никита ещё более -- опыт PL/I, например, видимо не был учтён, многие языки сегодня всё так же безразмерно разбухают. Боюсь что с Си будет то же -- он уйдёт, на его место придёт почти такой же, не сделавший выводов из ошибок прошлого. Надеюсь эта статья поможет кому-то в будущем сделать лучше, хотя это и излишне оптимистично. =)

      Что касается заданных вами вопросов -- я тоже не имею на них ответов. =)


      1. MinimumLaw
        29.11.2021 16:36
        +1

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

        Мне кажется нас ждет некоторая "семиуровневая модель OSI" применительно к вычислительной технике. Где каждый уровень будет максимально изолирован от соседнего. Ассемблер (в смысле машинные коды) в обозримом будущем скорее всего не исчезнут. Значит нужен будет слой абстракции, который так или иначе приводит их к некому единому формату. В идеале максимально эффективно. Опять же в идеале не противоречиво. Вопрос лишь в том как этого достичь...

        В принципе, сейчас наблюдается что-то такое. Абсолютно не ведающие про "безопасность" машинные коды накрываются уровнем ядра операционной системы. Выше libc или ее аналоги WinAPI. Еще выше всякие QT/GTK/MFC/NET. Другое дело что сегодня нет строгой изоляции между этими уровнями...

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

        Мне кажется, что первый вариант на сегодня более вероятен. Но много я на него не поставлю. Как, впрочем, и на второй. И на третий, и на десятый, и на сотый... Поживем - увидим, доживем - порадуемся.


        1. ignat99
          01.12.2021 11:42

          Семиуровневая модель реализованная в железе. Уже сейчас можно использовать Систем С или Верилог или любой другой HDL.

          Единого формата не будет. Это скорее всего мечты юношей. Будут узкие специалисты. Каждая группа со своим собственном языком.

          Дело в физических процессах, а всё остальное это бантики сверху.


    1. Ritan
      29.11.2021 16:10
      -9

      И какая же парадигма программирования сменилась? ФП - мёртворождённое, а императивщину никто не отменять не собирается


      1. 0xd34df00d
        29.11.2021 19:43
        +7

        ФП — мёртворождённо

        Почему?


        1. Physmatik
          30.11.2021 02:56
          -3

          Фундаментальная несовместимость с физическими императивными вычислителями? Не, я слышал что-то про прототип Лисп-машины, но сейчас-то их нет нигде.


          А вне контекста этой статьи (системное программирование) всё у ФП прекрасно, разумеется.


          1. 0xd34df00d
            30.11.2021 03:05
            +9

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


            Но даже если считать, что ФП — это про функции, то почему map f… тьфу ты, извините, std::transform(begin, end, output, f); принципиально несовместимо с железом, а


            for (auto it = begin; it != end; ++it, ++output)
              *output = f(*input);

            совместимо? Особенно учитывая, что компилятором обе конструкции разворачиваются в примерно одинаковый код?


            Более того, для системного программирования я могу пойти и взять ivory, или просто, как сделали seL4, взять хаскель и обмазаться верификацией.


            1. red75prim
              30.11.2021 06:07
              +1

              просто, как сделали seL4, взять хаскель и обмазаться верификацией.

              Точнее, взять хаскель, написать прототип реализации, верифицировать. Смотря на этот прототип, написать реализацию на подмножестве С для улучшения производительности, верифицировать эту реализацию. См. http://www.sigops.org/s/conferences/sosp/2009/papers/klein-sosp09.pdf


            1. mayorovp
              30.11.2021 10:53
              -1

              Что-то ваша ссылка на ivorylang.org не работает...


              1. 0xd34df00d
                30.11.2021 11:14
                +2

                Только что проверил — открывается.


                1. mayorovp
                  30.11.2021 11:18
                  -2

                  Только что проверил — не открывается и не пингуется.


                  1. 0xd34df00d
                    30.11.2021 11:20
                    +9

                    Перепроверил с ssh-туннелем через машину в РФ — и правда, не открывается. Санкции, не иначе.


            1. Physmatik
              30.11.2021 19:11

              Скажите, насколько просто на Хаскелле реализовать, например, in-place quicksort (in-place обязателен)?


              1. vkni
                30.11.2021 19:41
                +1

                Очень просто - пишете в гугеле haskell in-place quicksort и копируете оттуда. Занимает примерно столько же, сколько in-place код на C, т.к. это непосредственный перевод.


                1. Physmatik
                  01.12.2021 19:18

                  Во-первых, попробуйте найти. Типичные in-place версии требуют O(N) памяти (что сразу убирает половину смысла in-place алгоритма).


                  Во-вторых, это ведь "например". Далеко не всегда вы сможете скопировать код необходимого вам алгоритма где-нибудь в интернете.


                  1. 0xd34df00d
                    01.12.2021 19:36
                    +5

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


                    Это как если бы ваш алгоритм сортировки на плюсах принимал пару произвольных итераторов (не только random access) и внутри делал из этого std::vector.


              1. 0xd34df00d
                30.11.2021 23:10
                +2

                Так же, как и на любом другом языке. Делаете thaw вашему (иммутабельному вектору), модифицируете что надо in-place, делаете freeze. Я бы сел и написал, но мне откровенно неохота включать мозги для того, чтобы написать правильный партишон.


                1. Physmatik
                  01.12.2021 19:11

                  https://hackage.haskell.org/package/array-0.5.4.0/docs/Data-Array-MArray.html


                  Converts an immutable array (any instance of IArray) into a mutable array (any instance of MArray) by taking a complete copy of it.

                  https://hackage.haskell.org/package/vector-0.12.3.1/docs/Data-Vector.html


                  O(n) Yield a mutable copy of the immutable vector.

                  То есть O(N) по памяти, насколько я понял?


                  1. 0xd34df00d
                    01.12.2021 19:19
                    +1

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


                    Там рядом есть modify, которая должна это скрывать, но она внутри делает некую магию, которую не все любят.


      1. nin-jin
        30.11.2021 09:24

        ООП, Реактивное Программирование, Логическое Программирование.


    1. chernish2
      29.11.2021 17:12
      +5

      Это видимо сарказм, про мертвые языки? Всё перечисленные очень живы, и даже пресловутый Cobol.


      1. MinimumLaw
        30.11.2021 07:15

        Безусловно. Не все, к сожалению, понимают сарказм. Особенно когда нет соответствующего тега.


        1. ignat99
          01.12.2021 11:52
          +1

          выше всякие QT/GTK/MFC/NET.

          Начтите с A2, но там много косяков с памятью у студентов и аспирантов Вирта.

          Посмотрите wxWidget - запускается на многих платформах. Ну потом уже делайте суждения про "всякие". А лучше напишите своё, только ради бога не на JS.

          Дело вовсе не в библиотеках, а в конкретных устройствах под которые эти оконные интерфейсы портировали (Начиная с 2004 года). IMHO

          На мощных компьютерах и в 2000 не было ни каких проблем. Брался Енлайгмент, KDE или будущий Гном или Next и т.д. и всё работало. Пока не набежали милиниалы и не начали "улучшать" архитектуру. Теперь у нас много проблем после этих "улучшений".

          А вот Doom до сих пор портируется на почти всё что считается микропроцессором.


          1. MinimumLaw
            01.12.2021 12:20

            Боюсь мой уровень сильно ниже. В основном от reset-вектора до exec("/sbin/init"). А порою даже ниже, чем вектор сброса.

            Собственно потому и говорю что сложно рассуждать про чужие огороды. Там все не так, как у меня. Мое "всякие" не содержит негативного оттенка. Оно скорее меня характеризует. Как человека не знакомого с данным конкретным слоем. Каждому свое.


            1. ignat99
              01.12.2021 13:05

              Я делал драйвера для первого смартфона с Линукс компании Самсунг Електроникс. В основном это об процессах в dev/mem Пришлось сделать для контроллера памяти и видеобуфера изменения, чтоб запустить всё это на новом железе.

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

              Как например слухи о скорой смерти СИ.


              1. MinimumLaw
                01.12.2021 13:29

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

                Как например слухи о скорой смерти СИ.

                Знакомый ник. Мы ж с вами где-то уже пересекались на тропке хабровских комментариев... Занятно все это... Вот я уже миллениал (хотя практически ровесник языку С - 1979-ого года выпуска). Вот я уже распускаю слухи о скорой смерти языка С (основного моего рабочего инструмента). Весело.

                Я ж не писал что С скоро умрет. Я писал "не сомневаюсь - рано или поздно умрет". И дальше список условно мертвых языков. Каждый из которых когда-то был универсальным, а потом оказался нишевым. И в своей нише продолжат жить (а в некоторых случаях еще и процветать). С (классический, не приплюснутый) уже давно не универсальный язык. И названные в статье проблемы весьма в немалой степени этому способствовали. Это достаточно очевидно. Как очевидна и связка С с (ранним?) UNIX и вытекающие отсюда проблемы. Беда в том, что по моему только настоящие проектировщики встраиваемых систем понимают что эта сцепка не жесткая, и в принципе не обязательная. Но это не формат комментария... Это тема для статьи. А на нее как водится категорически не хватает времени. А если совсем честно, то и желания. Она все равно у подавляющего числа хабражителей не вызовет ничего, кроме изжоги. Это я не хочу топтать их огород. Они мой завсегда и с удовольствием. Потому доношу тем, с кем работаю. Когда дозревают до понимания.

                И ладно когда "молодые и горячие" что-то подобное мне предъявляют. Это нормально. Каждый приходящий ко мне в отдел пытается меня перекричать и сдать в утиль. Пока, правда безрезультатно. Но от человека, который "делал драйвера для первого смартфона с Линукс компании Самсунг Электроникс" подобного явно не ожидаешь.


                1. ignat99
                  01.12.2021 13:39

                  А вы на ламповую технику переключайтесь. Это сейчас тренд.
                  Как было трендом развитие Qt в 2004 году.

                  Сколько раз уже сказали что ламповая техника умерла?

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


                  1. MinimumLaw
                    01.12.2021 13:50
                    +1

                    Лампы... А знаете как легко и здорово объяснять цифровую схемотехнику с помощью обычных реле. Нормально замкнутых, нормально размкнутых, переключающих. Как классно звучит (во всех смыслах этого слова) "сдвиговый регистр на электромеханических реле"? И насколько после этого по другому воспринимается Шеноновская нетленка "Надежные схемы из ненадежных реле"?

                    А лампы... Нет, спасибо. Меня "теплый ламповый 100Гц гул" с детства достал... Не ощущаю я в нем той могучей аудиофильской силы. И тем более не интересно в плане цифры. Впрочем, многосеточные лампы это сила. Практически не имеющая себе аналогов. Но опять же - сие тема совсем другого разговора.


                    1. ignat99
                      01.12.2021 14:06
                      +1

                      Список команд Урал-1 — Википедия (wikipedia.org)

                      Ну почемуже совсем другого ... гул в 55 Гц и выше в 89 Гц и сейчас присутствует, но уже от планета Земля. Люди делятся на 8, 13, 21, 34 и выше герцовых.
                      А ещё есть аналоговые вычислители

                      И натурные экспериме́нты.

                      Ещё про "Сетунь" и троичное округление можно поговорить, а так же про магнитные тракты в магнитных катушках управляемых током.

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

                      Но флагман AD - AD9213 по прежнему стоит очень дорого. Дороже 1500 евро, так что большинству тут присутствующих эта техника не доступна. Как и в 1996 году первые AD.


                      1. MinimumLaw
                        01.12.2021 14:36
                        +1

                        Ну почему же совсем другого ... 

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


                  1. AnthonyMikh
                    01.12.2021 18:07

                    Даже тот же Excel из Microsoft Office 95 на сях писать было бы, мягко говоря, неудобно.


                  1. 0xd34df00d
                    01.12.2021 19:53
                    +1

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


                  1. WraithOW
                    02.12.2021 13:27
                    +1

                    А кроме Экселя продуктов в мире нет?

                    чем каждый день пользуются

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


  1. gdt
    29.11.2021 16:27
    +25

    Если убрать предсказание смерти C - получится неплохая статья про UB :)


    1. aversey
      29.11.2021 16:44

      Спасибо, хотя это скорее крик души, чем предсказание. =)


  1. AVI-crak
    29.11.2021 16:36
    -12

    За использование int в программах на Си - нужно руки/ноги отрывать. А если человек плохо обучается - то ещё и голову.


    1. netch80
      29.11.2021 17:12
      +7

      Что, по-вашему, тут улучшит явное предписание какого-нибудь int32_t?


      1. Kircore
        30.11.2021 09:00
        +4

        Везде улучшит. В зависимости от ЦП int может быть размером и 16 и 32 и 64 бита.


        1. netch80
          30.11.2021 09:08

          Давайте без фантазий о мифических странах. Во всех реальных платформах вокруг меня int == int32_t. Вот я заменил в программе int на int32_t. Каким образом это поможет исчезнуть факту незамеченного переполнения в конкретном месте программы?


          1. Kircore
            30.11.2021 23:56
            +2

            Привет от нереальных платформ avr, pic, stm8, rpi4.


            1. netch80
              01.12.2021 01:02
              -1

              > Привет от нереальных платформ avr, pic, stm8, rpi4.

              Ни разу не приходилось под такие писать после ~2000 года, потому и указал явно, что «вокруг меня». Но предположим. То есть вы решили на платформах с естественными 16 битами решить проблему расширением разрядности до уровня, поддержка которого уже в разы более громоздкая, потому что длинная арифметика?
              А не боитесь, что и этот размер переполнится, только чуть позже?
              И не учитываете, что по сравнению с другими проблемами платформы эта конкретная будет минимально заметной?


              1. Kircore
                01.12.2021 02:53
                -2

                Ни разу не приходилось под такие писать после ~2000 года, потому и указал явно, что «вокруг меня».

                Я уже понял, что ты солипсист.

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

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

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


    1. OvO
      29.11.2021 20:23
      +16

      Ради интереса глянул Ваш git - скажите, Вы уже оторвали свои руки/ноги? И к чему такая эмоциональность?


  1. warlock13
    29.11.2021 16:57
    -2

    Си должен умереть, но совсем не потому, что два неадеквата (Торвальдс и felix-gcc) брызгая слюнями порят чушь.


  1. Gordon01
    29.11.2021 16:59
    +19

    Разбудите меня через десять лет и спросите о чем говорят Сишники и я отвечу — про разыменование нулевого указателя и прочие UB.

    Скучно.

    На практике, конечно, давно придуманы всякие valgrind, санитары, clangd, pvs студии и прочие штуки, которыми надо обмазаться, чтобы написать на с/с++ что-то, что проработает больше дня без падения.

    Настоящая же проблема с/с++ — отсутствие средств сборки. Точнее их тысячи, но дай бог, если одно из них совместимо хотя бы с парочкой других. Над ними надстроены еще обычно проекто-специфичные самописные системы сборки, которые управляют ширпотребными системами сборки...

    То же самое с остальным тулингом вокруг этих языков. В каком-нибудь вскоде официальный плагин для c/c++ довольно ограниченный и заметно хуже того экспириенса, который есть в студии с решарпером. И чтобы этот опыт улучшить надо возиться. В нашей компании ребята попробовали приспособить clangd, но за несколько месяцев НИКТО из плюс-минус сотни разработчиков так и не попробовал. Никому это не интересно, все привыкли страдать и принимать отсутствие IntelliSense как должное. Да, можно пользоваться студией или редакторами от JB, но это в случае если ты пишешь только на с/с++. Мне, например, нравится что в вскоде, как в виме или емаксе можно работать с любым языком.

    Другая проблема в непортабельности исходников с/с++. Код на питоне, тайпскрипте или расте можно просто взять и запустить/собрать на любой из поддерживаемых платформ и он сразу заработает. Удачи просто собрать исходники с/с++ на новой платформе.

    Так что разыменование нулевого указателя это так, мелочи.

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

    За все время работы с сишниками у меня сложилось впечатление что им нравится этим заниматься. Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него. Поговорите с любым отбитым сишником (или почитайте их в твиттере) про раст и сразу все поймете.

    Ну не хотят люди жить хорошо, привыкли они так. И это не плохо. Просто так сложилось.


    1. netch80
      29.11.2021 17:15
      +6

      > Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него. Поговорите с любым отбитым сишником (или почитайте их в твиттере) про раст и сразу все поймете.

      Видите ли… сишники/плюсовики подобные средства и так регулярно используют. Проблема в том, что они не дают никакой гарантии — и анализаторы, и санитайзеры ловят, грубо говоря, 99% случаев, а 1% всё равно остаётся. И при мельчайшей смене чего-то в коде их надо напускать заново, потому что от того, что карты разложились чуть иначе, компилятор нашёл новый вариант схалявить.

      > Ну не хотят люди жить хорошо, привыкли они так.

      Нет «привычки». Есть понимание реальности. Вы же её не понимаете и рассказываете какие-то байки о том, что подсмотрели в замочную щёлку вместо собственного опыта.

      > Другая проблема в непортабельности исходников с/с++. Код на питоне, тайпскрипте или расте можно просто взять и запустить/собрать на любой из поддерживаемых платформ и он сразу заработает.

      Потому что они непосредственно стыкаются с железом, не? В отличие от прочих названных, где как раз C/C++ обеспечили им независимость и возможность поплёвывать свысока на мучения «холопов»?


      1. Gordon01
        29.11.2021 18:42
        -2

        Проблема в том, что они не дают никакой гарантии — и анализаторы, и санитайзеры ловят, грубо говоря, 99% случаев, а 1% всё равно остаётся. 

        Если человеку нужна 100% гарантия, то он выбрал совершенно не тот язык.

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

        Это более чем очевидно. Не думал, что такое нужно объяснять.

        Другое дело, что у пользователей современных языков большинство этих проверок выполняется после каждого нажатия на клавишу в редакторе. И начинают работать сразу из коробки после установки одним кликом бесплатного плагина из стора, никогда не разваливаются, не падают и не отжирают 32 ГБ памяти в попытках проиндексировать 100 тысяч строк кода.

        Нет «привычки». Есть понимание реальности. Вы же её не понимаете и рассказываете какие-то байки о том, что подсмотрели в замочную щёлку вместо собственного опыта.

        Так расскажите свое понимание, только без перехода на личности.

        Потому что они непосредственно стыкаются с железом, не? В отличие от прочих названных, где как раз C/C++ обеспечили им независимость и возможность поплёвывать свысока на мучения «холопов»?

        Давайте расскажите как раст не стыкуется с железом)))

        Типичная байка, будто на с/с++ только ОС и драйверы пишут.


        1. netch80
          29.11.2021 19:06
          +1

          > Если человеку нужна 100% гарантия, то он выбрал совершенно не тот язык.

          А вы думаете, чему посвящён исходный постинг? Мы же тут обсуждаем, что можно было бы сделать, чтобы получить гарантии.

          > Это более чем очевидно. Не думал, что такое нужно объяснять.

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

          А в расте вам тоже «очевидно», что изменение кода одного модуля может взорвать код другого, безо всякой видимой связи между ними? Тогда зачем тот раст был бы нужен? (заметьте, я не утверждаю)

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

          Потому что у них нет таких перекрёстных эффектов? Ну так, в третий раз повторяю, за то и боремся.

          > Так расскажите свое понимание, только без перехода на личности.

          Вы первый задали такой тон. И это не на личности, а на конкретные специфики, безусловно ввязанные в данную дискуссию. Вы сами поставили своими словами себя отдельно от мира C и высказываете при этом то, что ему не соответствует аж никак.

          А по сути — уже рассказано, не вижу смысла повторяться: есть анализаторы, есть санитайзеры, есть здравый смысл авторов, и в сумме они… ну почти всё таки покрывают. И вот этот вот недостающий кусочек от «почти» до полного — предмет, в 4-й раз, обсуждения.

          > Давайте расскажите как раст не стыкуется с железом)))

          Сколько лет C и сколько расту? Сколько кода на C накопилось? И зачем вы мне стали тут приписывать какие-то утверждения, на которые я даже не намекал?

          > Типичная байка, будто на с/с++ только ОС и драйверы пишут.

          Нет, конечно. А зачем вы это говорите? Говорите тому, кто придумал эту байку (это не я, а упомянули тут её зачем-то вы — сами и придумали?)


      1. PsyHaSTe
        01.12.2021 03:52

        На каких холопов поплевывает раст? Гц языки-то ещё ладно, но тут вполне bare metal можно писать.


        1. netch80
          01.12.2021 10:48
          +1

          > На каких холопов поплевывает раст?

          Вы про последний абзац? Там про Rust как-то очень вскользь и сомнительно. Утверждать, что на Rust может может сразу скомпилироваться и запуститься то, что на C не завелось, как-то очень странно (вопрос не в отсутствии багов или тому подобном, а именно в переносимости). Это я просто пропустил, извините, но там и так был очень объёмный комментарий. Хотелось бы обоснований и примеров, как такое может получиться, причём не в исключительном случае.

          (Наперёд — случаи размеров типов не считаем. И в C есть int${N}_t давно, и случаи, где применяются всякие int, от его размера зависят в редчайших ситуациях.)


          1. cepera_ang
            01.12.2021 10:51
            +2

            [no_std] и пиши весь рантайм ручками :)


          1. PsyHaSTe
            01.12.2021 13:54

            Вы про последний абзац? Там про Rust как-то очень вскользь и сомнительно. Утверждать, что на Rust может может сразу скомпилироваться и запуститься то, что на C не завелось, как-то очень странно (вопрос не в отсутствии багов или тому подобном, а именно в переносимости).

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


            Я сказал лишь то что раст может запускаться на голом железе и не требует никаких холопов для работы. Единственное к чему можно (и иногда это делают) прикопаться это к "вот вы присосались к сишному llvm, ничего сами сделать не можете!!1". Ну тут уже людям объяснять нечего, конечно.


            1. netch80
              01.12.2021 14:16
              +1

              > Я не понимаю откуда тут в комментах люди всегда додумывают так сильно.

              Додумывать — это нормально, мы все всегда так делаем по определению (подразумеваемый контекст — например, что я вам лучше тут отвечу по-русски, а не по-арабски;)) Но додумывать с представлением своей додумки как утверждения, как делают тут некоторые собеседники (не вы) — действительно плохо. Я же спрашиваю, угадал ли с додумкой — чтобы лучше понять собеседника, потому что плохо знаю эту тематику.

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

              Спасибо, сложил в закладки. Там интересно и что явно сказано, и что не сказано. Например, я в reset_handler не увидел инициализации стека. Вероятно, это обеспечено линкером. Главное, что несколько импортированных символов не потянули за собой половину стандартной библиотеки, как часто бывает в сишных реализациях, не рассчитанных на embedded. Значит, уже есть «achievement unlocked».

              > Единственное к чему можно (и иногда это делают) прикопаться это к «вот вы присосались к сишному llvm, ничего сами сделать не можете!!1». Ну тут уже людям объяснять нечего, конечно.

              +100. LLVM не сишный, он универсальный, на чём бы ни был написан.


    1. 0xd34df00d
      29.11.2021 19:46
      +9

      За все время работы с сишниками у меня сложилось впечатление что им нравится этим заниматься. Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него. Поговорите с любым отбитым сишником (или почитайте их в твиттере) про раст и сразу все поймете.

      Да потому, что никакие pvs/санитайзеры/валгринды никогда не дадут тех же гарантий, которые даёт хотя бы раст, я уж не говорю о более хардкорной наркомании.


      Другая проблема в непортабельности исходников с/с++. Код на питоне, тайпскрипте или расте можно просто взять и запустить/собрать на любой из поддерживаемых платформ и он сразу заработает. Удачи просто собрать исходники с/с++ на новой платформе.

      В моей практике (особенно за деньги) на это тратилось очень малое количество времени. Возможно, специфика работы, когда ты сидишь и фигачишь один проект условный год, а не делаешь create-react-app или stack new раз в неделю.


      1. vkni
        29.11.2021 21:58

        Да потому, что никакие pvs/санитайзеры/валгринды никогда не дадут тех же гарантий, которые даёт хотя бы раст, я уж не говорю о более хардкорной наркомании.

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

        А так - да, когда у нас более "расслабленный" язык, на нём часто легально пишут с провоцированием ошибок. Конкретный пример - функция filter-map в стандартной библиотеке Racket. Она принимает в качестве первого параметра функцию, которая должна выдавать значения разных типов: #f (false : bool), если элемент нужно выбросить, и какой-то 'a, если его нужно отобразить и оставить. Очень удобная эта filter-map, но из-за неё уже нельзя вот так просто брать и требовать у каждой функции тип возвращаемого значения.

        И отделять тут козлищ (тех, кто по-ошибке возвращает значения разных типов) от агнцев можно лишь "интуитивно". Это значительно опаснее того же C.


        1. 0xd34df00d
          29.11.2021 22:57
          +2

          Она принимает в качестве первого параметра функцию, которая должна выдавать значения разных типов: #f (false: bool), если элемент нужно выбросить, и какой-то 'a, если его нужно отобразить и оставить. Очень удобная эта filter-map, но из-за неё уже нельзя вот так просто брать и требовать у каждой функции тип возвращаемого значения.

          Эм, а чем это отличается от вполне себе существующего и типизируемого хаскелевского mapMaybe :: (a -> Maybe b) -> [a] -> [b] с очевидной семантикой?


          1. vkni
            29.11.2021 23:17

            Тем, что у вас там внутри скобочек есть тип после стрелочки, а у filter-map - принципиально нету. И ваша функция явно указывает Just/Nothing, а в Racket мы не пишем эти два конструктора.

            То есть, выразительные средства языка у Racket такие, что некоторые вещи выглядят двусмысленно по сравнению с Haskell. И ничего тут стат. анализатор Racket не сделает.


            1. 0xd34df00d
              29.11.2021 23:41
              +5

              Сложно-то как без статической типизации.


              1. vkni
                30.11.2021 00:45
                +2

                Некоторые из моих знакомых считают, что статическая типизация убивает креативность. :-) :-) :-)


              1. WraithOW
                01.12.2021 13:44
                +6

                Проблем у Си нет, есть проблема ухудшающегося качества программистов и проблема переоптимизации у компиляторов.
                Э… ну написал с полгода назад небольшой демон, который читает конфиг, слушает сокет и в потоках раскладывает данные по файликам в зависимости от того, что в конфиге написано.

                Небольшой демон можно на чем угодно написать, от lua до ассемблера (на хабре точно была пара статей про вебсервера на asm). Если вы позиционируете его как язык для небольших поделок — это одна история, если как язык для серьезных проектов на много лет и много программистов — совершенно другая.


          1. Sdima1357
            30.11.2021 19:33
            -1

            1024 Пива?


      1. mayorovp
        29.11.2021 22:14
        +1

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


    1. vkni
      29.11.2021 21:55

      Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него.

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


    1. slovak
      29.11.2021 23:18
      +4

      Да, можно пользоваться студией или редакторами от JB, но это в случае если ты пишешь только на с/с++. Мне, например, нравится что в вскоде, как в виме или емаксе можно работать с любым языком.

      Пишу в Clion от JB. Есть поддержка всего что только нужно. Обычно с C/C++ в проекте есть Python, Bash, Lua, CUDA, Rust, yaml/json/toml/xml/, всякие шейдеры и прочая дичь.

      Вероятно Вы мимо JB лишь мимо проходили Ж)

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

      Да, конечно, особенно если учесть что под капотом каждой первой либы на питоне все тот же C/C++. Удачи с запуском scipy на любой платформе. Там еще и фортран подтянуть прийдется. Или может LAPACK уже на чистом питоне переписан?


      1. 0xd34df00d
        29.11.2021 23:43
        +2

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


      1. cepera_ang
        30.11.2021 07:20
        +6

        А вы положите на эту машину кошелек с битком на миллион долларов и выставите ваш сокет в открытый интернет и сюда ссылку приведите.


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


    1. Kircore
      30.11.2021 09:04
      -1

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

      Собрать код на питоне или тайпскрипте? Раст у тебя везде заработает?


      1. DirectoriX
        30.11.2021 15:41
        +3

        на любой из поддерживаемых платформ
        Раст у тебя везде заработает?
        Как ни странно — да. Конечно, у конкретный крейтов могут зависимости быть от библиотек, которые отсутствуют на какой-то платформе, но это проблема крейтов, а не языка/компилятора; точно так же как если я буду использовать /dev/random в качестве источника случайных значений, то на Windows ну никак не будет работать.


        1. Kircore
          30.11.2021 23:37
          +1

          на любой из поддерживаемых платформ

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

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

          Так и код на Си везде заработает, если доступны нужные библиотеки и API.


          1. DirectoriX
            01.12.2021 00:12

            Значит если интерпретатора питона нет на платформе или твой код использует сишную библиотеку, распространяемую в прекомпилированных бинарниках, то не заработает?
            Именно так. Как и со всеми другими скриптовыми языками: нет интерпретатора — нет интерпретации.
            Так и код на Си везде заработает, если доступны нужные библиотеки и API.
            Так и на C код не заработает, если нужных библиотек/API нет, в чём тут недостаток Rust? Пишете код на «чистом» C/Rust — перенесётся без проблем, пишете с библиотеками — уже от них будет зависеть ваша переносимость.
            При этом зависимость не только от сторонних библиотек, но и от std. Банальный пример: на всяких микроконтроллерах нет stdio.h, зато есть прямой доступ к регистрам процессора (без ассембреных вставок, просто по адресам), но вы же не скажете, что C — непереносимый язык?


            1. Kircore
              01.12.2021 00:26

              Так и на C код не заработает, если нужных библиотек/API нет, в чём тут недостаток Rust?

              Изначальная претензия была к Си.

              Банальный пример: на всяких микроконтроллерах нет stdio.h

              Вообще-то есть. И переопределением, например, putc() можно использовать printf для вывода в последовательный порт или любой другой интерфейс.

              зато есть прямой доступ к регистрам процессора

              Указатели на абсолютные адреса использовать можно ("регистры" периферии), именованные регистры общего назначения без ассемблера - нельзя.


  1. F0iL
    29.11.2021 17:22
    +4

    Избежать неопределённого поведения можно также с помощью объединений (union)

    А потом кто-нибудь этот код заинклудид или скопирует с C++-проект и тоже получит undefined behavior.

    Потому что в Си использование union для type puning явно разрешено стандартом

    6.5.2.3 Structure and union members

    95) If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

    а вот в C++ уже нет

    9.5 Unions [class.union]

    In a union, at most one of the non-static data members can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time.

    ...потому что там это нарушает active member rule :(


    1. AxXxB
      29.11.2021 20:47

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


      1. F0iL
        29.11.2021 22:28
        +1

        Так в том-то и дело, что никаких указателей явно тут не используется, обычный union value type :)


  1. johnfound
    29.11.2021 17:37
    +6

    Нет, вообще-то я согласен. Потому что ассемблер намного лучше чем C.


    1. F0iL
      29.11.2021 17:56
      +3

      Настоящие мужики пишут сразу на машинных кодах, не?


      1. johnfound
        29.11.2021 19:19
        +7

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


    1. arm039
      29.11.2021 20:48

      А Vax assembler ещё лучше))


      1. johnfound
        29.11.2021 21:54
        +5

        Нельзя так говорить! Каждый ассемблер еще лучше. ????


        1. le2
          01.12.2021 01:16
          +2

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

          Короче, сила ассемблера в том что никто не нужен для понимания. Интернет тоже не нужен, потому что все однозначно. Холивара тоже нет. Нужна только документация.

          Конечно, ассемблер это зло, но это то к чему нужно стремиться при разработке инструментов — инструмент не изменяется в руке. Всё однозначно понятно. Порог входа (для технарей) тоже очень низкий. То есть у новичка код будет плохой, но он будет работать.

          На ассемблерах я писал годами и когда перешел на Си я в голове постоянно прокручивал как эти конструкции выглядят на ассемблере и что происходит в процессоре. И должен признать что испытывал все те же новичковые сложности с синтаксисом ссылок-указателей. Осознал всю вселенную неоднозначностей что скрывает компилятор и стандарт. Хотя казалось бы — почему? Ведь я реально знал контроллеры наизусть до последнего бита-флага, систему команд ассемблеров тоже (8051, AVR).

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


          1. mayorovp
            01.12.2021 13:32

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


            Я долгое время сидел на ruSO и отвечал на глупые вопросы начинающих, и могу сказать что там почти нет вопросов относящихся именно к пониманию языка. Большинство проблем растёт либо из непонимания алгоритмов, либо вовсе из неспособности провести декомпозицию задачи. Ещё есть проблемы непонимания некоторых библиотек, но это снова проблемы библиотек, а не языков.


          1. johnfound
            01.12.2021 15:52
            -2

            Если отбросить троллинг,

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


            Конечно, ассемблер это зло,

            Это не факт и совершенно не следует из вашего поста.


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

            Я перестал писать на ЯВУ когда осознал что все время приходится бороться с компилятором, чтобы он выдал тот код, который мне нужен. Зачем, если можно сразу написать то что нужно?


            1. DirectoriX
              01.12.2021 16:35

              Если вы разрабатываете драйвер для железки или пишете прошивку микроконтроллера — да, вероятно ассемблер будет лучшим выбором, по крайней мере в некоторых блоках кода.
              Но у языков более высокого уровня (С и далее) есть два ключевых преимущества: многое сделано и протестировано за/до вас, а также многие операции, которые делают одно и то же, имеют один и тот же вид.
              Пример: вот три цикла.

              for (char i = 0; i < 10; i++)
                  do_something_char(i);
              
              for (unsigned long long i = 0; i < 1000000000000000; i++)
                  do_something_ull(i);
              
              for (float i = 0.0; i < 10; i += 0.01)
                  do_something_float(i);

              Синтаксис у них идентичный, назначение — тоже: вызвать функцию много раз, передав ей текущее значение цикла, а всё отличие — в типе данных. Эти сишные циклы выглядят одинаково и на x86_64, и на ARM64, и даже на AVR. Внимание, вопрос: зачем писать одно и то же 9ю (3 цикла на 3 архитектурах) разными наборами инструкций на ассемблере, когда есть ровно одна работающая конструкция на C?
              Повторюсь: я не говорю про низкоуровыневые коды, я про общий случай (судя по вашим предыдущим комментариям, вы «топите» за ассемблер для всего).


              1. johnfound
                01.12.2021 17:13
                -1

                Внимание, вопрос: зачем писать одно и то же 9ю (3 цикла на 3 архитектурах) разными наборами инструкций на ассемблере, когда есть ровно одна работающая конструкция на C?

                По той же причине, из за которой я пишу здесь на русском, а не на эсперанто. Потому что общение на родном языке собеседника (даже если и корявенько ) обеспечивает лучшее взаимопонимание между сторонами. В частности, между машиной и программистом. ;)


                1. DirectoriX
                  01.12.2021 20:07

                  Если вы пишете код только под одну платформу, и точно знаете, что этот код никогда не будет переноситься на другую — да, конечно, ассемблер имеет намного больше смысла…

                  • … особенно если нужна критическая производительность
                  • … особенно если ваш код никто кроме вас и других товарищей по ассемблеру не читает (ассемблер сильно менее понятен, чем C, а ведь есть и гораздо более выразительные языки)
                  • … особенно если вы на 100% уверены, что напишете код не менее производительный и/или компактный, чем современные оптимизирующие компиляторы (а это ой как не факт)

                  Нет, я не призываю вас отказываться от ассемблера для решения ваших задач, но заявление «ассемблер лучше С» (без специфики, без конкретных задач) слишком смелое.


                  1. johnfound
                    01.12.2021 22:52

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

                    Это совершенно не так. Просто ассемблер мало знают и мало пишут на нем. Для меня например Лисп совершенно не понятен. Но это не значит что Лисп менее понятен в принципе. Он только мне непонятен.


                    1. forthuser
                      01.12.2021 23:37
                      +2

                      Ассемблеров от производителей, например, разных контроллеров много (AVR, MSP430, ARM, PIC, RISC-V, 8051, STM8, Z80, M6800, DSP...) с разной системой команд и способами в целом их использования.
                      Как возможно безболезненно перенести код программы с одного контроллера в другой используя ассемблер?
                      (при том ещё и аппаратная составляющая у разных контроллеров предполагает определённые шаги по работе с ней)


                      1. johnfound
                        01.12.2021 23:48
                        -1

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

                        А кто сказал что должно быть "безболезненно"? Совершенство постигается через боль. Как говорят, через тернии к звездам!


                        Безболезненно они хотят!


                        П.С. А еще говорят – глаза боятся, а руки делают. Часто этот процесс намного легче, чем кажется.


        1. forthuser
          01.12.2021 09:48
          +1

          Один из лучших «ассемблеров» — это Forth (Форт). ????
          У него, даже, и встроенный ассемблер может быть на свой лад.


          1. netch80
            01.12.2021 11:02
            +2

            Для современного железа затраты на call/ret чудовищны, так что для «гражданки» Forth уже не выйдет за нишу клея для загрузчиков и тому подобного (если без перерабатывающего супероптимизирующего компилятора, после которого дух языка, считаем, подменён чем-то другим).

            А вот для военного или космического применения Forth вполне вкусен — гарантированный не исправленный никакими подозрительными оптимизациями выхлоп, в сочетании с аналогичными мерами на уровне процессора (например, никаких кэшей памяти), и средствами защиты от сбоев, наверняка будет обязателен, чтобы безопасно лететь хотя бы к Марсу…


            1. forthuser
              01.12.2021 12:02
              +1

              Чужие: странная архитектура инопланетных компьютеров (c RTX2010).

              А, вот для «гражданки», вообще нет контроллеров с MISC построением,
              если не считать экзотичный GA144 на который, кстати, делали
              и проект «Си» компилятора — Chlorophyll Language and Compiler. ????

              Статья из журнала «Компоненты и Технологии»:
              Процессоры GreenArrays — GA144
              Хабр статья: GA144: русские спецификации процессоров

              P.S. Хотя в кремнии и в России есть сделанные «MISC» кристаллы К1894,
              В Минском Интеграле — K1881BE1T (IN16C)


          1. ignat99
            01.12.2021 12:28
            +2

            Согласен с вами. А хорошо бы если вы статью написали по теме Fort на хабре. И можно отдельно про сшивку кода. А если бы коснулись темы малой площади занимаемомой форт-процессором на ПЛИС, то было бы совсем идеально.

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


  1. KReal
    29.11.2021 17:48
    +20

    А вот в Китае такой пост бы зацензурили!
    *простите, не сдержался :)


    1. Vilgelm
      30.11.2021 02:38
      +2

      Думал шутки про Китай будут прямо в первых комментариях.


  1. ionicman
    29.11.2021 17:49
    +2

    int main()
    {
        int x;
        int y;
        int *p = &x + 1;
        int *q = &y;
        printf("%p %p %d\n", (void *) p, (void *) q, p == q);
        return 0;
    }

    А можно объяснить, почему это вызывает вопросы?

    x где-то лежит в памяти
    y где-то так-же лежит
    указатель p = адрес памяти x + размерность x
    указатель q = адрес памяти y
    почему p должен быть равным y?

    С чего указатели, даже содержащие одни и теже данные, должны указывать на одно и тоже место? В общем случае они всегда не будут равны. Хочешь чтобы были равны — породи их от одного адреса.

    Для меня указатель — это просто адрес и размерность — и ничего больше, почему из сравнивать нельзя? Даже если этого адреса не существует? Видимо, я давно стандарты не читал )))


    1. mayorovp
      29.11.2021 19:03
      +1

      Вопросы вызывает тот факт, что при некоторых ключах некоторого компилятора printf выводит сначала два одинаковых числа, а потом признак их неравенства.


      1. ionicman
        29.11.2021 19:11

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


      1. Gordon01
        29.11.2021 19:43
        +1

        Просто вы не понимаете что такое указатель.

        Указатель в Си — это не адрес в памяти, это абстракция. Тот указатель, который передается инструкции x86 mov не имеет ничего общего с указателем в языке Си, хотя и компилируется в нее.

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

        Если вы хотите сравнивать, вычитать, складывать разные указатели как числа — пишите код на ассемблере. В Си это невозможно и некорректно.

        Программисты на С/С++ ошибочно полагают, что эти языки — низкоуровневые, хотя они ими не являются. https://queue.acm.org/detail.cfm?id=3212479

        This is a fairly trivial example, yet a significant proportion of programmers either believe the wrong thing or are not sure. When you introduce pointers, the semantics of C become a lot more confusing. The BCPL model was fairly simple: values are words. Each word is either some data or the address of some data. Memory is a flat array of storage cells indexed by address.

        The C model, in contrast, was intended to allow implementation on a variety of targets, including segmented architectures (where a pointer might be a segment ID and an offset) and even garbage-collected virtual machines. The C specification is careful to restrict valid operations on pointers to avoid problems for such systems. The response to Defect Report 2601 included the notion of pointer provenance in the definition of pointer:

        "Implementations are permitted to track the origins of a bit pattern and treat those representing an indeterminate value as distinct from those representing a determined value. They may also treat pointers based on different origins as distinct even though they are bitwise identical."


        1. Travisw
          29.11.2021 20:22
          -1

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


          1. 0xd34df00d
            29.11.2021 20:40
            +4

            А где в этом числе можно найти pointer provenance?


          1. insecto
            29.11.2021 22:20
            +5

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


        1. mayorovp
          29.11.2021 22:24
          +1

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


  1. rg_software
    29.11.2021 18:17
    +16

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

    Аналогично, человек, кладущий в память два int-значения, и вытаскивающий обратно float, зачем-то это делает. Вероятно, у него есть на то хорошая причина, но нельзя же не понимать, что ты суёшься ну в очень уж тёмный угол, в котором законы зыбки. В этом отношении юзер felix-gcc из диалога ни разу не прав: у него есть легальный способ получить нужное ему поведение, но он из принципа делать этого не хочет. Он хочет, чтобы его Ариан упал, а виноваты были разработчики компилятора.

    Язык C страдает от массы болезней, порождённых особенностями его развития. Некоторые вещи нельзя получить легальными способами, и на то должны быть официальные compiler-specific пути, за которые отвечают разработчики компиляторов. Некоторые беды, по всей видимости, неискоренимы, но опять-таки, люди, выбирающие Си из массы вариантов, чем-то же руководствуются. Ну а так, да, пусть будет "более лучший язык". Например, Ада специально спроектирована, чтобы подобные ошибки предотвращать. На ней, кстати, код "Ариана" написан.


    1. Gordon01
      29.11.2021 21:16
      +10

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

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

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


    1. nin-jin
      30.11.2021 10:04
      +2

      felix-gcc говорит, что недопустимо намеренно и неявно создавать уязвимости в уже написанном и активно использующемся коде. Независимо от того, что там постфактум написано в спецификации. Дело совсем не в том, как именно проверять переполнение. Совсем не в этом. Это больше этический вопрос, чем технический.


      1. rg_software
        30.11.2021 11:52
        +4

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

        Felix-gcc прикопался к конкретному виду UB, но на практике ведь UB везде, начиная с хрестоматийных f(++x, ++x), и я не удивлюсь, если даже сами авторы компилятора не знают, как именно выполняется f(++x, ++x) на текущей версии, ибо зачем им это знать? То есть в мире felix-gcc мы должны сначала прогнать миллион тестов на миллионе видов UB, потом закрепить это всё в тысячестраничном документе и следить, чтобы не дай бог новая версия компилятора ничего не сломала.


        1. nin-jin
          30.11.2021 12:24
          +1

          Да, представьте себе, стандартов приходится придерживаться. Будь они де-юре или де-факто. А даже если хочешь сломать обратную совместимость - делать это надо аккуратно. Либо пройдясь по всем проектам и поправив их самостоятельно, либо выпустив автомиграцию, либо кидая ошибку на потенциально опасном коде, либо предоставив флаг, который по умолчанию выключен. Но уж точно не открывать дополнительные дыры там, где их не было.


          1. rg_software
            30.11.2021 13:23

            Так проблема в том, что непонятно, с чем совмещать. Юзер пишет, и говорит, что в ревизии x.5 работало так, а в x.6 работает эдак. Мы чешем репу и выясняем, что действительно так, хотя мы даже об этом не подозревали. Окей, поправили. А потом находится другой юзер, который сообщает, что в ревизии x.4 было вообще иначе, и что теперь делать?

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


        1. netch80
          30.11.2021 12:40
          +1

          > Felix-gcc прикопался к конкретному виду UB, но на практике ведь UB везде, начиная с хрестоматийных f(++x, ++x), и я не удивлюсь, если даже сами авторы компилятора не знают, как именно выполняется f(++x, ++x) на текущей версии, ибо зачем им это знать?

          Не так:

          1. Это не undefined, это unspecified behavior. Разница принципиальная в том, что если UdB позволяет формально вообще всю программу выкинуть и заменить на показ котика, то UsB даёт определённое поведение везде, кроме проблемного места.

          2. Конечно, можно получить непредсказуемый результат в таком выражении, если происходит, например, раскрытие макры, которое не очевидно при чтении кода, но это по крайней мере эффект легко отлавливаемый (какое-нибудь gcc -E и посмотреть выхлоп).

          > То есть в мире felix-gcc мы должны сначала прогнать миллион тестов на миллионе видов UB, потом закрепить это всё в тысячестраничном документе и следить, чтобы не дай бог новая версия компилятора ничего не сломала.

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


          1. rg_software
            30.11.2021 13:31

            Это не undefined, это unspecified behavior. 

            Да, согласен. Но я думаю что в мире felix-gcc разницы нет: если я запустил свой код, и он выдал на выходе, скажем, нуль, то теперь нуль должен быть всегда, при любых обновлениях компилятора, и неважно, UB там или UC.


            1. gecube
              02.12.2021 02:07
              +2

              Какой-то прямо закон Хирама в действии!


  1. GBR-613
    29.11.2021 18:26
    +5

    Почему нельзя вернуться к Паскалю, доработав его?
    1. Исправить очевидные глупости, на которые жаловался Керниган в своей знаменитой статье (Собственно, может быть их уже и выкинули в новых версиях Free Pascal и Delphi/Lazarus, я давно ими не занимался.)
    2. Вставить несколько необходимых вещей типа long jumps, которых, собственно, должно быть не так уж и много
    3. Чтобы была как минимум опция начинать индексы массивов с 0, как все люди делают.
    И вперед, можно будет писать новую операционку.
    После Python и даже Java понимаешь, что С это мазохизм. Я понимаю, что раньше время было такое (послевоенная разруха, пережитки сталинизма :-)), но в 21 веке пора жить по принципу "explicit is better than implicit".


    1. MiIs
      29.11.2021 19:24
      +1

      Выше уже написали, что уже есть паскалеподобный язык ADA, который был создан на 10 лет позже С на основе задания министерства обороны США для безопасной разработки, а также созданный на его основе язык Spark программы на котором если скомпилировались, то с большой долей вероятности они не буду содержать ошибок вообще.

      Минусы этих языков - многословны (как и все паскалеподобные), серьезные инструменты и поддержка - платны, немного медленоватее С/C++ - на тестах , в связи с внутренними проверками ошибок, чуть быстрее java, но в отличие от java не жрет столько памяти и не имеет задержек сборки мусора.

      Языки применяются в критических к ошибкам областях: министерстве обороны США, авиастроении, астронавтике, разработке медицинских приборов, электронике.


      1. GBR-613
        29.11.2021 20:57

        1. Многословны (как и все паскалеподобные) - потому что "explicit is better than implicit". Но я согласен, что в Аде с многословностью увлеклись и переборщили. Чем плох Free Pascal? Если не считать отсутствия long jumps и т.д.?

        2. Серьезные инструменты и поддержка платны - потому что ими мало пользуются, в том числе в мире OpenSource. Больше бы использовались - было бы больше и бесплатных. Опять таки - чем несерьезен Free Pascal?

        3. Немного медленоватее С/C++ - вот именно немного ! Когда-то ядро Windows писали на ANSI C, потому что C++ немного медленоватее. Но ведь уже давно не пишут!


    1. Praksitel
      30.11.2021 18:50

      Доработанный Паскаль - это Java.


      1. GBR-613
        30.11.2021 19:10

        Есть в этом нечто, но Java как таковая для ядра ОС непригодна по ряду причин. Например, классы уместны не всегда и не везде. Такая вещь как StringBuffer, не может не быть медленной по сути своей. И все, где вместо явного освобождения памяти - сборка мусора, не может не быть медленным. Есть еще причины. А если это в Java изменить, то получится обратно Pascal.


    1. quwy
      30.11.2021 22:08

      Исправить очевидные глупости, на которые жаловался Керниган в своей знаменитой статье

      Та статья -- банальный троллинг противников по холивару (весьма горячему в те времена). Есть похожая и про C.

      Вставить несколько необходимых вещей типа long jumps, которых, собственно, должно быть не так уж и много

      А можете как-то обосновать необходимость сего адского извращения? Проблем от goto слишком мало, хочется еще больше? Оно без проблем реализуемо средствами встроенного ассемблера, но ЗАЧЕМ?

      Чтобы была как минимум опция начинать индексы массивов с 0, как все люди делают

      В паскале любой массив можно начинать с любого числа, никто не запрещает начинать с 0, если хочется.


  1. KanuTaH
    29.11.2021 19:06
    +9

    В статье много лишних эмоций, а по технической части она, честно говоря, довольно банальная. Подобное писали и 10 лет назад, наверняка писали и 20 лет назад (просто лень искать), и, что самое забавное, продолжат писать и через 10 лет, да, возможно, и через 20 лет тоже. Я такие называю "за все хорошее и против всего плохого" или "статья ради срача".


    1. netch80
      29.11.2021 19:21

      > Подобное писали и 10 лет назад, наверняка писали и 20 лет назад (просто лень искать)

      Вот как раз и хочется, чтобы не нужно было дальше такие статьи писать, а вместо этого можно было писать хотя бы «вставьте вот эти 3 строки в начало каждого исходника, и ваши зубы всегда будут белые и пушистые». А ещё лет через 5 — «перестаньте писать эти 3 строки, уже не нужно».


  1. Sdima1357
    29.11.2021 19:19
    +6

    Смерть "C" неизбежна как крах капитализма! Победа коммунизма неизбежна! Даешь Раст на всех платформах включая 8bit AVR! . Rust on Bare metal !!!. Старичков (Торвальд и компания) на пенсию. Офигеваю я от студентов...


    1. aversey
      29.11.2021 19:30

      Мы вроде такого не писали. =)


      1. Sdima1357
        29.11.2021 19:54
        +5

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


        1. netch80
          29.11.2021 19:56
          +3

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


          1. Sdima1357
            29.11.2021 20:09
            +2

            Это потому что сначала компилятор Си писали его пользователи, а потом абстракционисты. Но язык в этом не виноват

            „Знаешь загадку про верблюда? Что такое верблюд? Это лошадь, проект которой составлял творческий коллектив.“ — Том Уэйтс


            1. ignat99
              01.12.2021 13:51
              -2

              Всё точно. Обдумываю переход на ламповую технику и жалею что в МИЭТ не пошел на Антенно-фидерные устройства, а пошёл на ПКИМС, поддавшись стадному инстинкту.

              По крайней мере на ламповой технике и общий плюсовой провод и набор команд сокращённый

              Список команд Урал-1 — Википедия (wikipedia.org)


        1. PsyHaSTe
          01.12.2021 03:56

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


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


          1. Sdima1357
            01.12.2021 09:15
            +1

            Молодежь нормальная, такой она и должна быть. Тема просто избитая и исхоженная как "смысл жизни" .


          1. forthuser
            01.12.2021 09:55

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


          1. ignat99
            01.12.2021 13:53

            Хорошая техника стоит дорого и по сей день. Сейчас один хороший АЦП\ЦАП стоит дороже 1500 евро. Вот такие устройства в комбинации с ламповой техникой и есть наша альтернатива.


  1. AlexTheLost
    29.11.2021 20:14
    +1

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

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


    1. insecto
      29.11.2021 22:25
      +3

      Было бы действительно неплохо иметь язык который закрывает некоторые проблемы в Си и дает разумное кол-во синт. сахара

      Неплохо для чего? Зачем такой язык может пригодиться? Переписать на нём ядро Линукс со всеми драйверами? Или coreutils? Или user32.dll на нём перепишут? Кто им будет пользоваться и зачем? Работники каких областей создадут вокруг него экосистему?


  1. ruomserg
    29.11.2021 20:18
    +5

    Что-то мне кажется, что все беды тут от близкого родства с C++. В старые времена, когда деревья были большими, компилятор, встретив кусок кода типа a << sizeof(int)*8 должен был сгенерировать соответствующее число команд сдвига влево, принятых в данной архитектуре. Предполагалось, что программист который это написал — знает что он делает, и зачем.

    Однако, когда завезли C++ сначала как «C with classes» по Страуструпу, добавили темплейты, а потом ударились в template meta-programming — оказалось что эта хрень генерирует просто тонны dead code! И шансов на выживание у языка в таком виде нет, несмотря на закон Мура (который в те времена все еще действовал). И вот именно тогда разработчики компиляторов (не будем показывать пальцем на gcc/g++ как на наиболее известную свободную реализацию...) ударились в dead code elimitation — и пошло-поехало… А поскольку поддерживать две совсем разных ветки для C и для C++ дорого — все эти новые веяния пришли и в C. Что дало компилятору совершенно неприличные возможности для интерпретации канонического кода на языке высокого уровня.

    С моей точки зрения — в идеальном мире с пони и единорогами, в случае UB компилятор обязан сгенерировать команды, которые максимально точно передадут написанное в исходнике в коды target-платформы. Даже если на некоторых (или даже на всех) платформах из этого получится идиотизм. И да, проблемы этого подхода мне понятны — но я как минимум разрешил бы любые варианты dead code elimination ТОЛЬКО по указанию соответствующих ключей при вызове компилятора. А не наоборот. Хотя и reordering в сочетании с мультипроцессорными архитектурами, out-of-order execution и прочими штуками современных вычислительных систем может давать неожиданные эффекты.

    А пока программисту на C/C++ следует просто знать ассемблер. И если не в первую, так во вторую очередь смотреть, что компилятор вам сгенерировал… Ну или вперед на Java. Там, благо среда исполнения тоже более-менее отвязана от аппаратуры, и оно если скомпилировалось, то так же и работать будет.


    1. GBR-613
      30.11.2021 19:21
      +1

      Я до такой степени согласен, что я бы оптимизации компилятора в С вообще запретил. В крйнем случае, разрешил бы печатать warnings. Если ты ожидаешь от компилятора оптимизации, значит тебе нужен не С, а Java или С#.
      Что же касается С++, то вот его я бы убил точно. Ну кому он сейчас нужен? Как замена ассемблера он не годится, а как "С с классами" есть Java и С#, которые и лучше, и проще.


      1. Sdima1357
        30.11.2021 19:29
        +1

        я бы оптимизации компилятора в С вообще запретил

        Как хорошо что это не от Вас зависит. Вызывайте с -O0.

        Что же касается С++, то вот его я бы убил точно

        C++ закуклился и спрятал плюсы под совместимость с "C".


  1. Mingun
    29.11.2021 20:24

    Ответ кроется в тексте стандарта. Чтобы всё-таки дать теоретическую возможность программистам писать низкоуровневые процедуры, а значит непереносимые, было введено ещё одно понятие — неопределённое поведение (undefined behavior, раздел 1.6, "DEFINITIONS OF TERMS"):

    ...


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

    Божественно


    1. wander
      30.11.2021 10:17

      Просто первая цитата во-первых не может использоваться без контекста, потому что говорит о том, "что" было в начале пути. Во-вторых она несколько смещает поле зрения, что приводит к искаженному восприятию - создается впечатление, будто бы UB специально придумали, чтобы им пользоваться в коде. Однако это совсем не так.


  1. SShtole
    29.11.2021 20:51
    -5

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

    Э-э-э… А что это за компилятор такой?

    Мне кажется, это не оптимизация, а чистой воды вредительство. Что значит: «Использовать memset не хочешь ты! yoda.jpg»? Что значит «выкинуть вызов»? Если такая оптимизация входит в какой-нибудь стандартный пакет O1 или O2, пусть такой компилятор лучше выкинет себя на помойку.

    Прикол будет, если мне сейчас расскажут, что так ведёт себя, например, VS и «всё, что жил — всё зря».


    1. rg_software
      29.11.2021 20:58
      +9

      Так ведут себя все современные компиляторы C++ точно, и подозреваю, что C тоже. Сильно сомневаюсь, что комментатор выше прав, но его теория выглядит рационально.


      1. SShtole
        29.11.2021 21:13

        ruomserg Не совсем понял его/вашу мысль. Как связана оптимизация шаблонного шлака с выкидыванием вручную прописанного вызова memset? Компилятор разве не может различить эти две ситуации?


        1. Ritan
          29.11.2021 21:35
          +1

          https://godbolt.org/z/n84ej87xP

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


          1. SShtole
            29.11.2021 21:41

            Как это связано с шаблонами, которые, якобы, спровоцировали такую агрессивную оптимизацию?

            Что касается «memset не даёт гарантий» — да, теперь буду знать, спасибо.


        1. rg_software
          29.11.2021 22:02
          +8

          А что такое "вручную"? Забудем о шаблонах. В C есть простые обычные макросы. Вы в макрос запихнули несколько инструкций. Потом этот макрос оказался внутри другого макроса, а другой -- внутри третьего. И в итоге когда это всё развернётся, у вас окажется, например, фрагмент вида

          a = 5;
          a = 15;
          a = 7;

          И теперь по вашей логике нельзя выбросить первые две строки? Ведь программист написал, значит, знал, что делает.


          1. ruomserg
            30.11.2021 07:44
            +2

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

            И мне кажется, что для «С» — это правильная модель. Идеология «С» в 80-е годы была именно что «высокоуровневого ассемблера». Предполагалось, что программист достаточно хорошо понимает — что и для чего он пишет — а дело компилятора, просто перевести это в команды процессора. И оно примерно так и работало — и все были более-менее довольны. Проблемы начались в тот момент, когда началась автоматическая кодогенерация. Поскольку это не живой человек — оно свой сгенерированный код оптимизировать вдумчиво не собирается. А нужно было после сборки чтобы оно хоть как-то шевелилось. Ну и всем показалось, что компилятор — самое подходящее средство чтобы им лечиться. Благо тактовые частоты подросли, теория компиляторов подтянулась — и фактически компилятор начал переводить в команды не тот код, который написал программист, а другой код — относительно которого он (компилятор) может доказать его (другого кода) эквивалентность исходному относительно некоторого абстрактного вычислителя.

            Для C++, собственно, выхода не было — либо надо выбрасывать темплейтное мета-программирование (и идею SFINAE — инстанциировать тучу специализаций, и из них выбирать наиболее подходящую), либо соглашаться что компилятор будет компилировать не написанный человеком код, а эквивалентный ему над абстрактным вычислителем. Для языка «C», с моей личной точки зрения, это является большой ошибкой. В «C» нет встроенных механизмов генерации больших объемов dead code. Поэтому компилятор должен компилировать в ассемблер ровно то, что написано — возможно применяя некоторые локальные оптимизации, если таковые разрешены флагами при сборке.


            1. 0xd34df00d
              30.11.2021 09:34
              +1

              и идею SFINAE — инстанциировать тучу специализаций, и из них выбирать наиболее подходящую

              В SFINAE как раз множество специализаций не инстанциируется. F там Failure означает.


              1. ruomserg
                30.11.2021 11:11

                Я не буду спорить, потому что давно не смотрел во внутренности компиляторов современных. В моем представлении, когда они начинают процесс инстанциирования шаблонов — это может порождать процесс инстанциирования других шаблонов — и они генерируют целый лес в синтаксическом дереве (если вообще используется это представление...). И дальше два варианта — либо если где-то инстанциирование наткнулось на непреодлимую проблему, то откатывать обратно дерево и начинать сначала, либо оставлять куски в надежде что они скоро понадобятся снова. Я подозреваю (это мое ничем не подкрепленное спекулятивное суждение), что для увеличения скорости компиляции — они оставляют в дереве как можно больше того что сумело инстанциироваться, и полагаются на оптимизатор, который вычистит то, что в конце-концов не пригодилось…


            1. rg_software
              30.11.2021 12:06
              +2

              Мне кажется, в некотором смысле это игра в терминологию, потому что выше уже в комментариях приводили ссылку на статью "C is not a low-level language" с подзаголовком "Your computer is not a fast PDP-11". Нюанс в том, что ассемблер вашего компьютера совершенно не совпадает к логикой конструкций языка C, в котором, например, существуют отдельные операции A++ и ++A, имеющие смысл именно в контексте того ассемблера. Даже какое-нибудь тривиальное действие вида int a = 5; in b = 15; можно (и нужно?) выполнять тем способом, который можно считать родным для компьютера сейчас, а не в семидесятых годах. И даже в семидесятых компилятор понимал, что если в формуле с делением выгоднее сперва посчитать знаменатель, это и надо сделать. (Что-то помню подобное, но неточно). А отсюда уже один шаг до dead code elimination и прочих вещей, которые мы так любим. Я бы сказал, в текущей статье ещё очень малоагрессивные виды оптимизаций обсуждают.


              1. ruomserg
                30.11.2021 14:14
                +1

                Ну… я ж не отрицаю свободу компилятора в выборе конкретного набора инструкций, в который нужно пространслировать конкретную строку кода. Однако, в рамках языка «C», если в строке программист что-то написал — обязанностью компилятора являтся сгенерировать команды, которые максимально выражают написанное в машинных кодах. Понятно, что на одной платформе a++ будет атомарным, а на другой, например, нет. Это уже другое — и оно может решаться библиотеками или intrinsic-ами, или варварским asm {} в обрамлении ifdef-ов. Я не вижу причины, по которой в стандарте «C» (в отличие от C++ — там необходимость понятна...) разрешили компилятору генерировать код для программы не той, которую написал программист — а той, которая ей эквивалентна над определенным в стандарте абстрактным вычислителем. В целом, я бы убрал из стандарта понятие «UB» и потребовал чтобы компилятор во всех этих случаях либо выдавал синтаксическую ошибку, либо генерировал команды, выполняющие ровно те действия, которые написаны в исходном файле. И да, на разных платформах эти действия могут приводить к разным результатам.

                Возьмем, например, правило pointer aliasing. Мы можем предположить существование вычислительной системы с неоднородной памятью, где память для int-ов и память для float-ов — это две разные памяти (например, адресуемые разными группами регистров). Если принять, что указатель — это смещение относительно начала каждого вида памяти, то понятно что операция копирования значения int * в float * (и наоборот) — не имеет смысла. Они физически не могут указать на одну область памяти. Но это не значит, что компилятор может выкинуть такие присвоения. Он должен либо упасть с синтаксической ошибкой (если способен такое обнаружить) для данной платформы, либо все-таки перенести значение из одного адресного регистра в другой несмотря на то, что семантика «пусть float * указывает на ту же ячейку памяти куда указывал int *» поменялась на «пусть float * указывает на ячейку в банке float-памяти с таким же смещением как была у int *». Скорее всего, эта операция не будет иметь смысла для данной платформы. Возможно, при таком переносе будет происходить усечение разрядности регистра и что угодно еще. Но это будет описано не в стандарте языка, а в спецификации данной вычислительной платформы. И программист, который под нее пишет — будет знать что тут действительно нельзя менятся значениями указателей между int и float. И это — нормально. Но компилятор не должен выкидывать операции на одном только основании, что ему кажется, что эти операции не имеют смысла или ни на что не влияют… По крайней мере, компилятор «C». В этом случае он останется надолго востребован и актуален…


                1. netch80
                  30.11.2021 16:28
                  +4

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

                  Вопрос в границах этого действия.

                  Вот… что бы такого совсем простого:)) Решаем квадратное уравнение.
                  Дискриминант: d = b*b — 4*a*c;
                  Должен ли этот код реально читать b дважды из памяти (где он там лежит), или таки он не тварь дрожащая, и может сохранить его в регистре?
                  Умножать на 4 должно быть сделано с конверсией 4 из целого в плавучее каждый раз, или может сразу умножить на 4.0, или применить какой-то FSCALE на 2 двоичных порядка (может быть ещё быстрее)?
                  x1 = (-b+sqrt(d))/(2*a);
                  x2 = (-b-sqrt(d))/(2*a);
                  Надо ли sqrt(d) считать дважды, если мы точно знаем, что sqrt() — чистая функция (то есть не зависит от любых внешних данных)?

                  z = a*b + b*c — 2*c*a;
                  и снова, читать переменные каждый раз или через раз?

                  for (int i = 0; i < 3; i++) {
                    v[i] *= x;
                  }
                  


                  то же самое.

                  Пусть вы решили перевалить вопрос на программиста — что нужно, он пометит register.
                  Теперь: у процессора в варианте A — 4 относительно свободных регистра для этого, в варианте B — 7 регистров. Должен ли программист о всём этом заботиться?

                  double *a, *b, *c;
                  for (int i = 0; i < 3; ++i) {
                    a[i] = b[i] + c[i];
                  }
                  


                  могу ли я прочитать сначала все b, потом все c, сложить и положить в a (это реально векторная операция, можно сэкономить на процессоре), или должен делать всё попунктно по каждому i (это уже вопрос к алиасингу, если мы не знаем, не пересекаются ли области a с b или c)?

                  > Возьмем, например, правило pointer aliasing.

                  > Но компилятор не должен выкидывать операции на одном только основании, что ему кажется, что эти операции не имеют смысла или ни на что не влияют…

                  А где и как провести границу?

                  Вот мы перемножаем матрицы:

                  for (int row = 1; row <= N; ++row) {
                    for (int col = 1; col <= N; ++col) {
                      double sum = 0;
                      for (int xi = 1; xi <= N; ++xi) {
                        sum += A[row][xi] * B[xi][col];
                      }
                      sum[row][col] = sum;
                    }
                  }
                  


                  Имеет ли право компилятор оптимизировать все вычисления адресов, типа, строки A[row] которая на уровне байтов будет &A[1][1]+(row-1)*N*sizeof(double)? А смещение от этой позиции, которое равно (xi-1)*sizeof(double)? А само вычитание 1, если индексация от 1 (может, недействительно для C, но есть языки, для которых важно)?
                  Эти вопросы встали ещё в конце 1950-х, когда стали делать первые компиляторы.




                  Я мог бы тут ещё 100500 таких примеров привести. Главное то, что если вы всё перевалите на программиста, код станет неэффективен за пределами конкретного процессора — фактически, это будет ассемблер. А если не перевалите, то придётся дать компилятору возможность разных оптимизаций.

                  А вот насколько их разрешать — вот то, что сейчас решается компиляторозависимыми опциями.

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


                  1. ruomserg
                    01.12.2021 00:23

                    Я боюсь, что мы сейчас начнем придумывать новый стандарт «C». :-) Но смотрите — моя идея примерно следующая:

                    1. Арифметические оптимизации разрешены — потому что арифметика является как раз подмножеством «хорошо определенного» вычислителя. Все существующие UB там переводятся в target-specific behavior. То есть компилятор генерирует команды для target-платформы, а будет ли при этом в рантайме аппаратное прерывание, потеря точности, или процессор зависнет — его не волнует.

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

                    3. Константные выражения — вероятно как в случае с арифметикой, по-умолчанию можно.

                    В целом, я бы оптимизации начиная с reordering внутри блока начал ограждать красными флажками в виде опций. А dead code elimination — уже даже не флажками, а светящимися шарами пару метров диаметром (как провода ЛЭП в авиации :-)


                    1. netch80
                      01.12.2021 01:20

                      > Все существующие UB там переводятся в target-specific behavior. То есть компилятор генерирует команды для target-платформы, а будет ли при этом в рантайме аппаратное прерывание, потеря точности, или процессор зависнет — его не волнует.

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

                      В случае ассемблера я знаю особенности конкретной платформы и выбираю реализацию под неё. Если это что-то с NZVC — я буду проверять флаг V (OF) при подозрении, что что-то не так пойдёт и не поймано раньше. Если что-то совсем без флагов, как RISC-V или MIPS — для него есть свои достаточно простые средства — писал рядом, но это ещё проще описать так: `(b<0) != (sum<a)` (если sum уже подсчитано с усечением). Если System/Z — переход по CC=3 после знакового сложения. Если мне нужно сложение с усечением — я просто вызову команду без детекта переполнения. Главное, на этом этапе я всем полностью управляю.

                      И такого же управления я хочу на уровне языка повыше. Нужна операция с усечением — вызываю её (вокруг кроме машин с дополнительным кодом сейчас ничего нет, поэтому пойдёт). Нужна с проверкой — вызываю и её. Как минимум — явно вызываю, это уже первый уровень возможности. (Его GCC с компанией наконец-то начали обеспечивать, хоть и громоздко.) Но дальше хочется второго уровня — чтобы это было удобно. Чтобы, например, сделать

                      int seen_ovf = 0;
                      [[int_arith(checked_with_flag(seen_ovf))]] {
                        ... рабочий код со стандартной записью операций ...
                      }
                      if (seen_ovf) { error("всё было зря"); }
                      


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

                      А если ещё и можно будет описать всякими директивами вместо тупых int32_t что-то типа integer range(0..200000) и компилятор поэтому выкинет большинство проверок как ненужные — ещё лучше.

                      > Вынос инварианта за цикл — только по отдельной опции.

                      В большинстве случаев он не влияет. Влияет тогда, когда начинаются проблемы алиасинга. Но, да, запретить (переносимо) такие оптимизации… ну я бы таки попросил. Хотя бы как контрольную меру.

                      Управление оптимизацией на уровне всего проекта или даже одного исходного файла, которое было нормой в первые десятилетия компиляторостроения, сейчас стало неадекватным. Можно строить зависимости от режима сборки, но управление по функциям и блокам становится критически важным для того, чтобы не впадать в режим «включили -O2 и всё сломалось в пятнадцати местах, и интерференция ошибок такая, что мы нафиг не понимаем, как вообще начинать искать». GCC (>=5) сделал прагмы режима раздельно по функциям, но это 1/20 от желательного.

                      > А dead code elimination — уже даже не флажками, а светящимися шарами пару метров диаметром (как провода ЛЭП в авиации :-)

                      Так полезная же штука. Иногда (особенно при раскрытии макр и шаблонов) превращается в экономию на порядки.
                      Так что с этим я бы не очень спешил. А вот пометить расслабление именно на типовые проблемы типа разыменования вероятно пустого указателя — полезно.
                      (Кстати, не везде можно исключить конкретное значение указателя, чтобы это был NULL, и не везде 0 надо запрещать. Но это, наоборот, имеет смысл явно контекстно включать для тех небольших блоков особо системного кода, которые таки полезут по адресу 0. Привет от Хоара.)


                1. rg_software
                  30.11.2021 16:34
                  +1

                  Не могу сходу ссылку привести, но в паре кликов отсюда (где описываются трюки похлеще здешних) говорилось, что отследить UB для компилятора -- это штука технически почти неосуществимая. Современный тулчейн типа llvm устроен так, что отдельные оптимизации и прочие трюки существуют во многом независимо и работают с промежуточными представлениями, которые уже очень далеко отстоят от исходного кода. Поэтому отделить "вот эту кашу, которая получилась из рукописного кода" от "вон той каши, что вышла из раскрытия макросов, ассемблерных вставок и всяких прагм" уже никак не выходит.


                1. 0xd34df00d
                  30.11.2021 20:23
                  +3

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

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


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


                  1. ruomserg
                    01.12.2021 00:13

                    Ну так я и не требую от компилятора детектировать UB во всех случаях. Если компилятор не может детектировать UB — он тупо генерирует код, выполняющий то, что написано в файле. То есть если написано int x=0;float *p=&x — то берется, мать его, адрес «x» (чем бы это значение не выражалось в целевой платформе), и записывается бит-за-битом туда, где выделено место под указатель «p». Имеет ли это смысл на целевой платформе, будет ли в этом месте ошибка времени исполнения, зависнет ли после этого процессор — компилятор волновать не должно. Разработчик написал сделать — компилятор перевел. В «С» изначально предполагалось, что разработчик знает, что он делает когда пишет код. В отличие, например, от паскаля…


                    1. vkni
                      01.12.2021 00:43
                      +1

                      Насколько я понимаю там 50 штук разных проходов, в которых теряется информация о том, что же изначально было написано. А проходы эти в таком кол-ве потому, что С не является языком низкого уровня для современных десктопов - https://queue.acm.org/detail.cfm?id=3212479

                      А ещё есть прекрасная статья по-поводу UB https://www.complang.tuwien.ac.at/kps2015/proceedings/KPS_2015_submission_29.pdf

                      То есть, исторически эти UB позволяли подправлять код под машину (когда C был близок к железу, а компилятор был очень прост).

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


              1. ignat99
                01.12.2021 13:59
                -1

                В сигнальных процессорах были так же две параллельные команды и иногда три. Вообщем ASM надо знать (или хотябы держать перед глазами схему регистров данного микропроцессора) перед тем как писать на СИ.


    1. Gumanoid
      29.11.2021 21:20
      +4

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


      1. Kircore
        30.11.2021 09:27
        -1

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

        В Си это невозможно предсказать.


        1. Ritan
          30.11.2021 11:19
          +4

          Если указатель не покидает пределы функции, где выделен - можно


          1. F0iL
            30.11.2021 12:36

            ...и если он при этом не обозначен как volatile.


            1. Ritan
              30.11.2021 14:56

              Ну с volatile вообще мутное. Вот тут, например, вызов memset всё равно удалён. Хотя clang почему-то решил память обнулить всё же
              https://godbolt.org/z/aK49aqT6d


            1. Kircore
              01.12.2021 00:00
              -3

              У volatile свободная реализация, он ничего не гарантирует.


          1. Gumanoid
            30.11.2021 14:08

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


          1. Kircore
            30.11.2021 23:59

            Нет, если у тебя не выделяется/очищается память при этом через malloc/free. Указателю ведь можно задать абсолютный адрес, который можно использовать за пределами функции.


            1. Ritan
              01.12.2021 14:30
              +1

              Я же сказал "пределы функции, где выделен". Про какой абсолютный указатель речь?

              Т.е. говорил про ситуацию

              void foo() {
                int* ptr = malloc(42);
              	// code using ptr
                memset(ptr, 0, 42);
                // more code not using ptr
                free(ptr);
              }

              Этот указатель не покидал функцию foo, использование(чтение, запись) такого же указателя за пределами этой функции было бы UB.


    1. rafuck
      29.11.2021 21:23
      +2

      1. SShtole
        29.11.2021 21:36
        +1

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

        Но мне это всё равно кажется глубоко неправильным.


      1. Sdima1357
        29.11.2021 21:36
        +3

        Собака еще глубже. Допустим мемсет отработала и стерла секретные данные. Но нет гарантии что эта операция прошла дальше кеша если процесс к тому времени завершился. Теперь другой случай, когда эта часть памяти - ввод вывод (внешнее устройство). Вобщем на компилятор надейся , а результат проверяй!


        1. rafuck
          29.11.2021 21:54

          Ну, гарантии уйти дальше кеша вообще дать проблематично, если у вас процесс падает.


          1. Sdima1357
            29.11.2021 22:37

            Даже если не падает, а завершается законно- кеш не обязан сохраняться


            1. rafuck
              29.11.2021 23:10
              -1

              Не очень понимаю, к чему этот комментарий. Я отвечал вот на это:

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


              1. Sdima1357
                29.11.2021 23:20
                +1

                Комментарий к тому, что даже при нормальном завершении есть риск утечки данных


                1. rafuck
                  29.11.2021 23:33
                  -1

                  Я бы даже сказал, что он есть всегда. Но все равно, какая-то несвязная беседа получилась… ,)


                  1. Sdima1357
                    30.11.2021 00:16
                    +1

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


                    1. rafuck
                      30.11.2021 10:27
                      -1

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


                      1. Sdima1357
                        30.11.2021 10:42

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


                      1. rafuck
                        30.11.2021 11:18
                        -1

                        И как это поможет выявить несогласованность кеша и физической памяти?
                        P.S. На всякий случай. Минусую ваши комментарии не я, мне правда интересно.


                      1. Sdima1357
                        30.11.2021 11:26
                        +1

                        Элементарно. Пассворд останется в памяти. Ядро освободит страничку без коммита кеша при освобождении ресурсов процесса на его завершении


                      1. netch80
                        30.11.2021 11:41

                        > Ядро освободит страничку без коммита кеша

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


                      1. rafuck
                        30.11.2021 11:47

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


                      1. Sdima1357
                        30.11.2021 13:01
                        +1

                        Страница помечена для ядра как освобожденная(вот прям сейчас). В странице по определению мусор, с точки зрения ядра. Зачем ядру синхронизировать запись в кеше, которая ссылается на память в этой странице? Это лишние расходы времени и шины.


                      1. vanxant
                        30.11.2021 13:05

                        Можете привести инструкцию какого-нибудь процессора, при помощи которой ядро может отменить запись процессором в write-back память из кэша?


                      1. Sdima1357
                        30.11.2021 15:01

                        INVD — Invalidate Internal Caches

                        Invalidates (flushes) the processor’s internal caches and issues a special-function bus cycle that directs external caches to also flush themselves. Data held in internal caches is not written back to main memory.

                        Use this instruction with care. Data cached internally and not written back to main memory will be lost.


                      1. rafuck
                        30.11.2021 15:23

                        Хм. Интересно!
                        Насколько я понимаю, эта инструкция используется в очень специфических случаях (например, при использовании части кеш-памяти в качестве адресуемой физической памяти).


                      1. cepera_ang
                        30.11.2021 15:23
                        +2

                        Последствия вызова такой инструкции в процессе нормальной работы будут весьма разрушительными.


                      1. Sdima1357
                        30.11.2021 15:41
                        +1

                        Последствия вызова

                        А я и не спорю. Но никто не гарантирует что в новом процессоре не будет добавлено других иструкций с кешем. У меня просили пример. Я привел. Кроме того пусть падает, пассворд то уже меня. А вообще вот:

                        Unfortunately, for architectures that do not manage their cache or Translation Lookaside Buffer (TLB) automatically, hooks for machine dependent have to be explicitly left in the code for when the TLB and CPU caches need to be altered and flushed even if they are null operations on some architectures like the x86. These hooks are discussed further in Section 3.8.

                        https://www.kernel.org/doc/gorman/html/understand/understand006.html


                      1. cepera_ang
                        30.11.2021 16:06
                        -1

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


                        — А что если нам надо память очистить, а компилятор выкидывает вызов memset, разве это дело? "
                        — Да пофиг, ведь у нас ядро может не дать кеш сбросить
                        — Эм, но ядро не будет так делать
                        — Ха, вот и инструкция для этого есть и вот тут есть какой-то никем не используемый процессор, где кешем надо вручную управлять, так что будет
                        — Эммм, а что если где-то есть процессор, который содержимое регистров на ЖК-дисплей выводит, а там камера стоит которая автоматически пароли считывает, значит ли это, что нам можно вообще не пытаться пароли в памяти зачищать?


                      1. Sdima1357
                        30.11.2021 16:15
                        +1

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

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

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

                        Современные процессоры позволяют программисту ошибаться миллиарды раз в секунду. :)

                        Вы не поверите сколько и каких делают проверок, например в медицинской аппаратуре, в приличных фирмах


  1. insecto
    29.11.2021 21:31
    +11

    Нуу, в такой дискуссии я на стороне стандарта и gcc. Чуваки пишут в коде какую-то чушь, и жалуются что она не работает. Например, переполнение при сдвиге знакового. Не думают же они, что компилер на каждый такой сдвиг вставит проверку на переполнение? Мы не для того используем Си.

    Или вот это int x; int *p = &x + 1; это ж совсем за гранью, такое писать! Они ждали каких-то гарантий расположения переменных (а то и их существования) на стеке? Да ещё и с арифметикой указателей поверх этого? Безумие.

    Вот такой ассерт: assert(a + 100 > a); где a+100 это уже чёрт-те какое значение. Очень странной выглядит жалоба, что нельзя сравнить чёрт-те какое значение с другим значением, и на выходе не получить чёрт-те что.

    Я ещё понимаю Торвальдса и Кармака, у них список платформ ограничен и известен зараннее, им можно полагаться на то что int* и double* можно свалить в одну кучу, и не получить по рукам (да и то не всегда). А у комитета такой роскоши нет, им надо рассчитывать на потенциально любой компилер и платформу, даже те гипотетические, которых ещё нет.


    1. Mingun
      29.11.2021 21:42
      +2

      где a+100 это уже чёрт-те какое значение.

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


      Или вот это int x; int *p = &x + 1; это ж совсем за гранью, такое писать! Они ждали каких-то гарантий расположения переменных (а то и их существования) на стеке?

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


      Вот уже надеяться, что &x + 1 укажет на y наверное странно… если только в стандарте не прописано, что переменные кладутся в стек в порядке их объявления. А то, что y тоже будет на стеке следует из того, что берется ее адрес.


      1. insecto
        29.11.2021 21:49
        +4

        неопределенное поведение задумывалось как возможность написать программу под специфическую платформу

        Нет, для этого есть implementation-defined behaviour и unspecified behaviour.

        изощренный способ сказать "не пишите так"

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


    1. netch80
      30.11.2021 11:30
      +1

      > Чуваки пишут в коде какую-то чушь, и жалуются что она не работает. Например, переполнение при сдвиге знакового. Не думают же они, что компилер на каждый такой сдвиг вставит проверку на переполнение? Мы не для того используем Си.

      А почему в таком случае переполнение при сложении или сдвиге беззнакового не происходит, остаются только N младших бит в любом случае? В чём такая принципиальная разница между знаковыми и беззнаковыми?

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

      Отлично — что мешает завтра появиться платформе, на которой «add a,b» вышибает нафиг, если carry flag поставился бы?

      Только почему-то прогресс идёт в обратном этому направлении, в ARM и RISC-V деление на 0 проглатывается молча. Странные люди, наверно…
      И платформ с тритами вместо битов никто не пишет…
      А в C++20 вообще дополнительный код канонизировали как единственно возможный. Наверно, не рассчитывают, что завтра Intel скажет «всё, нафиг» и перейдёт на 1-complement…
      [/sarcasm]


  1. gregorybednov
    29.11.2021 21:57
    +1

    Вспоминается статья Криса Касперски "Языки, которые мы потеряли". Тут статья немного о другом, но в целом получилось "На этот раз мы теряем и Си".

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

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

    2. Си мог бы наверное развиваться в сторону полноценного метапрограммирования (с самомодифицирующимся кодом, с построением программ программами), но вместо этого мы имеем непрозрачные шаблоны C++, которые вроде как облегчают копипаст кода и... тут же создают тучу неоднозначностей, в которой вроде как "нет" ответственных, вроде того а что будет, если я попробую сравнить теплое с мягким (или хотя бы даже треугольник с прямоугольником) используя полиморфизм оператора сравнения.

    3. Настоящие языки метапрограммирования, поддерживающие СМК (самомодификацию кода) на официальном уровне - а именно Лисп и Форт - по сути забыты (отдельное уточнение: здесь не соглашусь с Крисом - и тот, и другой язык жив, но назвать их существование достойным на фоне однотипных потомков языка C во главе "плюсами", а с учетом последних стандартов и описанных в статье фокусов с компиляторами уже и обыкновенного C, всё же язык не повернётся)

    Ну и само собой пояснение: вопрос "жирности" не праздный - да, мы можем себе позволить на своих рабочих станциях такое количество вычислительных мощностей, которые Крису могли бы разве что присниться (причем, скорее всего, в страшном сне), но можем ли мы понять, как работает "жирная" программа, даже если нам полностью доступны отлично документированные исходные тексты?


    1. rg_software
      29.11.2021 22:08
      +2

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


      1. gregorybednov
        29.11.2021 22:25
        -2

        Не могу согласиться. С такими доводами как у Страуструпа, во времена Кобола говорили бы то же самое про Кобол, и где он сейчас? Речь идёт как раз о том, и в статье Криса это подчеркивается, что в программировании решают отнюдь не люди, не технологии, а индустрия. И индустрии было выгодно, чтобы ходить в соседний ларёк можно только на космическом корабле с облётом вокруг Плутона - потребовалось делать продукты на заказ, имелись компьютеры, которые работали быстро только если программы были на Си - ну вот, как говорится, "получите, распишитесь".

        Если и сравнивать "по Страуструпу", тогда уж уместнее это делать так: языки делятся на те, на которых людей всё равно заставят работать, и те, на которых они могли бы; в такой интерпретации данное суждение действительно неоспоримо.


        1. rg_software
          30.11.2021 07:24
          +1

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

          То есть имелась ясно очерченная проблема (предметная область без приличного языка) и были сделаны хорошо подготовленные усилия для её преодоления.

          Сейчас мы наблюдаем совершенно иной процесс: "маленький и приятный" язык, который изначально создавался в пику чему-то монструозному тоже превращается в монструозность, см. Python. А если не превращается, то остаётся нишевым. Можно посмотреть, как растёт с годами объём документации.

          При этом распухание языка -- это ни разу не про распухание кода. Это про (а) реализацию модных фишек, потому что вот в языке X есть лямбды и дженерики, я в своём тоже такое хочу и (б) как раз про эффективность кода как по объёму, так и по скорости.

          Заметьте, мы тут обсуждаем оптимизации gcc, которые все как раз направлены на рост производительности итогового приложения. Расширения стандарта C++ (move semantics, в частности) тоже про более эффективный код. Так что мы видим попытки сделать софт не толще и медленее, а ровно наоборот. Нередко скорость достигается ценой разбухания, но это отдельный вопрос.


        1. paluke
          30.11.2021 09:02

          Ну сам кобол может и помер, но потомки живы ru.wikipedia.org/wiki/ABAP/4


  1. WhiteWhiteWalker
    30.11.2021 00:10
    -4

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


    1. kmeaw
      30.11.2021 01:21

      Если gcc нетривиальным образом обновился, то он поменяет своё поведение. Иначе зачем его вообще обновлять?

      Каким образом разработчики gcc должны отличать "хорошие" изменения поведения от "плохих"?


      1. WhiteWhiteWalker
        30.11.2021 19:41

        Имеются ввиду изменения в поведении, когда по стандарту UB, а по логике всё в порядке. примеры вроде как есть в статье.


  1. Solovey572
    30.11.2021 01:23

    Я сначала подумал, что это какой-то азиатский язык, а потом как вспомнил...


  1. Sergey_zx
    30.11.2021 02:03

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

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

    Но зато оптимайзер сэкономил сто байт памяти переменных.

    По моему цель не оправдывает средства.

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

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

    Си, он как скальпель. Можно делать тончайшие операции, а можно сдуру зарезаться. Нет опыта и мастерства владения скальпелем, используй картофелечистку. Ей не зарежешься, но и ничего кроме ее задач и не сделаешь.


  1. samoreklam
    30.11.2021 07:00

    Полезная статья! (для меня)
    Показало, ещё одну возможную проблему в моём коде. Хотя, я уже сталкивался с "strict aliasing" . Исправлял код, не вникая в суть проблемы. Теперь отнесусь более внимательно к стандарту. (и снова, долго и упорно буду проверять свой код)
    А PVS-Studio обнаруживает подобные ошибки? (наверно да, но хочется узнать точно)

    И раз уж здесь такая тема, то хочу задать вопрос!
    Я сейчас пишу программу (сервер) и делаю тесты производительности с компиляцией через gcc без оптимизации и с -O3 . И по результату, скорость работы почти одинаковая. Что это означает? (код хорошо/плохо написан? компилятор не находит то, что можно оптимизировать? или ещё что?)


    1. vanxant
      30.11.2021 08:59
      +1

      Подозреваю, что если "сервер", то основную часть времени он сидит в ядре (epoll или что там у вас). Эффективно перекладывать байты с диска/БД в пакеты можно и на PHP, и на node.js =)


  1. app-z
    30.11.2021 07:27
    +1

    float Q_rsqrt( float number )
    {
    	long i;
    	float x2, y;
    	const float threehalfs = 1.5F;
    
    	x2 = number * 0.5F;
    	y  = number;
    	i  = * ( long * ) &y;                       // evil floating point bit level hacking
    	i  = 0x5f3759df - ( i >> 1 );               // what the fuck? 
    	y  = * ( float * ) &i;
    	y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //	y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed
    
    	return y;
    }

    Если бы не C то в Doom бы мы не поиграли на Intel386


  1. piratarusso
    30.11.2021 09:08

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

    Современный компилятор GNU C++ можно заменить разве что на LLVM. И обе эти программы написаны на с и обеспечивают широкую поддержку современных платформ . Так что если вы хотите избавиться от с, а вам следует предложить ЯП, пригодный для написания компилятора. Но поскольку в этом направлении никто не двигается, то очевидно все новые языки будут лишь сокращать область применения с, но заменить его полностью не смогут. до тех пор, пока не будет реализован набор компиляторов, написанных на них с достаточным охватом современных платформ


    1. netch80
      30.11.2021 09:11

      > вам следует предложить ЯП, пригодный для написания компилятора. Но поскольку в этом направлении никто не двигается

      В каком смысле «никто»? Подавляющее большинство новых языков после C++ пригодны к тому, чтобы сделать на них свой компилятор.

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

      В данном случае это факт инерции — уже есть две мощнейших реализации на C/C++ и переписывать их на нечто новое никто просто так не будет.


      1. piratarusso
        30.11.2021 09:33

        Вот и получается, на текущем уровне развития технологий с незаменим.


        1. netch80
          30.11.2021 09:58

          > Вот и получается, на текущем уровне развития технологий с незаменим.

          Если «уровнем развития технологий» называть таковой чисто из-за лени, то да, согласен. Но это не значит, что C/C++ не могут уйти в свою специфическую нишу «для компиляторов», где доживут ещё лет 20. Всё может — как только появится полноценная замена, поддержанная кем-то толстым.


          1. piratarusso
            30.11.2021 12:59

            «Уровнем развития технологий» я называю доступность для практической работы инструмента, иногда употребляют слово "экосистема". Универсальных инструментов на самом деле не так уж и много. С на текущий момент пригоден для большого количества платформ и задач, он обеспечен своей экосистемой для широкого применения, экосистема rust, для примера, куда более скромна, потому и применимость его куда более ограниченна_. Ну а, скажем, более экзотические языки, при всех своих достоинствах, обычно не выходят за пределы своих ниш.


    1. 0xd34df00d
      30.11.2021 09:36
      +4

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

      Любой язык из ML-семейства куда более пригоден для написания компиляторов, чем C.


      1. piratarusso
        30.11.2021 13:01
        -1

        Потенциально пригоден - возможно.


        1. 0xd34df00d
          30.11.2021 20:27
          +3

          Почему потенциально? Компиляторы на нём вполне пишутся. Компиляторы всяких ML'ей написаны на этих ML'ях, компилятор хаскеля написан на хаскеле. Компиляторы всяких ресерч-языков пишутся на том же хаскеле. Я для своих проектов, где надо было написать компилятор или его кусочки, тоже выбирал совсем не С.


        1. DirectoriX
          30.11.2021 23:08
          +2

          Компиляторы на C пишут потому что а) так исторически сложилось и б) потенциально можно добиться очень высокой производительности.
          Кстати, а вы можете назвать хотя бы один язык, который был бы принципиально непригоден для написания компилятора, учитывая что языки Тьюринг-полные, а значит на них можно написать эквивалентную GCC/Clang логику?
          Я даже не очень сильно удивлюсь, если лет через 5 в новостях будет «игрок собрал компилятор подмножества C в Minecraft»


          1. piratarusso
            01.12.2021 10:02

            Компиляторы давно пишут на чём попало. Это просто праздник какой-то . Особенно в последние пару лет популярна идея source-to-source compiler. Из ruby в javascript (opalrb.com)), ещё WASM кое где пропагандируют. Я не знаю, может быть в будущем появятся операционные системы, написанные на интерпретируемом языка.

            Разногласия у нас только в пункте б)

             б) потенциально можно добиться очень высокой производительности.

            И тут я бы переформулировал. Существует toolchain, который обеспечивает цикл разработки ядра операционной системы, системных и прикладных программ. Если мы собираемся убить с, то мы должны это чем-то заменить. Ключевая часть этого toolchain - это компилятор с.


            1. permeakra
              01.12.2021 12:18

              Ключевая часть этого тулчейна - это форматы объектных файлов, утилиты для работы с ними плюс соглашения типа call conventions. От языка и компилятора Цэ они вполне отдираются, тот же gnu ld нормально работает c сlang

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


  1. volodyaleo
    30.11.2021 09:13

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

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

    Человеку, допустившему такие ошибки, ничего не помешает при портировании кода на Rust просто взять и затолкать в блок unsafe всё (взяв первую ссылку которая бы посоветовала это сделать и не дочитать). Если следовать той же логике - rust тоже дает себе в ногу стрелять и поэтому надо заменить. + большой пласт ошибок с дедлоками и конкаренси никак не наловишь.

    Если интересно писать на си безопасно, обычно применяют базовые вещи - юнит тесты, и санитайзеры: undefined behavior, address sanitizer, memory sanitizer, thread sanitizer. Однако если стиль кода хороший, чаще всего ловятся логические ошибки, от которых ни один язык программирования не сможет защитить, только тестирование. Также существуют стат анализаторы которые большой пласт ошибок ловят.

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

    П.С. я не против раст, а против слабых аргументов.


    1. netch80
      30.11.2021 09:17
      +5

      > На счет «пароль останется в регистре процессора». Размер регистра не позволит пароль там хранить из-за размера регистров. Абсурд в квадрате.

      В 64-битном регистре помещается 8 символов ASCII. У многих пароли не длиннее. Если у кого-то длиннее — то знание первых 8 символов вместе с 1% личной информации может помочь догадаться об остальных.
      В XMM, YMM соответственно 16 и 32 (через них любят копировать память). Редко какие пароли длиннее.
      Прежде чем рассказывать про «абсурд в квадрате», примените арифметику.

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

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


      1. volodyaleo
        30.11.2021 09:38

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


        1. netch80
          30.11.2021 10:04
          +2

          > 8 символов влезет, но если пользователь 9 впишет?

          Попробуйте внимательно прочитать мой комментарий.

          > И куда терминальный символ делся?

          Для задачи типа «подобрать пароль с 10 попыток» он не критичен.

          > Как быть с фактом, что пароль в регистре ничего не дает?

          Такого факта нет. Ознакомьтесь, пожалуйста, с проблемами неочищенной памяти в теме секьюрити.

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

          И всё-таки ознакомьтесь с проблемами неочищенной памяти. Минимальный комплект проблем:
          — Нарушение секьюрити в другом компоненте (библиотеке), через которую утекают данные (например, недочищенный padding).
          — Дампы всяких падений, которые увидит админ.
          — Spectre и тому подобные средства на чтение памяти.

          Полный комплект сильно шире, но sapienti sat.


          1. volodyaleo
            30.11.2021 12:37

            Не думаю, что другие языки не используют регистры и RAM. Соответственно это применимо ко всем языкам. Чем тогда именно си плох? Если в rust (да любой язык вместо) создать переменную строку, потом сравнить с другой, где строки будут?

            И сколько байт от пароля окажется в регистре - детали имплементации. Может один байт. В других языках как будет дело?

            Нарушение секьюрити в другом компоненте - также при чем тут си? В других языках такого нет? Например в Rust подключенная либа с unsafe (любой язык кроме rust также)


          1. volodyaleo
            30.11.2021 12:49

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


            1. netch80
              30.11.2021 13:32

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

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

              > Это просто то, что вне языка программирования.

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

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


          1. volodyaleo
            30.11.2021 13:35
            +1

            Я понимаю, что си не без грешка. Но он и применяется сейчас в основном в эмбеддед. И да, рядышком улеплены везде ассемблерные вставки обычно в sdk так как работа в эмбеддед касается железа напрямую. Зачастую больше кода там "unsafe" так как приходится обращаться напрямую к регистрам, аппаратной части, DMA итд. Там и аут оф баунд используют на структурах во благо (в парсере протоколов кастуют буфер на структуру с массивом единичной длинны и вкладывают в структуру байтик длины, не безопасно, но видал такое часто...). Поэтому аргументы кажутся слабыми ибо рассматривался язык как высокоуровневый. Но это давно не так.

            В мире эмбеддед еще есть свои ограничения по памяти, например 1кб под ROM. Есть специфические требования, например DMA не умеет работать с ROM и вызовет хардфолт. Иногда требуется вообще к какому-нибудь адресу пригвоздить данные. Я не пробовал rust, но подозреваю, что в тех местах, где меня надо "спасать", он не поможет. Ибо эмбедеры обычно берут готовые абстракции для очередей и мем пулов. Выключают heap. И работают на стат буферах, где ошибиться довольно сложно... Таблицу векторов прерываний на си пишут или asm, там вообще функции строго по адресам должны быть. Пишут стартап файлы свои, чтобы из своих секций загружать данные правильно.

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

            А spectre не страшен, если ты экзекьюшен с RAM выключил и отключил debug интерфейс (аппаратная защита).


            1. netch80
              30.11.2021 13:43

              > Я понимаю, что си не без грешка. Но он и применяется сейчас в основном в эмбеддед.

              Ну с точки зрения application development — вероятно, да. С точки зрения остального… Linux на толстом сервере это embedded? А какая-нибудь Oracle DB?
              Вы слишком сбоку смотрите.

              > И да, рядышком улеплены везде ассемблерные вставки обычно в sdk так как работа в эмбеддед касается железа напрямую.

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

              > Поэтому аргументы кажутся слабыми ибо рассматривался язык как высокоуровневый. Но это давно не так.

              Как правильно говорят рядом в комментариях, он остаётся высокоуровневым, но иначе.


      1. volodyaleo
        30.11.2021 09:41

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


        1. 0xd34df00d
          30.11.2021 09:45
          +1

          Например, результат инлайнинга одной функции в другую.


          1. netch80
            30.11.2021 10:06

            Ну в случае инлайнинга, скорее всего, в компиляторе эта логика не сработает (хотя кто их знает, этих писателей). А вот если код подставлен макрой — я бы дал 100%…


            1. 0xd34df00d
              30.11.2021 10:24
              +1

              Ну это же легко проверить. И gcc, и clang ломают ваш код.


              1. netch80
                30.11.2021 11:11

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

                А вот если бы был код типа

                int val = *ptr;
                if (!ptr) log();
                


                в одной функции (существенно), и оно, определив, что есть проверка ptr, сочло, что чтение значения *ptr в val — UdB, и на этом основании что-то сломало (например, записало бы всегда 0 в val) — было бы интереснее. Но такого я добиться не смог.


                1. 0xd34df00d
                  30.11.2021 11:18
                  +1

                  Как это не то? Изначальный тезис:


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

                  В коде выше сначала разыменование, а потом проверка. Вопросу удовлетворяет, ИМХО.


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


                  Но такого я добиться не смог.

                  Оно ж в таком случае просто вырежет проверку — то есть, сломает код.


          1. volodyaleo
            30.11.2021 12:40

            Инлайн никогда порядок не меняет. Частенько приходится глядеть в asm или бинари. Если человек грамотный, то проверки делает в начале функции перед всеми вызовами. Или сначала делает проверку на ноль, потом вызывает верификаторы использующие адреса.


        1. mayorovp
          30.11.2021 11:00
          +2

          Почитайте блог PVS-Stuidio. Так регулярно кто-то делает.


    1. cepera_ang
      30.11.2021 09:36
      +5

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


      Было бы интересно посмотреть на процент проектов на С, в которых строго применяется все вышеперечисленное и процент проектов на Rust, в которых всё просто запихнуто в unsafe блок.


      1. PsyHaSTe
        01.12.2021 04:03
        +2

        Во-первых ансейф блок не гарантирует отсусттвия проверок: https://steveklabnik.com/writing/you-can-t-turn-off-the-borrow-checker-in-rust


        Во-вторых стд останется в сейф блоках, да ещё и провалидированная сверху.


        Так что даже в таком сравнении раст может выглядить интереснее


  1. Flux
    30.11.2021 09:42
    +1

    Мы без конца ругаем язык С, и, разумеется, за дело. И все же я хочу спросить — кто написал четыре миллиона строк говнокода?

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


    1. cepera_ang
      30.11.2021 09:49
      +3

      как С должен умереть

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


      компании не готовы к массовым суицидам сишников пытающихся написать на расте двусвязный список.

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


      1. redsh0927
        30.11.2021 12:08
        +2

        так это ещё и максимально неудобная для железа структура данных — скакать по всей памяти, вымывая кеши и таблицы ради непонятно чего
        Не обязательно выделять тупо каждый объект маллоком! Это лишь способ связать объекты в некий ряд по каким-либо правилам для мгновенной навигации между объектами без пложения всяких дополнительных таблиц (в которые именно что и придётся лазить, портя кэши, прежде чем всё равно обращаться к нужному объекту). Сами по себе объекты могут быть выделены как угодно и где угодно. Разумеется, объект не обязан быть элементом только одного списка и вообще структура быть именно линейным списком тоже не обязана. Ссылок из объекта может быть произвольно много на родительские, дочерние, соседние, связанные, и ещё какие угодно объекты. Двусвязность же даёт возможность мгновенно убирать объекты из структуры, имея только указатель на объект.
        Можно наплодить сколько угодно «более лучших языков», но пока в них надо строить структуры данных из убогих «контейнеров», никуда сишечка гарантировано не денется.


        1. AnthonyMikh
          30.11.2021 20:46
          +1

          Двусвязность же даёт возможность мгновенно убирать объекты из структуры, имея только указатель на объект.

          При условии, что объект реально лежит в каком-то списке.


          При условии, что вы это делаете из одного потока.


          При условии, что указатели могут не указывать на сам этот объект.


          При условии, что сам объект не меняет своего положения в памяти.


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


        1. PsyHaSTe
          01.12.2021 04:06
          +3

          Двусвязный список в сишке сделать просто только потому, что на часть проблем структуры ответ дается в стиле "да мамай клянус такого не будет", а раст в маму не верит, ему нужно доказать. А вот написать всегад корректный список который с помощью произвольного количества вставок/удалений не выйдет развалить — уже дроугая задача.


          Ну и да, в реальности я связных списокв не видел ни разу в итоге — везде их выпиливали в пользу какого-нибудь вектора или кольцевого буффера.


    1. Kubera2017
      03.12.2021 15:52
      -1

      Это коммент 100%)) я попытался граф на расте написать, вообще непонятно кто чем владеет. а уж если гиперграф то полный аут


      1. warlock13
        03.12.2021 18:15
        +1

        Если граф нужен именно domain-specific, то есть если нельзя его как-то факторизовать от domain-specific кода, то стоит рассмотреть вариации на тему ECS, generational arena. Например, см. мою библиотеку https://crates.io/crates/components-arena Понятно, что у такого подхода есть определённые минусы, но если они существенны, то без подсчёта ссылок скорее всего всё равно не обойтись - что раст у тебя, что не раст.


        1. Kubera2017
          07.12.2021 23:49

          Спасибо. Хочется прям adjacency list на поинтерах конечно, про контейнеры я уже думал. Меня, если честно, еще очень волнует вопрос можно ли как то обойтись без итераторов, neo4j тоже вся на итераторах, реализации графов на расте, которые я нашел поиском тоже


      1. bm13kk
        04.12.2021 01:28

        Есть вероятность что я сейчас начну холивар. Но мне кажется эта тема стоит рисков.

        Возможно проблема не в расте\графе. А в коде, который пользуется графом. Попробуйте подход, который использует лисп к графам. И к сожалению, для этого надо выучить весь лисп.


        1. Kubera2017
          07.12.2021 23:24

          А что имеется ввиду под этим кодом? Код, который пользуется графом максимально прост: 1) добавить вершин штук n со свойствами 2) добавить граней между ними со свойствами на гранях 3) найти вершину по значению свойства и от нее сделать 10 хопов с фильтром по значению свойства на грани


          1. bm13kk
            08.12.2021 10:47

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

            Например. Чтобы SQL был быстрее обычного чтения с диска, на данные накладывают гигантские ограничения. Они хранятся в столбиках, типы данных вылизаны, есть ключи и индексы и тп и тд. Мы даже придумали дополнительные правила, чтобы держать в голове, а не коде - нормальные формы.


            1. nin-jin
              08.12.2021 12:54

              Почитайте что-нибудь про графовые субд - сильно удивитесь.


              1. Kubera2017
                08.12.2021 13:03

                :))


            1. Kubera2017
              08.12.2021 13:08

              Ну я практик, графовые базы моя специализация. Примеров можно придумать много. Например, граф телефонных номеров и звонков между ними - абоненты вершины, на них свойство "номер", факт звонка грань, на ней counter сколько раз звонили. Каждый звонок может добавлять вершины (если абонента еще нет в графе), создавать грани (если до этого еще не звонили друг другу), и обновлять грани (инкрементить counter для уже существующих). И в реальном времени это все пишем.


              1. bm13kk
                08.12.2021 13:40

                Извините, я с графовыми БД плохо знаком. Что почитать о том как графовые БД хранят данные?

                Потому что я тут вижу типичную SQL: таблица вершин и таблица граней.



                1. Kubera2017
                  08.12.2021 14:22
                  -1

                  ну это самый дубовый подход, скажем так, на каждом шаге обхода графа нужно дергать следующую грань по индексу, т.е. получится ничем не лучше табличных баз. Пока что два академических подхода существует - adjacency list и adjacency matrix. В первом объект вершины хранит на себе указатели на соседей (что я пытаюсь сделать, это подход neo4j https://youtu.be/LSKa3as_S7I?t=635), второй пока что только Редис прорабатывают, там можно почитать https://oss.redis.com/redisgraph/


        1. Kubera2017
          07.12.2021 23:28

          По Лиспу буду рад каким-то пояснениям или ссылочкам, я знаю подход TerminusDB, она на прологе, там скажем так "версионирование" (ничего никогда не удаляется), но это нереально сейчас с нормальным размером графа


          1. bm13kk
            08.12.2021 10:57

            Если я не ошибаюсь, я эту читал https://en.wikipedia.org/wiki/Structure_and_Interpretation_of_Computer_Programs

            Подход у лиспа такой. Верхина владеет всем ниже. Если Вам надо читать - берете вершину и идете вниз. Если надо поддерево добавить в вершину - поддерево никак не меняется, меняется только вершина (в ней ссылка на поддерево). Если надо изменить лист - строите новое дерево.


  1. redsh0927
    30.11.2021 11:04
    +1

    Не дождётесь.


  1. disputant
    30.11.2021 11:47
    +1

    С завидной регулярностью ходят слухи о скорой смерти С/С++...

    Просто есть языки для начинающих, в которых компилятор + машина помощнее служат памперсом для программиста, а есть языки для... (ну, слово "профессионал" страшно не нравится — часто вот тот в памперсах, получая за код деньги, "профессионал", в отличие от знающего и умеющего "любителя") ну, для взрослых дядь...

    Просто не давайте спички детям без присмотра :)


    1. AnthonyMikh
      30.11.2021 20:47

      Практика показывает, что на C не умеет писать никто.


  1. byko3y
    30.11.2021 15:49

    Настоящая история и мотивация создания языка Си практически не отражена, приведена книжно-википедийная версия пересказа кума подруги тёлки брата. Даже языки B и BCPL не упомянуты.

    Абстрактная машина должна была решить две проблемы одновременно

    И создать новую проблему -- проблему "что такое абстрактная машина и как она работает?". Во всем стандарте нет ни малейшего намека, как работает абстрактная машина, но при этом весь стандарт построен вокруг неё, в частности, многие вещи подразумевают некий "доступ". Например, "a = &b->field" -- я делаю "доступ" к b? Отсутствие определения абстрактной машины -- это причина, по которой компиляторы ломают ранее работавшие программы. Отмаза одна и та же -- "стандарт не запрещает". Да, он и не разрешает. Абстрактная машина -- это одно сплошное undefined behaviour.

    Эта свобода интерпретации стандарта привела к тому, что отдельные неадекватные вахтеры из команды разработки GCC забаррикадировались от здравого смысла и отстреливаются лишь дежурными "я так увидел стандарт". Что их фичи превращают написание и отладку программ в кошмар -- их не волнует. Слава богу, что у нас есть ключи "-fwrapv -fno-strict-aliasing"


  1. anonymous
    00.00.0000 00:00


    1. cepera_ang
      30.11.2021 16:08
      +5

      т.к. в коректной программе на Си никогда не может быть неопределённого поведения.

      И поэтому не существует ни одной корректной программы на Си, верно?


  1. ncr
    30.11.2021 16:23
    +8

    Вижу статью с:
    — Кликбейтным заголовком.
    — Кучкой классических примеров UB.
    — Ссылками на людей, не понимающих смысла слова «undefined».

    Ожидаю увидеть в комментариях:
    — Множество экспертов, точно знающих, как на самом деле надо правильно писать компиляторы.
    — Комментаторов, в очередной раз тщетно пытающихся донести до экспертов смысл слова «undefined».

    Так и случилось.


  1. firehacker
    30.11.2021 19:25

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

    А можно выдержку из стандарта, где зафиксирована легитимность такого вывода?


    1. vkni
      30.11.2021 19:44

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


    1. wataru
      30.11.2021 20:22
      +1

      В стандарте написано, что разыменовывание нулевого указателя — UB. Все. После этого компилятор может считать что разыменовываемый указатель никогда не будет нулевым. Если программист не допустил UB и компилятор угадал правильно — программа работает, никаких проблем нет. Если допустил — то компилятор прикрыт бумажкой "при UB я могу хоть диск форматировать".


  1. 4p4
    30.11.2021 22:35

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


  1. j123123
    30.11.2021 22:53
    +1

    Возьмём следующий фрагмент кода на языке Си:

    int x = 1;
    x = x << sizeof(int) * 8;

    Попробуем предположить, какой результат у нас получится.

    Если размер байта больше 8 бит (такое вполне бывает и разрешено стандартом), ничего страшного не произойдет. Если хочется получить UB, лучше использовать макрос CHAR_BIT

    int x = 1;
    x = x << sizeof(int) * CHAR_BIT;

    Кстати, тут можно и unsigned int взять. Если unsigned int 32-битный, сдвиг единицы на 32 тоже будет UB.

    Для своей операционной системы разработчик решил использовать собственную реализацию функции memset. Но он не учёл, что в процессе трансляции компилятор gcc обнаружит в этом коде весьма заманчивую возможность для оптимизации.

    Да, есть такой баг. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888

    Только вы определитесь, вы конкретный компилятор Си ругаете, ли сам Си?

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

    int check_password(const char *pwd)
    {
        char real_pwd[32];
        get_password(real_pwd);
        return !strcmp(pwd, real_pwd);
    }

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

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

    Кстати, по поводу хешей. Взять например SHA256Transform за авторством Aaron Gifford которое присутствует в OpenBSD например http://fxr.watson.org/fxr/source/crypto/sha2.c?v=OPENBSD#L309

    Там есть такая замечательная строчка, как a = b = c = d = e = f = g = h = T1 = 0; , где все эти a b c d ... являются локальными переменными в этой функции. Что вообще автор хотел этим сказать? Видимо он хотел избежать утечки каких-то данных через стек, и даже возможно через регистры. Компилятор это зануление локалок конечно имеет право выкинуть, но ДОПУСТИМ он не выкинет их, и действительно занулит соответствующие регистры и стек. Только в стеке и в регистрах могут быть записаны какие-нибудь промежуточные результаты вычислений, а их как вообще занулить? Писать специализированный компилятор, который все использованные функцией адреса в стеке и все использованные регистры (кроме возвращаемого значения) забивает нулями?


    1. exegete Автор
      01.12.2021 01:04
      +1

      Если размер байта больше 8 бит (такое вполне бывает и разрешено стандартом), ничего страшного не произойдет. Если хочется получить UB, лучше использовать макрос CHAR_BIT

      Да, стандарт действительно не устанавливает верхнюю планку для размера char, однако минимальный размер - это всегда 8 (2.2.4.2 Numerical limits):

      The values given below shall be replaced by constant expressions suitable for use in #if preprocessing directives. Their implementation-defined values shall be equal or greater in magnitude (absolute value) to those shown, with the same sign.

      * maximum number of bits for smallest object that is not a bit-field (byte) CHAR_BIT 8

      Так что использование явной константы 8 в качестве размера для битового сдвига достаточно, чтобы вызвать UB в большинстве случаев, в том числе и при компиляции примера gcc для x86 (что и было продемонстрировано).

      Формально вы, конечно, правы, и чтобы словить UB во всех возможных ситуациях, нужно использовать макрос, но идея тут была в другом. На самом деле пример со сдвигом нужен, чтобы показать, что даже для той архитектуры, для которой известны размер минимально адресуемой ячейки памяти (8 бит) и поведение инструкции битового сдвига, нет гарантии, что будет сгенерирован тот код, который ожидает программист.

      Да, есть такой баг. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888

      Только вы определитесь, вы конкретный компилятор Си ругаете, ли сам Си?

      Спасибо за отличную ссылку, вот этот комментарий порадовал особенно:

      We are not presently experiencing this issue in musl libc, probably because the current C memcpy code is sufficiently overcomplicated to avoid getting detected by the optimizer as memcpy.

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

      Почему вы считаете, что это баг компилятора? Компилятор ведет себя в соответствии со стандартом (4.1.2 Standard headers):

      Each library function is declared in a header, whose contents are made available by the #include preprocessing directive. The header declares a set of related functions, plus any necessary types and additional macros needed to facilitate their use. Each header declares and defines only those identifiers listed in its associated section. All external identifiers declared in any of the headers are reserved, whether or not the associated header is included. All external identifiers that begin with an underscore are reserved. All other identifiers that begin with an underscore and either an upper-case letter or another underscore are reserved. If the program defines an external identifier with the same name as a reserved external identifier, even in a semantically equivalent form, the behavior is undefined.

      Да и на самом деле мы ругаем не вакуумный язык Си, а то, что его концепция и история вводит людей в заблуждение о его сущности. И это заблуждение поддерживалось в том числе комитетом стандартизаторов - для чего было вводить столько UB, перекладывая ответственность на создателей компиляторов, и делать вид, что с Си ничего не произошло? Си, который создал Ритчи, и тот Си, который определен в стандарте - это абсолютно разные языки, только внешне они практически неотличимы. Первый - это язык для конкретной машины PDP-11, второй - абстрагированный язык высокого уровня.

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

      Мы упомянули о том, что пароль (или его часть) может остаться в регистре. На самом деле ответ прост - в рамках самого языка Си эта проблема неразрешима. В тексте стандарта даже, очевидно, слово "stack" не упоминается - об очищении чего тогда вообще может идти речь? Локальные переменные могут находиться где-угодно, и исключительно средствами Си это проконтролировать невозможно. Подробнее об этом написано тут (ссылка также есть в статье):

      http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html

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


      1. j123123
        01.12.2021 02:17

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

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

        Почему вы считаете, что это баг компилятора? Компилятор ведет себя в соответствии со стандартом (4.1.2 Standard headers):

        Потому что см. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888#c12 :

        Now, if this replacement still happens when you compile with -nostdlib, that would be a bug since it becomes legal code in that case.

        Т.е. флаг -nostdlib по-идее должен это разрешать, а он не разрешает. Там советуют использовать опцию -fno-tree-loop-distribute-patterns

        Можно использовать для таких функций attribute ((optimize ("-fno-tree-loop-distribute-patterns")))и тогда должно нормально компилироваться без рекурсий.

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

        Для переносимости и для более агрессивной оптимизации.

        Мы упомянули о том, что пароль (или его часть) может остаться в регистре. На самом деле ответ прост - в рамках самого языка Си эта проблема неразрешима. В тексте стандарта даже, очевидно, слово "stack" не упоминается - об очищении чего тогда вообще может идти речь?

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

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


        1. exegete Автор
          01.12.2021 03:41

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

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

          Потому что см. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888#c12 :

          Now, if this replacement still happens when you compile with -nostdlib, that would be a bug since it becomes legal code in that case.

          Это не баг, -nostdlib работает так, как написано в документации gcc:

          -nostdlib

          Do not use the standard system startup files or libraries when linking. No startup files and only the libraries you specify will be passed to the linker. The compiler may generate calls to memcmp, memset, memcpy and memmove. These entries are usually resolved by entries in libc. These entry points should be supplied through some other mechanism when this option is specified.

          Так или иначе вопрос не в том, как заставить компилятор не делать какие-то оптимизации, а в том, валидны ли эти оптимизации с точки зрения языка. И да, они абсолютно валидны. Единственный момент тут в том, что если вы сообщите компилятору, что у вас freestanding окружение (для gcc нужен флаг -ffreestanding), то тогда все, что связано со стандартной библиотекой, будет зависеть от реализации компилятора:

          In a freestanding environment (in which C program execution may take place without any benefit of an operating system), the name and type of the function called at program startup are implementation-defined. There are otherwise no reserved external identifiers. Any library facilities available to a freestanding program are implementation-defined.

          Но это совсем не означает, что в таком случае компилятор не может самостоятельно добавлять в код вызовы функций стандартной библиотеки. Однако implementation-defined behavior разработчики компилятора уже должны документировать, что они и сделали для упомянутого флага -nostdlib.

          Для переносимости и для более агрессивной оптимизации.

          Ну так и сделали бы новый, прекрасный, переносимый, легко оптимизируемый язык. Зачем было брать прибитый ржавыми гвоздями к архитектуре PDP-11 Си и вводить в заблуждение кучу людей?

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

          Любой, в документации/спецификации/стандарте которого написано, что он на такое способен. И да, скорее всего это будет только ассемблер, но это не такой уж и плохой вариант. Никто не мешает вам линковаться с объектными файлами, написанными на асме. Решение с флагами компилятора тоже неплохое, но только при условии, что вы действительно уверены, что оно сработает как нужно. Но в рамках самого языка Си решить эту проблему невозможно - в этом и был смысл примера, который мы дали в тексте.


          1. j123123
            01.12.2021 16:42
            +1

            Это не баг, -nostdlib работает так, как написано в документации gcc:

            Ну ок, значит не -nostdlib а какой-то другой флаг не сработал. Я просто привел цитату из багзиллы GCC.

            Багом GCC является то, что использование некоторых флагов не позволяет эту оптимизацию отключить. То, что по стандарту (т.е. не используя специальных опций компилятора) нельзя написать свою реализацию memcpy, не является серьезной проблемой языка Си, т.к. во-первых такие функции не надо каждый день писать и во-вторых, это решается специальными флагами компилятора. Серьезных проблем в Си хватает, но это не одна из них

            Ну так и сделали бы новый, прекрасный, переносимый, легко оптимизируемый язык

            Под который нужно было бы переписывать весь старый код? А кто сказал, что такой язык никто не сделал? Вот D например есть. Или Rust.

            Почему не сделали совместимый с Си язык, назвав его по-другому? Тоже сделали, называется он "C++", есть еще "Obj-C". Хотя насчет прекрасности я б тут поспорил.

            Зачем было брать прибитый ржавыми гвоздями к архитектуре PDP-11 Си и вводить в заблуждение кучу людей?

            Потому что под Си написано много кода, и его хотелось бы не переписывать, а дорабатывать. Введение в заблуждение в чем заключается? Си времен K&R был хорошим, а потом стал плохим? Вот например вы ругаете правила сравнения указателей:

            Вот небольшой фрагмент кода, демонстрирующий некорректное с точки зрения стандарта сравнение:

            int *p = malloc(64 * sizeof(int));
            int *q = malloc(64 * sizeof(int));
            if(p < q) /* Undefined behaviour! */
                do_something();
            

            Только вот это было еще в K&R, даже можно найти архивы ньюзгрупп, где обсуждалось еще в 1988 году https://compilers.iecc.com/comparch/article/88-07-002

            >K&R 1, page 98:
            >"But all bets are off if you do arithmetic or comparisons with pointers
            >pointing to different arrays. If you're lucky, you'll get obvious
            >nonsense on all machines. If you're unlucky, your code will work on one
            >machine but collapse mysteriously on another."


            1. exegete Автор
              01.12.2021 20:43
              +2

              Багом GCC является то, что использование некоторых флагов не позволяет эту оптимизацию отключить.

              Как багом может являться документированное поведение компилятора? Ни один из указанных автором поста флагов не гарантирует, что компилятор не будет вставлять вызовы memcpy в код. Даже упомянутый "-fno-tree-loop-distribute-patterns" не позволяет утверждать, что компилятор не добавит вызов memcpy куда-нибудь еще, так как этот флаг лишь отключает конкретную оптимизацию циклов.

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

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

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

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

              Под который нужно было бы переписывать весь старый код?

              Был старый код, а стал неработающий код. Если у комитета была задача сломать как можно больше кода (и старого, и нового), то они ее достигли с большим успехом.

              Потому что под Си написано много кода, и его хотелось бы не переписывать, а дорабатывать.

              Ну правильно, давайте бесконечно дорабатывать работающие программы - делать нам что ли больше нечего?!

              А кто сказал, что такой язык никто не сделал?

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

              Введение в заблуждение в чем заключается? Си времен K&R был хорошим, а потом стал плохим?

              Си, созданный Ритчи, был языком для конкретной машины. Как только он вылез за рамки PDP-11, начались проблемы. Компиляторы Си под новые архитектуры постоянно ломали старый код. А стандарт еще больше подлил масла в огонь - внешне язык практически не изменился, но при этом связь с PDP-11 перестал иметь вообще. Но им было этого мало, и чтобы залатать часть неработающих оптимизаций, они добавили ключевое слово volatile. Без него компиляторы не могли понять, какие обращения в память можно оптимизировать, а какие - нет:

              https://groups.google.com/g/comp.std.c/c/tHvQhiKFtD4/m/zfIgJhbkCXcJ

              Но спустя 30 лет и этот костыль стал практически бесполезен и даже опасен из-за своей крайне туманной семантики:

              https://www.kernel.org/doc/Documentation/process/volatile-considered-harmful.rst

              K&R 1, page 98:

              "But all bets are off if you do arithmetic or comparisons with pointers pointing to different arrays. If you're lucky, you'll get obvious nonsense on all machines. If you're unlucky, your code will work on one machine but collapse mysteriously on another."

              Разница между тем, что написано тут, и что в стандарте очевидна. Программирование на Си до C89 предполагало понимание того, какой код генерирует компилятор под целевую архитектуру. Это было хоть как-то возможно потому, что сами компиляторы были гораздо проще, да и портировать нужно было намного меньше. Но бесконечно так продолжаться просто не могло. Для Си того времени просто немыслима возможность наличия двух указателей с одинаковым содержимым, проверка на равенство для которых возвращает ложь. Разумеется речь идет про плоскую память. А вот для стандартного Си - это вполне нормально, так как подобное сравнение - это неопределенное поведение. И на самом деле еще хорошо, что такое сравнение ложь возвращает, а ведь может еще, если верить стандарту, вам исходный код удалить или монитор сжечь.


              1. ruomserg
                02.12.2021 20:39
                +2

                +1 Мне не нравится определени UB в стандарте, и мне совсем не нравится текущая интерпретация UB современными компиляторостроителями. Понятно, что в стандарт вводили UB для того, чтобы дать возможность компилятору разрешить сложную ситуацию тем способом, который наиболее подходит для конкретной платформы. Почему нельзя было написать вместо UB — «поведение зависит от платформы», я не понимаю. Но они выбрали написать UB, и стало допустимым любое поведение. А потом случилась автоматическая генерация кода и dead code elimitation. И UB внезапно стали все понимать как "… и тогда код можно вообще не генерировать". Для C++ — это понятно, и он без этих оптимизаций не жилец. Но «C» еще можно спасти, чтобы UB перестали быть UB, а стали platform-specific задокументированным поведением.


              1. j123123
                02.12.2021 22:12

                Как багом может являться документированное поведение компилятора?

                Багом это является по причине того, что в багзилле GCC признали, что это баг.

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

                Был старый код, а стал неработающий код. Если у комитета была задача сломать как можно больше кода (и старого, и нового), то они ее достигли с большим успехом.

                Т.е. надо было ничего в Си не менять, а создать новый язык, и было бы всем счастье? Ну допустим не меняли бы они ничего, выпустили бы вместо C89 новый частично совместимый с Си язык, назвали бы его как "Extended-C" например, напихали бы туда этих оптимизаций через неопределеное поведение при стрикт-алиазинге и прочее, на что вы тут жалуетесь, старые программы портировали бы на этот "Extended-C" а сам исконно-посконный Си забросили бы, как забросили языки B и BCPL, и что бы от этого принципиально поменялось?

                Си, созданный Ритчи, был языком для конкретной машины. Как только он вылез за рамки PDP-11, начались проблемы.

                А в каких случаях проблем бы не было? Вот например в паскале у Integer какой размер в байтах? https://archive.org/download/iso-iec-7185-1990-Pascal/iso-iec-7185-1990-Pascal.pdf - попробуйте там что-нибудь найти про это. На 55 странице этого PDF файла есть упоминание про maxint, который (сюрприз!) implementation-defined. Если посмотреть сюда https://wiki.freepascal.org/Integer - тут сказано Typical sizes of integer generally are 16 bit (2 byte), 32 bit (4 byte) or 64 bit (8 byte) - отлично, т.е. если кто-то в своей программе на паскале предполагает, что Integer 32-битный, на паскале с 16-битным Integer его код корректно не заработает. А если бит не 8-битный? Какие компилируемые в машинный код языки программирования, сопоставимые по уровню абстракции с тем же Си или паскалем, могут работать на (т.е. компилироваться под) архитектурах с не 8-битным байтом, чтобы код при этом не нужно было переделывать? Может надо делать отдельный язык для архитектур с 8-битным байтом, отдельный для 9-битного байта, отдельный для 16-битного байта, отдельный язык для two's complement, отдельный для one's complement знаковых чисел, и так под каждую архитектуру делать язык с особым уникальным именем?

                Сейчас уже сами архитектуры подстраивают под языки, например в ARM есть инструкции, добавленные специально для джаваскрипта https://stackoverflow.com/questions/50966676/why-do-arm-chips-have-an-instruction-with-javascript-in-the-name-fjcvtzs

                И специально для Си тоже добавляли инструкции чтобы код нормально работал http://c-faq.com/null/machexamp.html

                The Prime 50 series used segment 07777, offset 0 for the null pointer, at least for PL/I. Later models used segment 0, offset 0 for null pointers in C, necessitating new instructions such as TCNP (Test C Null Pointer), evidently as a sop to [footnote] all the extant poorly-written C code which made incorrect assumptions.


                1. exegete Автор
                  03.12.2021 16:23
                  +2

                  Багом это является по причине того, что в багзилле GCC признали, что это баг.

                  Багом эту ситуацию обозвал Richard Biener, который в конечном итоге просто добавил к флагу -ffreestanding отключение конкретной оптимизации циклов, которая не позволяла реализовать memcpy на языке Си.

                  В том же обсуждении другой разработчик gcc упоминает, что "проблема" остается актуальной до сих пор, так как компилятор продолжает добавлять вызовы memcpy во freestanding окружении, но уже в других местах:

                  Note that the compiler emits calls to memcpy for struct copies anyway, so if there is a problem it is a long-standing one.

                  И это, как уже было отмечено ранее, не противоречит ни документации gcc, ни стандарту Си. То, что реализация компилятора не позволила реализовать на нем memcpy не говорит о том, что gcc - это неправильный компилятор языка Си. Это поведение абсолютно валидно с точки зрения стандарта - никто вам такой функционал предоставлять не обязан.

                  Эту точку зрения подтверждает все тот же Richard Biener:

                  -fno-builtin-XXX does not prevent GCC from emitting calls to XXX. It only makes GCC not assume anything about existing calls to XXX.

                  Так что исправление "бага" - это просто костыль, который позволяет не писать memcpy на языке ассемблера. По сути ничего остального в работе компилятора он не меняет. Т.е. он все еще может теоретически добавить рекурсивный вызов в ваше определение функции memcpy, если вы напишите код определенным образом.

                  Ну допустим не меняли бы они ничего, выпустили бы вместо C89 новый частично совместимый с Си язык

                  Что вы имеете в виду под частично совместимым? C89 - это не частично совместимый с Си язык, а его единственно верный, как уверяет комитет, вариант:

                  The need for a single clearly defined standard had arisen in the C community due to a rapidly expanding use of the C programming language and the variety of differing translator implementations that had been and were being developed. The existence of similar but incompatible implementations was a serious problem for program developers who wished to develop code that would compile and execute as expected in several different environments.

                  Как говорится, благими намерениями вымощена дорога в ад.

                  старые программы портировали бы на этот "Extended-C" а сам исконно-посконный Си забросили бы

                  Разница тут в том, что когда вы переписываете программу на новом языке, вы не ожидаете, что семантика у него будет такая же, как и у старого. Тут же случилось так, что все написанные к 89-ому году работающие программы на Си неожиданно в одну секунду оказались, согласно стандарту, неработающими. Только никто этого не заметил, и проблема как раз в том, что программы людьми действительно не портировались (а этих программ было много - один 4.3BSD чего стоил). Все продолжали писать так, как делали это раньше. Последствия неожиданной метаморфозы языка Си из низкоуровневого (по сути ассемблера) в высокоуровневый язык программирования стали очевидны далеко не сразу. А большинство из них неочевидны до сих пор - сколько UB сейчас хранится в исходных кодах легиона программ - страшно себе представить.

                  Вот например в паскале у Integer какой размер в байтах?

                  Паскаль всегда был и остается языком высокого уровня, поэтому размер типа Integer там не указан намеренно. Причем переполнение числа является, согласно стандарту Паскаля, ошибкой (Error):

                  D.47 6.7.2.2
                  It is an error if an integer operation or function is not performed according to the mathematical rules for integer arithmetic.

                  Причем сам Error стандарт Паскаля определяет гораздо мягче, чем Undefined Behavior у Си. В частности, такие ограничения накладывает документ на обработку Error-ов:

                  treat each violation that is designated an error in at least one of the following ways:

                  1) there shall be a statement in an accompanying document that the error is not reported, and a note referencing each such statement shall appear in a separate section of the accompanying document;
                  2) the processor shall report the error or the possibility of the error during preparation of the program for execution and in the event of such a report shall b e able to continue further processing and shall be able to refuse execution of the program-block;
                  3) the processor shall report the error during execution of the program

                  Т.е., если компилятор Паскаля явно не указал в своей документации иное поведение, он обязан сообщить об ошибке либо во время исполнения программы, либо еще во время ее компиляции.

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

                  Какие компилируемые в машинный код языки программирования, сопоставимые по уровню абстракции с тем же Си или паскалем

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

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


                  1. j123123
                    04.12.2021 02:20
                    +1

                    То, что реализация компилятора не позволила реализовать на нем memcpy не говорит о том, что gcc - это неправильный компилятор языка Си. Это поведение абсолютно валидно с точки зрения стандарта - никто вам такой функционал предоставлять не обязан.

                    Еще раз. Критерием "баг это или не баг" я считаю статус в багзилле GCC. В багзилле GCC это подтвердили как баг - значит это баг GCC. Баги это по-вашему только те случаи, когда некое поведение компилятора не соответствует стандарту?

                    Что вы имеете в виду под частично совместимым? C89 - это не частично совместимый с Си язык, а его единственно верный, как уверяет комитет, вариант

                    То и имею в виду, что допустим вместо C89 выпустили бы "Extended-C", а Си оставили б в состоянии на момет выхода K&R. Это было бы существено лучше, это бы что-то радикально изменило?

                    Не нравится официальный стандарт - изобретите свой стандарт и сделайте там так, как вам нравится. Есть достаточно много ответвлений от языка Си.

                    Тут же случилось так, что все написанные к 89-ому году работающие программы на Си неожиданно в одну секунду оказались, согласно стандарту, неработающими.

                    Совсем-совсем все, т.е. 100% программ вдруг стали неработающими? Неработающими с точки зрения стандарта C89? А с точки зрения K&R они точно все были работающими? В K&R например написано, что бит в байте может быть 8, а может быть больше чем 8 - если какая-то программа полагается на то, что байт 8-битный, она может не заработать на архитектуре с 9-битным байтом. Или наоборот, если предполагается, что байт 9-битный, программа с 8-битным байтом может не работать корректно.

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

                    А какое это имеет отношение к "уровневости"? В Си кстати тоже размер int не указан, получается что он в вашей классификации язык высокого уровня? Вот в Java размер типа int как раз указан, выходит что Java это язык низкого уровня? https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.2 : The integral types are byte, short, int, and long, whose values are 8-bit, 16-bit, 32-bit and 64-bit signed two's-complement integers, respectively, and char, whose values are 16-bit unsigned integers representing UTF-16 code units (§3.1).

                    Причем сам Error стандарт Паскаля определяет гораздо мягче, чем Undefined Behavior у Си.

                    Это не отменяет того факта, что программы на языке Паскаль могут перестать работать из-за того, что тип Integer вдруг стал другим, и не вмещает в себя нужный диапазон. Так что же получается, "Паскаль должен умереть"?

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

                    Где в стандарте языка Си написано, что Си это язык низкого уровня?

                    Где в стандартах Си или Паскаля указано, что они компилируется в машинные коды?

                    Нигде. Вполне можно интерпретировать Си, впрочем как и Паскаль. Впрочем, как и ассемблер x86. Я о том, что Си и Паскаль можно компилировать в достаточно эффективный машинный код для фон-неймановских архитектур.

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

                    А для языка низкого уровня не это безразлично? Писать интерпретаторы ассемблера незаконно? Что значит "для языка безразлично", у него какое-то свое мнение есть? Что вообще такое язык высокого и язык низкого уровня? Я язык Си не считаю языком низкого уровня, язык низкого уровня для меня это ассемблер, машинный код.


                    1. Cerberuser
                      04.12.2021 11:13

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

                      Но они это сделают поняным и задокументированным образом, а не "как оптимизатору в голову взбредёт", по идее, в этом разница?


                    1. exegete Автор
                      04.12.2021 12:52
                      +1

                      Еще раз. Критерием "баг это или не баг" я считаю статус в багзилле GCC.

                      И что дальше? В самом начале вы писали:

                      Только вы определитесь, вы конкретный компилятор Си ругаете, ли сам Си?

                      Я продемонстрировал вам, что поведение компилятора до исправления "бага" и после не противоречит стандарту. Более того, компилятор продолжает порождать вызовы memcmp/memcpy/memset/memmove. По этой причине пример в статье вполне закономерный, и подобная оптимизация могла произойти в любом другом компиляторе.

                      Не нравится официальный стандарт - изобретите свой стандарт и сделайте там так, как вам нравится.

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

                      Совсем-совсем все, т.е. 100% программ вдруг стали неработающими? Неработающими с точки зрения стандарта C89? А с точки зрения K&R они точно все были работающими?

                      Хорошо, раз уж вы обратились к истокам, то вот вам, к примеру, мануал по языку Си, написанный Ритчи до того, как был принят стандарт:

                      https://www.bell-labs.com/usr/dmr/www/cman.pdf

                      Там, к примеру, рукой создателя языка написано следующее:

                      C supports four fundamental types of objects: characters, integers, single-, and double-precision floating-point numbers.

                      Characters (declared, and hereinafter called, char) are chosen from the ASCII set; they occupy the rightmost seven bits of an 8-bit byte. It is also possible to interpret chars as signed, 2’s complement 8-bit numbers.

                      Integers (int) are represented in 16-bit 2’s complement notation

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

                      Вот в Java размер типа int как раз указан, выходит что Java это язык низкого уровня?

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

                      Это не отменяет того факта, что программы на языке Паскаль могут перестать работать из-за того, что тип Integer вдруг стал другим, и не вмещает в себя нужный диапазон. Так что же получается, "Паскаль должен умереть"?

                      Во-первых, как я уже сказал, Паскаль - это не Си, и такое поведение в нем всегда было определено как Error. Во-вторых, прочитайте внимательно тот текст стандарта Паскаля, который я процитировал. Описанное там поведение сильно отличается от того, что говорит о знаковом переполнении стандарт Си. Если реализация Паскаля соответствует пунктам 2 или 3, то тогда ваша программа в худшем случае аварийно завершит исполнение, в то время как аналогичная программа на Си может, согласно стандарту, сделать что-угодно. В случае, если реализация следует пункту 1, то тогда вам нужно обращаться к документации компилятора, где это поведение должно быть четко определено. Опять же, стандарт Си такое требование не накладывает.

                      Я о том, что Си и Паскаль можно компилировать в достаточно эффективный машинный код для фон-неймановских архитектур.

                      Что такое достаточно эффективный код для фоннеймановских архитектур? И при чем тут упомянутые вами в предыдущем сообщении абстракции?

                      Что вообще такое язык высокого и язык низкого уровня?

                      Хорошо, я уточню используемую мною терминологию.

                      Когда я говорю, что язык Си - это язык высокого уровня, я имею в виду то, что он абстрагирован от конкретной реализации компилятора и компьютерной архитектуры. Как говорит сам стандарт, его описание содержит "unambiguous and machine-independent definition of the language C".

                      Когда же я говорю, что Си - это язык низкого уровня, я подразумеваю, что его описание напрямую зависит от реализации компилятора и особенностей компьютера PDP-11. Как пишет сам Ритчи в упомянутом ранее мануале:

                      This paper is a manual only for the C language itself as implemented on the PDP-11.

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

                      Писать интерпретаторы ассемблера незаконно?

                      Этого я тоже не говорил. Я сказал только то, что возможность трансляции текста языка высокого уровня в машинный код (или какой-либо текст на другом языке) не имеет никакого отношения к самому языку.

                      Я язык Си не считаю языком низкого уровня, язык низкого уровня для меня это ассемблер, машинный код.

                      Вы, к вашему счастью, не считаете, а огромное количество других программистов (в том числе и создатель языка, если верить написанному им же мануалу) считают или считали иначе. Статья написана именно для таких людей с целью предостеречь их от ошибок. Если мыслить о языке Си в терминах его реализации, предполагать, какой код сгенерирует компилятор, то тогда он действительно становится неотличим по семантике от того же ассемблера. При этом до 89-ого года писать на нем код каким-либо другим образом было невозможно - четкого, абстрагированного от конкретной архитектуры описания не существовало. За 20 лет жизни языка появилось огромное количество программ, которые в момент выхода стандарта оказались формально нерабочими. Причем из-за особенностей текста стандарта (в том числе благодаря постоянному использованию неопределенного поведения) это произошло практически незаметно. Разгребать последствия этих решений мы будем еще очень долго.


                      1. j123123
                        06.12.2021 16:19

                        Хорошо, раз уж вы обратились к истокам, то вот вам, к примеру, мануал по языку Си, написанный Ритчи до того, как был принят стандарт:

                        https://www.bell-labs.com/usr/dmr/www/cman.pdf

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

                        Это очень похоже на текст стандарта?

                        Может и похоже чем-то, но нет, текстом стандарта это не является. Например вот фрагмент

                        6.1 Characters and integers

                        A char object may be used anywhere an int may be. In all cases the char is converted to an int by propagating its sign through the upper 8 bits of the resultant integer. This is consistent with the two’s complement representation used for both characters and integers. (However, the sign-propagation feature disappears in other implementations.)

                        В каких-то реализациях нет sign propagation при переводе из char в int? Очень интересный "стандарт". А про поведении при делении на ноль там что-нибудь вообще сказано в этом "стандарте"? Вот в K&R первой редакции https://ia801303.us.archive.org/1/items/TheCProgrammingLanguageFirstEdition/The%20C%20Programming%20Language%20First%20Edition%20%5BUA-07%5D.pdf про это сказано вот что:

                        The handling of overflow and divide check in expression evaluation is machine-dependent. All existing implementations of C ignore integer overflows; treatment of division by 0, and all floating-point exceptions, varies between machines, and is usually adjustable by a library function.

                        Machine-dependend уже предполагает, что есть не только лишь PDP-11. В K&R даже в первой редакции написано, что число бит в байте может быть разным, и тут уже явно упомянут не только PDP-11:

                        Во-первых, как я уже сказал, Паскаль - это не Си, и такое поведение в нем всегда было определено как Error. Во-вторых, прочитайте внимательно тот текст стандарта Паскаля, который я процитировал. Описанное там поведение сильно отличается от того, что говорит о знаковом переполнении стандарт Си. Если реализация Паскаля соответствует пунктам 2 или 3, то тогда ваша программа в худшем случае аварийно завершит исполнение, в то время как аналогичная программа на Си может, согласно стандарту, сделать что-угодно.

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

                        Что такое достаточно эффективный код для фоннеймановских архитектур? И при чем тут упомянутые вами в предыдущем сообщении абстракции?

                        Что касается Си и Паскаля, то они достаточно легко ложится на фон-неймановские архитектуры, можно код функций перевести в код на ассемблере, при вызове функций, адрес возврата (место, откуда функция была вызвана) записывается в стек. Функции в процессе работы выделяют себе место в стеке под локальные переменные, есть передача аргументов в эти функции, для этого есть определенные соглашения вызовов, в адресном пространстве процесса еще есть область для глобальных и статических переменных. И эти языки явно делали с оглядкой на то, чтобы их было удобно компилировать. А некоторые языки на это так просто не натягиваются, например Prolog. И язык Prolog работает на другом уровне абстракций. Так понятней?

                        Когда же я говорю, что Си - это язык низкого уровня, я подразумеваю, что его описание напрямую зависит от реализации компилятора и особенностей компьютера PDP-11.

                        Его еще в первой редакции "The C programming language" в 1978 году начали от PDP-11 отвязывать, разрешив разное количество бит под байт, и упомянув архитектуры, отличные от PDP-11. Уже тогда Си должен был умереть?


    1. Amomum
      01.12.2021 01:37
      +3

       И кстати пароли в открыдом виде не хранят, а хранят их хэши.

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


  1. Pasha4ur
    01.12.2021 15:38

    Здравствуйте

    Не вдохновляющая статья для новичков. Хоть и полезная.

    Неужели, если делать все по новым стандартам, то будут такие же косяки? Что делать новичкам?

    Смотрю курсы для новичков по С для STM32 на Udemy, потому что Ардуино без отладки очень неудобный, а тут такое ) Хочу потом еще попробовать Unreal Engine c С++ для визуализаций.

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

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

    П.С. Кстати, в курсах для новичков и даже в начальных 2 томах книг Столярова о входе в программирование (на 3-ем томе с сокетами ос и сети я уже отложил это чтиво на потом, потому что куча теории без практики не интересна) про такие проблемы не рассказывают.


    1. Pasha4ur
      01.12.2021 15:46

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


      1. cepera_ang
        01.12.2021 16:54

        Модные стильные молодежные TypeScript, Python, Go, Swift, Kotlin, Rust — не подходят?


        1. Pasha4ur
          01.12.2021 17:06

          Смотрел интервью по питон у айтибороды. Говорили, что медленный и с многопотоком беда.

          Я для распберри пи немного пробовал его. Не очень зашел синтаксис. Очень не хватает классического for(;;), i++ и т п.

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

          Игровые движки что-то тоже пишут на С++ и C#.

          Хотя в планах питон есть. В тех же 3д редакторах много чего пишется именно на нем.

          Про тайпскрипт, гоу и раст ничего не знаю. Почитаю. Спасибо за совет.


          1. cepera_ang
            01.12.2021 17:33
            +2

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


            Тайпскрипт — это для веба, JS как его изначально стоило сделать. Гоу — для сетевых сервисов хорош. Раст — для "системного" программирования, но на самом деле вполне взлетает во многих областях потихоньку.


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


            Вообще, если нет проблем с английским, то я бы с какого-нибудь https://teachyourselfcs.com/ (перевод самого гайда) начал разбираться, а не с того, что на ютубе рассказывает какая-то борода.


            1. Pasha4ur
              02.12.2021 02:55

              Спасибо.

              "Ну и у вас немного каша в голове от всех этих "мнений экспертов"

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

              "Вообще, если нет проблем с английским, то я бы с какого-нибудь https://teachyourselfcs.com/ (перевод самого гайда) начал разбираться, а не с того, что на ютубе рассказывает какая-то борода."

              Я на Udemy смотрю курсы по STM32 на англ от индуса. Все понятно.

              Также читал Столярова (2 тома из первого издания прочитал полностью). Тоже все было понятно. Только раздел с кодом по Ассемблеру я пролистал, потому что это было слишком жестко.


  1. samoreklam
    05.12.2021 08:52

    int *p = malloc(64 * sizeof(int));

    int *q = malloc(64 * sizeof(int));

    if(p < q) /* Undefined behaviour! */ do_something();

    Меня интересует этот случай.
    Если p и q преобразовать в size_t после получения памяти, и делать сравнение. То оно корректно будет? (у меня в коде, есть такая же конструкция, где я определяю смещение данных. И по всем тестам, какие делал, всё работает правильно.) Может ли случиться "аномалия" при таком определении смещения данных?


    1. mayorovp
      05.12.2021 10:46
      +1

      Да, может, если модель памяти отличается от плоской (в таком случае ваш указатель внутрь size_t просто не влезет). Если уж приводить к числу — надо приводить к uintptr_t (но его на платформе может и не быть).