Язык Си - один из наиболее влиятельных языков программирования за всю историю. Он стал незаменимым инструментом разработки операционных систем, сместив с этого пьедестала языки ассемблера. Изучение Си обязательно для любого уважающего себя программиста. Этот язык любим за свою внешнюю простоту и ненавидим за беспощадность к ошибкам. Благодаря ему у нас есть ядро 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)
anonymous
00.00.0000 00:00aversey
29.11.2021 14:58+12Не совсем. Дело в том что с точки зрения стандарта есть вещи, которые вы видимо имеете в виду под выстрелом в ногу -- провоцирующие UB. Когда разработчики компиляторов дошли до того, что стали трактовать UB как запрещёнку, они начали ломать существующий код, который был написан не столько с опорой на стандарт, сколько на по факту наблюдаемое поведение -- и такого кода весьма много. В общем-то правы тут разработчики компиляторов, вот только жить от этого не легче -- и вся эта ситуация в целом показывает, чем плох Си -- это высокоуровневый язык, но мимикрирующий под низкий уровень, причём не самым умелым образом, ведь сложился он больше исторически (читайте: слеплен на коленке и допилен по ходу дела). При этом если писать с использованием реально низкоуровневых вещей, в отрыве от стандарта, разработчики компиляторов имеют полное право сломать такой код при очередном обновлении (естественно можно применить ассемблерные вставки -- но тогда и пишем мы не на Си, а на ассемблере). Надеюсь теперь стало понятней. =)
ibrin
29.11.2021 15:17+3Я понял как решить проблему с UB для знакового! Надо внести в стандарт поведение при переполнении и UB исчезнет, переполение станет предсказуемым!
netch80
29.11.2021 15:34+1И какое именно поведение будем вносить?
То, что для беззнаковых производится _молчаливое_ усечение (wrapping, truncating) до младших бит (и это жёстко в стандарте) — это тоже не лучший вариант. Да, проверку на переполнение в беззнаковых операциях делать чуть легче, но всё равно про неё надо явно помнить.
Возможности компиляторов позволяют сейчас управлять желаемым поведением: где ставить проверки, где усечение, а где, если автор кода уверен, и полагание на отсутствие переполнения, как сейчас для знаковых. Нужна только общая воля.BigBeaver
30.11.2021 11:25Честно говоря, проблема в принципе не очень понятна.
производится _молчаливое_ усечение (wrapping, truncating) до младших бит (и это жёстко в стандарте) — это тоже не лучший вариант
Это как раз хорошо и логично и часто используется на малопроизводительных железках с высокими требованиями к реалтайму (экономим память и такты на обработку счетчиков в циклических процессах типа генераторов ШИМ, сигнала развертки и тд). При чем, дело тут больше даже не в стандарте, а в том, что оно почти всегда так работает на уровне процессора (если не обрабатывать флаг переноса, если он есть вообще на платформе). То есть скорее просто протечка абстракций.
Разумеется, при это иподразумевается, что разработчик знает платформу, под которую пишет.netch80
30.11.2021 11:37> То есть скорее просто протечка абстракций.
Так в том и дело, что абстракции нижнего уровня становятся неадекватными и вредными на верхних уровнях.
В идеале, конечно, надо было бы иметь какой-то «безразмерный» int, который везде, где компилятор способен опознать реальный диапазон значений, сокращается до удобного машинного типа. Но если мы не можем себе это позволить, что делать в случае переполнения, то есть несоответсвии сгенерированного результата ожидаемому? Как и при любой ошибке программирования, jIMHO надо генерировать ошибку явно и вышибать выполнение (или просто ставить флаг, если явно попросили, или механизм немедленной реакции не работает).
(А если компилятор уверен, что переполнения не будет — он выкинет проверку, это законно и желательно.)
Ну а «на малопроизводительных железках с высокими требованиями к реалтайму» всё равно особая среда, которую можно явно пометить.BigBeaver
30.11.2021 12:48В целом, я согласен с каждым пунктом.
Но с другой стороны, мне кажется, что это решается некими coding policy. Либо можно вообще не писать «верхние» уровни на си.
exegete Автор
29.11.2021 15:06+2Да, по сути верно. Единственная загвоздка тут в том, что знаковое переполнение, ровно как и многие другие случаи неопределенного поведения, большинством программистов считалось правомерным. В этой позиции есть смысл, если думать о Си как языке низкого уровня. В действительности это не так. Отсюда и огромное количество сломанного кода, и требование откатить изменения компилятора.
vsb
29.11.2021 17:18+7В Java знаковое переполнение это определённое поведение. В C - неопределённое. Значит ли это по-вашему, что в этом аспекте язык C является языком более высокого уровня, нежели Java? На мой взгляд всё с точностью до наоборот. Язык Java даёт гарантии программистам, пусть даже за счёт теоретической производительности на каких-нибудь мифических платформах, где это знаковое переполнение придётся имитировать. Язык C этих гарантий не даёт как раз для того, чтобы иметь возможность генерировать более производительный код на этих самых платформах с нетрадиционным представлением знаковых чисел.
netch80
29.11.2021 17:21+1> чтобы иметь возможность генерировать более производительный код на этих самых платформах с нетрадиционным представлением знаковых чисел.
Этой проблемы никогда не было и сейчас нет: оптимизации используются потому, что нашлась лазейка их разрешить, а не ради мифических странных платформ.
Отсутствие этих оптимизаций для операций с беззнаковыми это чётко показывает.
exegete Автор
29.11.2021 23:41Я этого не говорил да и полагаю, что они оба просто высокоуровневые. Ни один из них не более или не менее высокоуровневый, чем другой, так как нормальных критериев я придумать не могу. Да и какой смысл? Вообще я имел в виду немного другое - большое количество программистов предполагают, что операции в Си будут имеют ровно такое же поведение, как и соответствующие им инструкции на целевой архитектуре. Т.е. если вы скомпилировали код под x86, то битовый сдвиг на размер переменной должен оставить ее нетронутой. В теории и на практике это так не работает. Поэтому Си - это не язык низкого уровня. То, как в стандарте языка определяется битовый сдвиг или знаковое переполнение не влияет на то, станет он от этого более высокого или низкого уровня (если только прямо не сказано, какую инструкцию должен использовать компилятор при трансляции). Да, неопределенное поведение позволяет совершать более агрессивные оптимизации, но всему есть предел. Си просто переполнен UB, приличная часть которого вообще имеет мало смысла. Конкретно проверять знаковое переполнение перед каждой потенциально опасной операцией гораздо неудобнее, чем это делать после.
paluke
30.11.2021 08:39А как переносимо проверять переполнение после? Например, если на какой-то платформе переполнение вызывает прерывание и аварийное завершение программы?
В каком-то языке (не помню каком) так и было определено поведение при переполнении, и на x86 компилятор инструкции into вставлял после арифметических операций.netch80
30.11.2021 09:21+1> В каком-то языке (не помню каком) так и было определено поведение при переполнении, и на x86 компилятор инструкции into вставлял после арифметических операций.
TurboPascal при соответствующем режиме компиляции (директива компилятора {$Q+}). Можно было выключать в определённых местах.
exegete Автор
30.11.2021 17:15-1Если бы в стандарте языка было указано, что вся целочисленная арифметика явным образом использует дополнительный код, то для тех архитектур, которые ее не поддерживают, пришлось бы имитировать такое поведение. Впрочем, тут нет ничего чрезвычайно страшного - компиляторам языка Си и без этого приходится программно реализовывать те операции, которые целевые архитектуры также не поддерживают. Так, например, для тех машин, которые не содержат инструкций для целочисленного деления и не только, gcc использует специальную библиотеку libgcc. Подробнее можно почитать здесь:
vkni
30.11.2021 04:49+1Есть и противоположная точка зрения - https://dreamsongs.com/WorseIsBetter.html
netch80
29.11.2021 15:06+17> разработчики научили компилятор предотвращать выстрелы в ногу и убрали из него саму возможность выстрелить себе в ногу
Нет, всё строго наоборот, насколько я понял вашу метафору.
Пользователи хотят иметь возможность сделать предохранитель к пистолету, потому что контролировать все действия и все ситуации невозможно — чтобы он не выстрелил, например, при падении с лошади. (Самые продвинутые согласны на убирание предохранителя, но только по своему явному желанию — заменить стандартизованной заглушкой.)
Разработчики компилятора говорят, что предохранитель утяжеляет конструкцию, и если пользователь не может предусмотреть все ситуации, то он сам виноват, а падать с лошади не надо. (Скрытым намёком идёт, что кто хочет безопасности — пусть берёт пистолеты от Ada.)
В результате пистолет стреляет даже когда лошадь просто пугается лая собаки из-за забора и делает рывок.
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)
netch80
30.11.2021 09:55+4> Вообще-то, возможность выстрелить себе в ногу лежит в основе философии Си и вообще Unix.
Возможность намеренно выстрелить себе в ногу. А не неявно и случайно. Это принципиальная разница.
Вы можете написать rm -rf / и оно удалит все файлы (если от рута). Но надо было его вызвать с -r и -f. Просто «rm /» такого не даёт, и с какого-то раннего момента по одной из этих опций — тоже (причём Bell V6 давало это по rm -r, а System III и ранние BSD, насколько помню, уже нет). «rmdir» тоже, оно удаляет только пустой каталог.
И это принципиально. Обсуждаемые тут фишки это неожиданный выстрел в ногу от действий, которые такого не предполагали.asor
30.11.2021 17:10+2Я так понимаю, неожиданный выстрел в ногу получился не от того, что Си допускает неопределённое поведение, о котором все кому надо давно знают, а от того, что конкретный мейнтейнер конкретного компилятора начал усердно реализовывать стандарты. Правильно Линус сказал: когда текст стандарта противоречит реальности - он является обычным куском туалетной бумаги. Стандарты должны помогать, а не мешать. Кстати, это происходит в естественных языках: когда неправильное употребление становится всеобщим, оно становится стандартом :)
demp
30.11.2021 22:07Мне вот интересно стало, если Линусу не нравится реализация GCC, то где можно увидеть ядро, собранное альтернативным компилятором? И легко ли это сделать в принципе?
Проект, на котором я сейчас работаю, успешно собирается на С++ компиляторах от 3-х вендоров. Этот проект бесконечно мал, по сравнению с ядром Линукса, но и ресурсы на его разработку также бесконечно малы.Вопрос: если новый компилятор GCC так не устраивает Торвальдса, то почему он ест этот кактус?
mpa4b
30.11.2021 22:28+1Ну вообще-то прямо сейчас ведутся работы для возможности компиляции ядра линукса также и на clang.
asor
01.12.2021 00:35+1Не то что "ведутся работы для возможности", а Андроид собирают clang-ом! https://source.android.com/setup/build/building-kernels#customize-build
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 года!
demp
01.12.2021 15:53Да, действительно, только что clang-13 без проблем собрал ядро 5.15.6
Видимо clang стал еще более совместим с gcc и/или из ядра таки выпилили gcc-only код.
PsyHaSTe
01.12.2021 03:38Просто это хреновая философия. У вас же перила на лестнице например стоят и двери у лифта закрываются — зачем? Нормальный человек ведь в шахту не упадет. Надо было специально подойти к двери (-r) и шагнуть вперед (-f), любое одно действие из этих не дают такого результата.
Кому от этого легче только.
netch80
01.12.2021 10:34+1> Нормальный человек ведь в шахту не упадет. Надо было специально подойти к двери (-r) и шагнуть вперед (-f)
Нет, -r это означает перелезть через перила, если они есть, а -f — проигнорировать отсутствие лифта. Такая аналогия сильно точнее вашей (полного соответствия, естественно, не будет, но и не требуется).
> Просто это хреновая философия.
При таких интерпретациях — да.
> Кому от этого легче только.
Тому, кто таки обращает внимание на то, что делает.
Сейчас вон мода в UX отменять запросы подтверждений — мол, их никто не читает и всегда жмёт «да»… но мне они реально помогают, иногда остановиться «перед краем» как раз подумав, что тут может быть что-то не так.PsyHaSTe
01.12.2021 13:49Нет, -r это означает перелезть через перила, если они есть, а -f — проигнорировать отсутствие лифта. Такая аналогия сильно точнее вашей (полного соответствия, естественно, не будет, но и не требуется).
То есть -r это что-то плохое чем никто не пользуется? Или это нормальный флаг без которого папочку не удалить если в ней что-то есть?
В реальности же на щитке кроме "не влезай убьет" ещё всегда замок висит. И если человек влез и погиб то виноват будет не он, а тот кто замок сломал. У вас же почему-то "ты че дурак не прочитал что написано" — ну ок
netch80
01.12.2021 14:25> То есть -r это что-то плохое чем никто не пользуется?
Это флаг, который явно говорит «удаляй каталог несмотря на наличие содержимого», и если он указан — то это значит преодолеть соответствующую защиту.
> Или это нормальный флаг без которого папочку не удалить если в ней что-то есть?
Если вы уверены, что надо удалить с содержимым — да. Но надо вначале стать уверенным (или слишком уверенным).
Для удаления пустого каталога отдельно есть rmdir, и я его регулярно зову в случае, если хочу получить эффект «непустое не удалять!»
> И если человек влез и погиб то виноват будет не он, а тот кто замок сломал.
В этой аналогии, как вы представляете себе, чтобы один сломал замок, а другой влез?
Админ ходит по системам и по ночам пишет `alias rm='rm -rf'`, или как?
Я, наоборот, часто вижу `alias rm='rm -i'` как защитное средство, и поддерживаю (в интерактивном режиме).
> У вас же почему-то «ты че дурак не прочитал что написано» — ну ок
И опять же хромая аналогия. Пусть есть на двери обычная ручка, которая чтобы войти (это как вообще вызвать rm), а есть особая, которую ещё надо нажать несмотря на красный цвет и предупреждение. Кто захочет это делать просто так? Ну да, может, 0.1% захочет. Ну так и файлы себе постоянно удаляют даже с предупреждением.
Цель всех этих средств — не тотальная защита от всей глупости, это невозможно — полный дурак или самоубийца всегда найдёт метод — а защитить от ненамеренных ошибок. А для них опций типа -r, -f достаточно (при прочих равных).
anonymous
00.00.0000 00:00Nehc
30.11.2021 10:33+9Нет, не правильно.
Суд по листингу, который привел felix-gcc, раньше assert(a + 100 > a) прерывало программу в случае переполнения (что в общем довольно логично — отрицательное число явно не больше положительного), а после очередного обновления — перестало… Типа на основании того, что такое поведение является «неопределенным», а потому — нарушением стандарта и не хрен такой код выводить в прод… ;)
В вашей аналогии люди знали, что можно случайно выстрелить в ногу, сооружали кастомные предохранители, но в очередной версиикольтакомпилятора эти предохранители перестали работать… НА основании того, что в инструкции по эксплуатации черном по белому написано: «ЗАПРЕЩЕНО стрелять в ногу!»
norn
01.12.2021 05:29-1Простите, что пользуюсь Вашим комментарием, чтобы выразить своё мнение. В своё оправдание скажу, что темы схожи. Если я правильно понял статью, Си должен умереть потому что:
стандарт языка - фуфло;
разработчик (ОДИН!) средства разработки (ОДНОГО!) этого не понимает и отказывается использовать СУЩЕСТВУЮЩУЮ ОПЦИЮ по умолчанию;
в качестве замены, как системного языка, появился юный Rust.
Я всё верно понял? Т.е., вопрос не к выразительным средствам языка, не к тому, что символ "*" в Си без контекста не понять (что есть правда)?
Я не защищаю Си и не обвиняю Rust. Я лишь хочу указать на сомнительность аргументации и использование цитат "больших программистов" в своих целях. Как пример последнего, отсылаю к цитате из переписки Торвальдса, который говорит (ИМХО) верно: если документ мешает писать, то к чёрту документ! Из цитаты Торвальдса "Это то, почему мы используем "-fwrapv", "-fno-strict-aliasing" и другие флаги." я делаю вывод, что у него претензии-таки не к Си. Хао! А теперь минусуем. :)
mayorovp
29.11.2021 14:43+2В примере с check_password есть ошибка: поскольку буфер pwd — внешний, выкидывать memset для него компилятор не может (по крайней мере, до тех пор пока не заинлайнит тело функции на уровень выше). Да и изменение памяти по указателю на константу является ошибкой само по себе, независимо от оптимизаций.
Вот какой вызов на самом деле не помешал бы и какой компилятор имеет полное право выкинуть — это очистка буфера real_pwd.
exegete Автор
29.11.2021 14:47+2Да, вы совершенно верно отметили, это опечатка. Имелся в виду real_pwd. Спасибо большое, исправлено!
Stronix
29.11.2021 14:43+1int 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;
aversey
29.11.2021 14:45Бывает по-разному, можно написать -1, можно +1, у меня при использовании clang работает один вариант, а при gcc -- другой.
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 /*
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)
exegete Автор
30.11.2021 17:46В тексте речь шла про оптимизации компилятора gcc. Второй ваш результат как раз и совпадает с тем, что я сказал выше - оба сравнения возвращают ложь, хотя как минимум два указателя имеют абсолютно одинаковое содержимое.
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).aversey
29.11.2021 15:17+7Спасибо!
Отчасти это и правда решение -- насколько знаю Линукс это активно использует, обвешивая весь свой код сложной системой сборки, где указывается какие правила где применить.
Из проблем тут могу назвать то, что всё же набор всех возможных правил оптимизаций уже невероятно увесистый, и он только продолжает расти -- поэтому хотя в стандарт их включить и можно, но его придётся постоянно обновлять, дописывая новые появившиеся оптимизации. А это в свою очередь значит, что оптимизирующие компиляторы (gcc и clang в том числе, а они сейчас самые популярные), в рамках которых новые оптимизации будут разрабатываться, будут несовместимы со стандартом большую часть времени.
Более того, думаю критически неудобно, что по сути в таком случае мы мыслим на многих языках сразу -- не считая необходимости постоянно осекаться и спрашивать "а включил ли я все необходимые свойства сборки?". Кажется что ассемблер в связке с ещё одним языком тут будет банально проще.
Ну и конечно "должен умереть" это преувеличение -- никому он ничего не должен, на нём вполне можно писать рабочий код, а если бы мог стать лучше -- это было бы прекрасно. Но кажется что этот язык по сути свернул куда-то не туда и шансов на улучшение у него мало. Но всё может быть, успехов вам! =)
netch80
29.11.2021 15:45+2> что всё же набор всех возможных правил оптимизаций уже невероятно увесистый, и он только продолжает расти
Не нужно такое делать на все оптимизации, мне кажется. Нужно на основные (все — тоже вряд ли получится) случаи UdB в стандарте, которые могут возникать именно из-за ограничений человеческого разума. Два первых кандидата — как раз переполнения и алиасинг. Разыменование пустого указателя — возможно, следующее, но его статический анализатор способен (обычно) опознать анализом кода (хотя был пример неожиданного для человека проявления, сейчас не найду ссылки). В общем случае, да, пересмотреть набор всех случаев (в C++ их несколько сотен, в C должно быть поменьше).
> не считая необходимости постоянно осекаться и спрашивать «а включил ли я все необходимые свойства сборки?»
Полиси кода со стандартной шапкой в каждом исходном файле решает это. Потом — и умолчания компилятора (хотя на это вряд ли пойдут).
> Но кажется что этот язык по сути свернул куда-то не туда и шансов на улучшение у него мало. Но всё может быть, успехов вам! =)
Вряд ли я лично буду этим заниматься, но всё равно спасибо :)
0xd34df00d
29.11.2021 19:23+7на нём вполне можно писать рабочий код
Только очень сложно. Локально работающий - да. Код, которому можно доверять - неа.
exegete Автор
29.11.2021 15:41+7Спасибо большое за ссылки и комментарий! К сожалению, в текст одной статьи невозможно уместить все сразу, тем более, что бездна UB неисчерпаема. Идея была в том, чтобы провести неискушенного в тонкостях стандарта читателя от достаточно тривиальных ошибок до гораздо более изощренных и опасных случаев неопределенного поведения. Безусловно, говоря о том, что Си должен умереть, я сам не надеюсь на то, что это произойдет. Смысл в том, что вокруг этого языка существует огромное количество серьезных заблуждений, и вызваны они отнюдь не только непрофессионализмом тех, кто на нем пишет. И учитывая историю Си, и то, как написан стандарт, неудивительно, что ошибки неопределенного поведения стали обыденностью. Я сам долгое время считал, что Си - это своего рода кроссплатформенный ассемблер. Потому я полагаю, что концепция языка, который создает столь опасную иллюзию , является по меньшей мере неудачной. И в том случае, если Си остается с нами надолго (а у меня нет оснований считать иначе), очень важно, чтобы программисты четко осознавали то, какой именно инструмент они используют.
0xd34df00d
29.11.2021 19:21+5Но это очевидным образом не уменьшает сложность языка, так как новый С с прагмами содержит старый С как строгое подмножество. Проще ли на нем писать? ХЗ, даже с прагмами в стандарте нужно учитывать возможные UB.
Короче, я пессимистичнее смотрю.
permeakra
30.11.2021 16:03+7Я вот считаю, что тут нужно призывать именно к смерти Си. Для низкоуровневого языка он делает слишком много предположений о том, как нам жить. А для современного высокоуровневого языка он не дает достаточной изоляции от платформы.
sys_adm1n
29.11.2021 15:13+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, на уровне ассемблерной вставки в код. Он не может быть пропущен стандартом на уровне идеи. Программисту, стоит указать, что этот код оптимизировать не нужно, и бац проблемы не существует.
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) опции компиляторо-зависимы и присутствуют не везде.0xd34df00d
29.11.2021 19:26+6Но нормальная реализация должна давать оповещение, а не замалчивание.
Идеальная нормальная реализация требует доказательства отсутствия переполнения во время компиляции, а не рантайм-проверок.
netch80
29.11.2021 19:54+3> Идеальная нормальная реализация требует доказательства отсутствия переполнения во время компиляции,
Это более продвинутый вариант, безусловно. Но доводить C до такого состояния вряд ли получится и вряд ли имеет столь высокий смысл.
Nehc
30.11.2021 10:44Так то да, но тут еще есть проблема уже написанной кодовой базы, которая по определению не идеальна… ( Получается где-то такая проверка использовалась и выдавала ошибку, а после какого-то момента перестала…
Наверное плохо, что код был написан так. Наверное автор кода не прав в том смысле, что использовал в коде неопределенное поведение относительно которого было отдельное предупреждение. Но плохо, что поведение компилятора изменилось в моменте, а кодовая база осталась прежней… Это реально чревато проблемами в самых неожиданных местах. Причем «тихими» проблемами. Было бы лучше, если бы переполнение стало везде приводить к ошибке…
Да и… Если честно я не понял логики разработчика… Почему при сравнении отрицательного числа с положительным оно внезапно не меньше?! Компилятор не производит вычислений? Типа видит что слева Х+константа, а справа просто Х и без вычисления понимает чтосправаслева больше, так что ли? "Хороший тамада и конкурсы интересные"(с)
Kircore
30.11.2021 08:48-41) само по себе двоичное (не троичное, не какое-то другое) представление
2) битовое представление числа в обычном счислении (а не в каком-нибудь коде Грея)
3) ячейки-«байты» не менее чем по 8 битЭто точно проблема языка, а не существующего железа? Для последнего пункта есть bit fields.
4) память с последовательной нумерацией ячеек (не может быть, что в массиве от 1000 до 2000 — 1001 пропущено, а 1002 есть)
Ты уверен, что в Си есть массивы? Точно знаком с языком?
и 100500 других признаков типовых современных платформ
Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.
netch80
30.11.2021 10:14+2> Это точно проблема языка, а не существующего железа? Для последнего пункта есть bit fields.
Вы вообще смотрите, на что отвечаете? Я говорю, что это требование языка к среде реализации кода: память состоит из сущностей, именуемых «байт» и имеющих не менее 8 бит. При чём тут битовые поля?
Если машина, например, с 6-битными байтами (как было типовым решением в 1950-60-х), то «байт» C будет состоять из двух байтов этой машины.
> Ты уверен, что в Си есть массивы? Точно знаком с языком?
Какие возражения против сказанного? Вы не в курсе, что массивы располагаются в памяти в последовательных адресах?
> Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.
В чём возражение? Или вы не попытались понять тезис?Kircore
30.11.2021 23:43-2Если машина, например, с 6-битными байтами (как было типовым решением в 1950-60-х)
А напомни, когда разработан Си и сколько таких машин было с того времени? Не было надобности - нет реализации.
Какие возражения против сказанного?
Утверждение, что в Си есть массивы.
Вы не в курсе, что массивы располагаются в памяти в последовательных адресах?
Элементы массивов? В каком языке?
В чём возражение?
В требовании абсурдной ерунды, которой ни в одном языке нет за ненадобностью. Код грея либо аппаратно реализованные контроллеры разбирают, либо собственный программный код. Ты ещё какой-нибудь виганд попроси в язык включить.
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, называю это массивом, как бы это кому-то ни не нравилось.
> В требовании абсурдной ерунды, которой ни в одном языке нет за ненадобностью.
Вы перепутали знак: я как раз объяснял, что такого требования нет, а есть, наоборот, требование отсутствия подобных особенностей. Разберитесь, кто это требовал (может, и вы), с него и спрашивайте.Kircore
01.12.2021 03:09-1Машины с 18- и 36-битными словами дожили до начала 90-х. C на них был.
Не, ты мне реализацию шестибитных байтов обоснуй. В большую размерность всё элементарно засовывается. Ещё интересно как это сочетается с твоей уверенностью в нереальности систем с другой размерностью int.
Собственные разработки Cray имели 32-битные слова без возможности байтовой адресации, на них и CHAR_BIT сишный был равен 32.
И сейчас такие системы есть, но в твоей реальности они не существуют.
Я честно не хочу влазить в режим language lawyerʼа плохого пошиба, поэтому скажу так: если я вижу определение типа
int a[1000];
, то я вместе со 100500 других источников, включая учебники C, называю это массивом, как бы это кому-то ни не нравилось.Называй как хочешь, устройство языка это не меняет.
PsyHaSTe
01.12.2021 03:46netch80
01.12.2021 11:18Да, оно у меня в закладках. Проблема в том, что C одновременно и такой высокоуровневый язык, как там описывают, и низкоуровневый, и вот это сочетание даёт как минимум заметную часть текущих проблем. С C++ ещё хуже — сохраняя всё это, он добавляет ещё несколько слоёв вплоть до отдельного compile-time языка темплетов. Понимать и поддерживать всю эту кашу можно только думая на нескольких уровнях.
Исходная же статья частично поднимает вопрос — а как всё это получилось? Абьюзинг всех изначальных UdB только часть этой проблемы (хотя и самая очевидная).
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-битные процессоры, есть секвенсоры, есть много чего, но это другой слой.
> Называй как хочешь, устройство языка это не меняет.
Верно. И в этом устройстве массивы есть, но вы об этом не хотите знать, судя по данному ответу.
permeakra
01.12.2021 11:4918-битная и другая странная арифметика до сих пор встречается в DSP, AFAIK. Да, программируют их обычно не на С, но как явление их упомянуть надо.
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.permeakra
01.12.2021 13:26Вопрос производительной и/или тонковывернутой арифметики сильно шире вопросов ширины арифметики как таковой, имхо. Там очень много тонкостей семантики, какие-то из которых важны для корректности результата, а какие-то для производительности. И на уровне языка системного программирования и тем более llvm asm эти тонкости теряются.
netch80
01.12.2021 13:40> Вопрос производительной и/или тонковывернутой арифметики сильно шире вопросов ширины арифметики как таковой, имхо.
«Тонковывернутой»… это о чём? Какие-то особые операции типа ДПФ в целых числах? Ну если их на C не описать, то по крайней мере интринсики уже способны их задать, а дальше дело компилятора, что он и как вызовет.
> И на уровне языка системного программирования и тем более llvm asm эти тонкости теряются.
На уровне LLVM IR таки, после укладки на дополнительный код и с дополнительными всякими nuw/nsw, оно достаточно неплохо отражается — можно восстановить и что это было вначале. Диапазоны, да, не сохраняются, но это там уже, наверно, и не нужно. Хотя тут могу и пропустить что-то важное.permeakra
01.12.2021 14:24Не только и даже не столько, хоть и близко. Операции типа DFT пишут специально обученные люди, обложившись мануалами, и пишут как правило под конкретное железо и один раз. Поэтому там вполне можно писать непосредственно в асме, если уж надо (см библиотеки типа openblas, где внутренние циклы натурально написаны на асме целевой платформы).
В качестве примера - поиск среднего над большой выборкой чисел максимальной для платформы разрядности. В случае, если вам сильно повезло, вопрос решает тривиально линейным суммированием и одним делением и в ряде случаев это даже будет правильным решением. Но в общем случае вы получите бред из-за переполнения. И локально решить, какой алгоритм использовать, нельзя - нужно иметь какие-то соображения о свойствах выборки и о том, какая точность результата вам нужна.
Если нужна производительность, то в качестве примера можно рассмотреть агрегацию значений сложной в вычислении функции на целочисленной сетке, причем распределение значений на этой сетке имеют нетривиальную симметрию и про некоторые диапазоны заранее известно, что их обходить не нужно. (см wikipedia://polytope+modell)
В любом случае, речь идет о том, что вопрос оптимизации и/или корректности вычислений часто оказывается завязан на неочевидную и нетривиальную информацию о входных данных, которые в модели вычислений на уровне С/llvm asm не отображается. Некоторые современные компиляторы достаточно умны, чтобы её из кода реконструировать (напр. gcc/graphite) но то такое.
unC0Rr
30.11.2021 12:29+1Конечно, массивы в Си есть, заблуждение утверждать обратное, основываясь на одном лишь сходстве работы с указателями и массивами: стоит положить массив в структуру, и оказывается, что его можно передавать и возвращать из функции как одно целое, как кусок памяти определённого размера, чем и является массив.
Kircore
30.11.2021 23:49-1Конечно, массивы в Си есть
Наверняка для них размерность и тип задаются, да? Или исключительно указатель на начало даже без типа?
стоит положить массив в структуру
А потом ты берёшь указатель на эту структуру с типом другой структуры и, оказывается, что массив уже не массив.
В Си нет массивов, они введены в синтаксис добавлением смещения к указателю.
netch80
01.12.2021 00:56+5> Наверняка для них размерность и тип задаются, да? Или исключительно указатель на начало даже без типа?
Ага, вы таки перепутали хрен с пальцем.
Когда написано: `int a[1000];` — это массив.
Более того, sizeof(a) выдаст в этом случае 4000, а не 4 (считая, что int — 4 байта).
А то, что в ряде случаев указание на массив превращается в указатель на первый элемент (индекс 0), не устраняет факт наличия такого типа.Kircore
01.12.2021 02:46-6Когда написано:
int a[1000];
— это массив.А когда после используешь a или *a - это указатель. Сам ты хрен с пальцем.
Более того, sizeof(a) выдаст в этом случае 4000, а не 4 (считая, что int — 4 байта).
У тебя вызов sizeof() заменится константой на этапе компиляции.
Очень удобно критиковать язык, не зная его.
AnthonyMikh
30.11.2021 20:17Особенно код грея (который не используется для хранения и работы с данными в памяти) и троичное счисление.
Таки машины на троичных кодах были.
aversey
29.11.2021 15:49+3Дополню ответ @netch80с которым согласен. Вы описываете Си как язык высокого уровня, и конечно тогда он имеет право давать любые гарантии и любую модель исполнения. Вот только как язык высокого уровня он плох и даёт сложные для понимания и контроля гарантии (если для вас это не так -- поздравляю, вы в сотни раз умнее меня =) ) -- и тогда вопрос -- а зачем он нужен? Ведь когда мы пишем программы мы хотим что бы они работали -- а в силу сложности Си гарантировать это ... сложно. И при этом не важно насколько быстро они работают -- если они работают с ошибками, неправильно -- то они просто не работают.
Ritan
29.11.2021 16:14Только вот проблема, что ни один язык не может дать таких же гарантий производительности. Переписал тут недавно небольшую ВМ на rust - замучался выпиливать проверки границ( при помощи того самого страшного unsafe кода ), т.к. с ними было медленее на 10-30% в зависимости от семпла. И даже после этого только приблизился к результату с++
0xd34df00d
29.11.2021 19:41+2Ни один язык вообще не даёт гарантий производительности. На тех же плюсах мне встречались примеры кода, которые на свежем clang работают в два-три раза быстрее (или в два-три раза медленнее), чем на свежем gcc. И аналогично встречались примеры кода, при малейшем изменении приводившие к тому, что компилятор выбирал другие оптимизации, что ломало его производительность.
Можно было бы сказать, что ассемблер даёт какие-то гарантии, но там тоже всё неочевидно.
Mingun
29.11.2021 20:03Программисту, стоит указать, что этот код оптимизировать не нужно, и бац проблемы не существует.
Так как, как указать, что код оптимизировать не нужно!?
Kircore
30.11.2021 08:53-1Зависит от компилятора. Должно быть что-то вида:
#pragma NO_OPTIMIZE_START ... #pragma NO_OPTIMIZE_END
Gumanoid
30.11.2021 18:20
AnthonyMikh
30.11.2021 20:13+4Благодаря Си Linux самая популярная серверная ОС
Не-а. Я бы даже сказал, что Linux — это великолепная иллюстрация тому факту, что крупные проекты на C писать невозможно. Почему? Да потому что Linux написан не на C, а на C с расширениями языка от GCC — вообще говоря, другой язык.
(самый переносимый язык кстати, один раз написал (если в пределах стандарта) собрал для любой платформы)
Вот с этим "в пределах стандарта" как раз большие проблемы. Приличная долю UB можно обнаружить, только проанализировав текст программы целиком, а у людей мозги маленькие, в которые вся программа целиком не умещается.
PsyHaSTe
01.12.2021 03:44+3Я читаю этот коммент и кажется вы обижаетесь на то, что си несправедливо обругали и говорите, что не язык виноват, а плохие человеки в вообще вон нам язык линукс дал.
Фишка в том, что из двух извечных вопросов важный "что делать", а виноватых искать — последнее дело. Человеки — ошибаются и им нужна помощь машины в виде компилятора чтобы писать сложные программы. И чем больше этой помощи тем лучше. В си из-за особенностей языка такой помощи дождаться тяжело, и изменить это не меняя самого языка не выйдет.
MentalBlood
29.11.2021 15:39-1Относится к действительно неопределенному поведению как к неопределенному — разумно
Относится к действительно неопределенному поведению как к определенному — рискованно, если не глупоМожно попробовать договориться и сделать действительно неопределенное поведение действительно определенным, но в статье, кажется, не о том речь
MrDEX123
29.11.2021 16:56-1Неопределенное поведение не делают определенным, как было упомянуто автором статьи, в качестве loopholов, чтобы язык стал действительно портируемым, не привязанным к железу и компилятору, так как некоторые вещи банально выполнены в них по-разному.
Mingun
29.11.2021 20:07+3Тем не менее, компилятор почему-то выбрал не спустится на уровень своей платформы и не реализовать "неопределенное поведение" определенным именно для этой платформы образом (для чего этот термин изначально и вводился!), а сделать так, как ни одна платформа не делает и молча. Это свинство.
kmeaw
30.11.2021 01:04+1Для таких случаев используются термины unspecified behavior и implementation-specified behavior.
Это свинство.
Разрабатывая программу на стандартном C, разработчик принимает контракт, в рамках которого он не будет писать код, приводящий к неопределённому поведению, а компилятор не будет вести себя неопределённым образом. Эти правила известны разработчику заранее.
Physmatik
30.11.2021 02:49+4Скажите, пожалуйста, сколько людей на планете физически способны писать без UB (опуская вопрос "нужно ли писать без UB", всё-таки UB — это стандарт, а мнения по его адекватности расходятся)? И если ответ "бесконечно мало", то не является ли свинством делать то, что делают современные компиляторы?
kmeaw
30.11.2021 03:11+1Мало. Что, впрочем, справедливо и для многих других классов ошибок — ведь почти никто не считает свинством, что если программист забудет прибавить или отнять единичку, то компилятор сделает не то, чего на самом деле хотел человек.
Чем же тогда контракт "я не буду писать UB-код" хуже контракта "я не буду совершать off-by-one errors" или "я не буду допускать логических ошибок при инвалидации кэша"?
Современные процессоры при обработке данных делают не в точности то, что написано в исполняемом коде, а могут сделать что-нибудь другое, но так, чтобы результат был неотличим от непосредственного исполнения каждой инструкции наивным образом при условии, что программа не содержит data races — и большинство пользователей процессора довольны тем, ведь это позволяет ему работать быстрее, хотя очень мало людей могут никогда не допускать гонок в своих программах. Оптимизирующие компиляторы C делают нечто похожее — они генерируют из исходника такой код, результат работы которого неотличим от интерпретации исходника абстрактной C-машиной при условии, что программа не содержит UB.
Mingun
30.11.2021 07:50ведь почти никто не считает свинством, что если программист забудет прибавить или отнять единичку
Не считают потому, что и прибавить, и отнять единичку — это обе валидные операции в терминах языка. ИИ еще недостаточно умный, чтобы понять, что от него хочет человек.
А вот в описываемых ситуациях одно поведение явно позиционируется, как хорошее, а другое — как плохое, но проще же сидеть на попе ровно и ничего не делать, чтобы дать об этом знать (если явно не попросили заткнуться).
svr_91
30.11.2021 10:38Но всеже программы мы пишем для людей, а не для машин. Если программа работает неправильно, то она работает неправильно
netch80
30.11.2021 11:16> Чем же тогда контракт «я не буду писать UB-код» хуже контракта «я не буду совершать off-by-one errors» или «я не буду допускать логических ошибок при инвалидации кэша»?
Off-by-one практически всегда ловятся в пределах одного экрана (ну, можно сделать, чтобы не поймались так, но это надо постараться). В случае кэша есть гарантированный метод — не идти на всякие lock-free, а ограничиться мьютексами (или вообще обменом сообщениями): дороже, но надёжно. В случае описанных ошибок C защититься без полного контекста (включая всякие библиотечные функции, которые могут обновляться независимо от основного кода) нереально, и вопрос не в производительности — если бы было только в ней, большинство бы использовало безопасный режим по умолчанию (как я и предлагаю в десятке соседних комментариев).
Mingun
30.11.2021 07:35+2Хорошие системы предполагают, что кроме известных правил вас предупредят о том, что вы близки к тому, чтобы их нарушить. Пора очнуться от ощущения, что разработчик способен уместить в своей голове всю программу — это не так уже лет 20. Это забота компилятора. Собственно, основная претензия к C/C++ компиляторам именно в том, что они делают преобразования молча. И не говорите мне, что контекст к тому времени уже потерян. Просто и нет попыток его сохранить.
netch80
30.11.2021 10:21+3> Разрабатывая программу на стандартном C, разработчик принимает контракт,
> Эти правила известны разработчику заранее.
Неизвестны. Не принимает. Во всяком случае, большинство интернов и джунов про эти проблемы не подозревает, и их не учат.
Если вы возьмёте типовую книгу «C для начитающих», «Освоение C++» и тому подобное, там или ни слова про это не будет, или будет очень вскользь, даже если это книга от какого-то корифея языка, который десять собак на нём съел.
Можно, конечно, спрятать голову в песок и сказать, что это проблема образования. Но сейчас это проблема всех нас — когда код взрывается.
MrDEX123
03.12.2021 15:53А как спуститься на уровень платформы, если на 2х разных платформах подразумевается совершенно разное поведение с разными результатами? Тот же пример со знаковым переполнением, понимаю что заезженно и уже неактуально после с++20, но гипотетически на платформе Winux отрицательные числа представлены не не доп кодом, то есть невозможно один и тот же код допускающий ub скомпилить, чтобы он выполнял одну и ту же работы для этой платформы и, например, linux
homm
29.11.2021 15:49+14Говоря об упомянутом в письме ядре Linux, его автор Линус Торвальдс также дал свою оценку strict aliasing в частности и работе комитета в целом.
Когда знаешь, что следующие три минуты чтения пройдут очень весело.
uuuuuuuu
29.11.2021 15:50+24Си никому ничего не должен. Считаете, что он не нужен — просто не используйте его.
gameplayer55055
29.11.2021 15:51+1Си умрёт только тогда, когда будут широко доступны всем квантовые компьютеры. Потому что он как кроссплатформенный ассемблер: плотно работает с железом
le2
29.11.2021 19:58+5Зря заминусовали.
Си это Unix поверх машины Тьюринга. Си неулучшаем. С изобретением бульдозера не наступила смерть лопаты, шуроповерт не уничтожил обычную ручную отвертку.Нельзя что-то улучшить не ухудшив одновременно какие-то старые свойства.
Главная проблема в том что все эти ошибки позволяет совершать аппаратная сущность - процессор.Требуется радикальная смена парадигмы. Такой вычислитель, который отправит в каменный век Unix.
MinimumLaw
30.11.2021 07:35Точно. И даже в этом случае паровой двигатель имеет все шансы трансформироваться в паровую турбину, которая серьезно востребована в атомную эпоху.
Прям не смерть, а буддистское перерождение. "А после из прораба до министра дорастешь" (с)
MinimumLaw
29.11.2021 15:55-2Хорошая статья. Плюсанул везде, где мог.
От себя скажу лишь одно - умрет. Обязательно умрет. Как умер Кобол, как умер Фортран, как умер Ада, как умер Паскаль... Так устроена жизнь - старички умирают уступая место молодым. Самые легендарные продолжают жить в мифах и легендах. Си уже более чем седовласый старичок. За время его жизни сменилась не одна парадигма программирования и не одно поколение вычислительных машин. Потому даже не сомневаюсь - умрет.
Вопрос ведь не в том умрет ли. И даже не в том когда именно. Вопрос в том, что будет потом. Появится ли тот самый "квантовый компьютер" - единственный и неповторимый, под который получится написать сразу и бесповоротно единственный и непротиворечивый стандарт без костылей в виде неопределенного поведения в бесконечном будущем? Помнится JAVA VM в свое время пыталась стать таким вот "универсальным компьютером"... И не учит ли нас та же JAVA VM тому, что у такого пути есть свои недостатки? Или у нас будет зоопарк архитектур, в которых подсчет бит в виде BE/LE и особенности работы со знаковыми и беззнаковыми числами это минимальное из зол? И кто возьмется написать что-то, что накроет все прошлые и еще живые архитектуры в купе с настоящими и будущими?
У меня нет ответов ни на один из вопросов.
aversey
29.11.2021 16:02-1Всё течёт, всё меняется. =)
К сожалению я тут немного пессимистичен, Никита ещё более -- опыт PL/I, например, видимо не был учтён, многие языки сегодня всё так же безразмерно разбухают. Боюсь что с Си будет то же -- он уйдёт, на его место придёт почти такой же, не сделавший выводов из ошибок прошлого. Надеюсь эта статья поможет кому-то в будущем сделать лучше, хотя это и излишне оптимистично. =)
Что касается заданных вами вопросов -- я тоже не имею на них ответов. =)
MinimumLaw
29.11.2021 16:36+1Сложно гадать о том, что будет. В первую очередь по той простой причине, что приходится залезать на чужой огород, в котором мало что понимаешь.
Мне кажется нас ждет некоторая "семиуровневая модель OSI" применительно к вычислительной технике. Где каждый уровень будет максимально изолирован от соседнего. Ассемблер (в смысле машинные коды) в обозримом будущем скорее всего не исчезнут. Значит нужен будет слой абстракции, который так или иначе приводит их к некому единому формату. В идеале максимально эффективно. Опять же в идеале не противоречиво. Вопрос лишь в том как этого достичь...
В принципе, сейчас наблюдается что-то такое. Абсолютно не ведающие про "безопасность" машинные коды накрываются уровнем ядра операционной системы. Выше libc или ее аналоги WinAPI. Еще выше всякие QT/GTK/MFC/NET. Другое дело что сегодня нет строгой изоляции между этими уровнями...
А может быть действительно удастся создать некий "универсальный стандарт проектирования кода" и нужда в строго разделенных уровнях абстракции просто закончится.
Мне кажется, что первый вариант на сегодня более вероятен. Но много я на него не поставлю. Как, впрочем, и на второй. И на третий, и на десятый, и на сотый... Поживем - увидим, доживем - порадуемся.
ignat99
01.12.2021 11:42Семиуровневая модель реализованная в железе. Уже сейчас можно использовать Систем С или Верилог или любой другой HDL.
Единого формата не будет. Это скорее всего мечты юношей. Будут узкие специалисты. Каждая группа со своим собственном языком.Дело в физических процессах, а всё остальное это бантики сверху.
Ritan
29.11.2021 16:10-9И какая же парадигма программирования сменилась? ФП - мёртворождённое, а императивщину никто не отменять не собирается
0xd34df00d
29.11.2021 19:43+7ФП — мёртворождённо
Почему?
Physmatik
30.11.2021 02:56-3Фундаментальная несовместимость с физическими императивными вычислителями? Не, я слышал что-то про прототип Лисп-машины, но сейчас-то их нет нигде.
А вне контекста этой статьи (системное программирование) всё у ФП прекрасно, разумеется.
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, взять хаскель и обмазаться верификацией.
red75prim
30.11.2021 06:07+1просто, как сделали seL4, взять хаскель и обмазаться верификацией.
Точнее, взять хаскель, написать прототип реализации, верифицировать. Смотря на этот прототип, написать реализацию на подмножестве С для улучшения производительности, верифицировать эту реализацию. См. http://www.sigops.org/s/conferences/sosp/2009/papers/klein-sosp09.pdf
mayorovp
30.11.2021 10:53-1Что-то ваша ссылка на ivorylang.org не работает...
0xd34df00d
30.11.2021 11:14+2Только что проверил — открывается.
mayorovp
30.11.2021 11:18-2Только что проверил — не открывается и не пингуется.
0xd34df00d
30.11.2021 11:20+9Перепроверил с ssh-туннелем через машину в РФ — и правда, не открывается. Санкции, не иначе.
Physmatik
30.11.2021 19:11Скажите, насколько просто на Хаскелле реализовать, например, in-place quicksort (in-place обязателен)?
vkni
30.11.2021 19:41+1Очень просто - пишете в гугеле haskell in-place quicksort и копируете оттуда. Занимает примерно столько же, сколько in-place код на C, т.к. это непосредственный перевод.
Physmatik
01.12.2021 19:18Во-первых, попробуйте найти. Типичные in-place версии требуют O(N) памяти (что сразу убирает половину смысла in-place алгоритма).
Во-вторых, это ведь "например". Далеко не всегда вы сможете скопировать код необходимого вам алгоритма где-нибудь в интернете.
0xd34df00d
01.12.2021 19:36+5Не-in-place там только из-за того, что на вход внешней функции-обёртки над ST подаётся обычный хаскелевский список. Подавайте сразу массив, и не будет лишней памяти.
Это как если бы ваш алгоритм сортировки на плюсах принимал пару произвольных итераторов (не только random access) и внутри делал из этого
std::vector
.
0xd34df00d
30.11.2021 23:10+2Так же, как и на любом другом языке. Делаете
thaw
вашему (иммутабельному вектору), модифицируете что надо in-place, делаетеfreeze
. Я бы сел и написал, но мне откровенно неохота включать мозги для того, чтобы написать правильный партишон.Physmatik
01.12.2021 19:11https://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) по памяти, насколько я понял?
0xd34df00d
01.12.2021 19:19+1Там рядом есть
unsafeThaw
, который копий не делает, но, увы, в отсутствие линейных типов это действительно небезопасная функция.Там рядом есть modify, которая должна это скрывать, но она внутри делает некую магию, которую не все любят.
chernish2
29.11.2021 17:12+5Это видимо сарказм, про мертвые языки? Всё перечисленные очень живы, и даже пресловутый Cobol.
MinimumLaw
30.11.2021 07:15Безусловно. Не все, к сожалению, понимают сарказм. Особенно когда нет соответствующего тега.
ignat99
01.12.2021 11:52+1выше всякие QT/GTK/MFC/NET.
Начтите с A2, но там много косяков с памятью у студентов и аспирантов Вирта.
Посмотрите wxWidget - запускается на многих платформах. Ну потом уже делайте суждения про "всякие". А лучше напишите своё, только ради бога не на JS.
Дело вовсе не в библиотеках, а в конкретных устройствах под которые эти оконные интерфейсы портировали (Начиная с 2004 года). IMHO
На мощных компьютерах и в 2000 не было ни каких проблем. Брался Енлайгмент, KDE или будущий Гном или Next и т.д. и всё работало. Пока не набежали милиниалы и не начали "улучшать" архитектуру. Теперь у нас много проблем после этих "улучшений".
А вот Doom до сих пор портируется на почти всё что считается микропроцессором.MinimumLaw
01.12.2021 12:20Боюсь мой уровень сильно ниже. В основном от reset-вектора до exec("/sbin/init"). А порою даже ниже, чем вектор сброса.
Собственно потому и говорю что сложно рассуждать про чужие огороды. Там все не так, как у меня. Мое "всякие" не содержит негативного оттенка. Оно скорее меня характеризует. Как человека не знакомого с данным конкретным слоем. Каждому свое.
ignat99
01.12.2021 13:05Я делал драйвера для первого смартфона с Линукс компании Самсунг Електроникс. В основном это об процессах в dev/mem Пришлось сделать для контроллера памяти и видеобуфера изменения, чтоб запустить всё это на новом железе.
Следующий шаг это разработка графического меню или оконного интерфейса. Так что не ограничивайте себя. А то как у всех миллиниалов у вас будет очень искажённое восприятие действительности, оторванное от реальности.
Как например слухи о скорой смерти СИ.MinimumLaw
01.12.2021 13:29А то как у всех миллиниалов у вас будет очень искажённое восприятие действительности, оторванное от реальности.
Как например слухи о скорой смерти СИ.
Знакомый ник. Мы ж с вами где-то уже пересекались на тропке хабровских комментариев... Занятно все это... Вот я уже миллениал (хотя практически ровесник языку С - 1979-ого года выпуска). Вот я уже распускаю слухи о скорой смерти языка С (основного моего рабочего инструмента). Весело.
Я ж не писал что С скоро умрет. Я писал "не сомневаюсь - рано или поздно умрет". И дальше список условно мертвых языков. Каждый из которых когда-то был универсальным, а потом оказался нишевым. И в своей нише продолжат жить (а в некоторых случаях еще и процветать). С (классический, не приплюснутый) уже давно не универсальный язык. И названные в статье проблемы весьма в немалой степени этому способствовали. Это достаточно очевидно. Как очевидна и связка С с (ранним?) UNIX и вытекающие отсюда проблемы. Беда в том, что по моему только настоящие проектировщики встраиваемых систем понимают что эта сцепка не жесткая, и в принципе не обязательная. Но это не формат комментария... Это тема для статьи. А на нее как водится категорически не хватает времени. А если совсем честно, то и желания. Она все равно у подавляющего числа хабражителей не вызовет ничего, кроме изжоги. Это я не хочу топтать их огород. Они мой завсегда и с удовольствием. Потому доношу тем, с кем работаю. Когда дозревают до понимания.
И ладно когда "молодые и горячие" что-то подобное мне предъявляют. Это нормально. Каждый приходящий ко мне в отдел пытается меня перекричать и сдать в утиль. Пока, правда безрезультатно. Но от человека, который "делал драйвера для первого смартфона с Линукс компании Самсунг Электроникс" подобного явно не ожидаешь.
ignat99
01.12.2021 13:39А вы на ламповую технику переключайтесь. Это сейчас тренд.
Как было трендом развитие Qt в 2004 году.
Сколько раз уже сказали что ламповая техника умерла?Подозреваю, что вы скажете что это сильно ниже вашего уровня компетенции... типа тут мы уже всё ...
MinimumLaw
01.12.2021 13:50+1Лампы... А знаете как легко и здорово объяснять цифровую схемотехнику с помощью обычных реле. Нормально замкнутых, нормально размкнутых, переключающих. Как классно звучит (во всех смыслах этого слова) "сдвиговый регистр на электромеханических реле"? И насколько после этого по другому воспринимается Шеноновская нетленка "Надежные схемы из ненадежных реле"?
А лампы... Нет, спасибо. Меня "теплый ламповый 100Гц гул" с детства достал... Не ощущаю я в нем той могучей аудиофильской силы. И тем более не интересно в плане цифры. Впрочем, многосеточные лампы это сила. Практически не имеющая себе аналогов. Но опять же - сие тема совсем другого разговора.
ignat99
01.12.2021 14:06+1Список команд Урал-1 — Википедия (wikipedia.org)
Ну почемуже совсем другого ... гул в 55 Гц и выше в 89 Гц и сейчас присутствует, но уже от планета Земля. Люди делятся на 8, 13, 21, 34 и выше герцовых.
А ещё есть аналоговые вычислителиИ натурные экспериме́нты.
Ещё про "Сетунь" и троичное округление можно поговорить, а так же про магнитные тракты в магнитных катушках управляемых током.
Реализация тригера там мне кажется энергетически более экономным в отличии от реле. А технически это достигалось взаимным парным (ручным) подбором параметров катушек и диодов.
Но флагман AD - AD9213 по прежнему стоит очень дорого. Дороже 1500 евро, так что большинству тут присутствующих эта техника не доступна. Как и в 1996 году первые AD.MinimumLaw
01.12.2021 14:36+1Ну почему же совсем другого ...
В первую очередь потому что обсуждается язык С. А не про пути развития, так или иначе оказавшиеся тупиковыми. Впрочем, опять же некоторые не тупиковые, а очень нишевые. Однако людей, способных пользоваться логарифмической линейкой сильно меньше, чем умеющих калькулятор. Впрочем, линейка штурмана, как я знаю, местами еще в ходу. Но все это косвенно говорит как раз о том, что понятие смерти у заслуженных работников - оно относительное и немного лукавое.
AnthonyMikh
01.12.2021 18:07Даже тот же Excel из Microsoft Office 95 на сях писать было бы, мягко говоря, неудобно.
0xd34df00d
01.12.2021 19:53+1Эксель 95 уже сам по себе достаточно крупный проект на много лет и программистов.
WraithOW
02.12.2021 13:27+1А кроме Экселя продуктов в мире нет?
чем каждый день пользуются
Ничем не пользуюсь. Не нужен этот ваш Эксель, позвоню в Майкрософт, чтоб выпиливали.
AVI-crak
29.11.2021 16:36-12За использование int в программах на Си - нужно руки/ноги отрывать. А если человек плохо обучается - то ещё и голову.
netch80
29.11.2021 17:12+7Что, по-вашему, тут улучшит явное предписание какого-нибудь int32_t?
Kircore
30.11.2021 09:00+4Везде улучшит. В зависимости от ЦП int может быть размером и 16 и 32 и 64 бита.
netch80
30.11.2021 09:08Давайте без фантазий о мифических странах. Во всех реальных платформах вокруг меня int == int32_t. Вот я заменил в программе int на int32_t. Каким образом это поможет исчезнуть факту незамеченного переполнения в конкретном месте программы?
Kircore
30.11.2021 23:56+2Привет от нереальных платформ avr, pic, stm8, rpi4.
netch80
01.12.2021 01:02-1> Привет от нереальных платформ avr, pic, stm8, rpi4.
Ни разу не приходилось под такие писать после ~2000 года, потому и указал явно, что «вокруг меня». Но предположим. То есть вы решили на платформах с естественными 16 битами решить проблему расширением разрядности до уровня, поддержка которого уже в разы более громоздкая, потому что длинная арифметика?
А не боитесь, что и этот размер переполнится, только чуть позже?
И не учитываете, что по сравнению с другими проблемами платформы эта конкретная будет минимально заметной?Kircore
01.12.2021 02:53-2Ни разу не приходилось под такие писать после ~2000 года, потому и указал явно, что «вокруг меня».
Я уже понял, что ты солипсист.
То есть вы решили на платформах с естественными 16 битами решить проблему расширением разрядности до уровня, поддержка которого уже в разы более громоздкая, потому что длинная арифметика?
А не боитесь, что и этот размер переполнится, только чуть позже?
И не учитываете, что по сравнению с другими проблемами платформы эта конкретная будет минимально заметной?Тебе только указали, что надо использовать stdint и явно указывать размерность.
warlock13
29.11.2021 16:57-2Си должен умереть, но совсем не потому, что два неадеквата (Торвальдс и felix-gcc) брызгая слюнями порят чушь.
Gordon01
29.11.2021 16:59+19Разбудите меня через десять лет и спросите о чем говорят Сишники и я отвечу — про разыменование нулевого указателя и прочие UB.
Скучно.
На практике, конечно, давно придуманы всякие valgrind, санитары, clangd, pvs студии и прочие штуки, которыми надо обмазаться, чтобы написать на с/с++ что-то, что проработает больше дня без падения.
Настоящая же проблема с/с++ — отсутствие средств сборки. Точнее их тысячи, но дай бог, если одно из них совместимо хотя бы с парочкой других. Над ними надстроены еще обычно проекто-специфичные самописные системы сборки, которые управляют ширпотребными системами сборки...
То же самое с остальным тулингом вокруг этих языков. В каком-нибудь вскоде официальный плагин для c/c++ довольно ограниченный и заметно хуже того экспириенса, который есть в студии с решарпером. И чтобы этот опыт улучшить надо возиться. В нашей компании ребята попробовали приспособить clangd, но за несколько месяцев НИКТО из плюс-минус сотни разработчиков так и не попробовал. Никому это не интересно, все привыкли страдать и принимать отсутствие IntelliSense как должное. Да, можно пользоваться студией или редакторами от JB, но это в случае если ты пишешь только на с/с++. Мне, например, нравится что в вскоде, как в виме или емаксе можно работать с любым языком.
Другая проблема в непортабельности исходников с/с++. Код на питоне, тайпскрипте или расте можно просто взять и запустить/собрать на любой из поддерживаемых платформ и он сразу заработает. Удачи просто собрать исходники с/с++ на новой платформе.
Так что разыменование нулевого указателя это так, мелочи.
Зачастую на отладку и тестирование программ уходит больше времени, чем на проектирование и написание самого кода.
За все время работы с сишниками у меня сложилось впечатление что им нравится этим заниматься. Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него. Поговорите с любым отбитым сишником (или почитайте их в твиттере) про раст и сразу все поймете.
Ну не хотят люди жить хорошо, привыкли они так. И это не плохо. Просто так сложилось.
netch80
29.11.2021 17:15+6> Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него. Поговорите с любым отбитым сишником (или почитайте их в твиттере) про раст и сразу все поймете.
Видите ли… сишники/плюсовики подобные средства и так регулярно используют. Проблема в том, что они не дают никакой гарантии — и анализаторы, и санитайзеры ловят, грубо говоря, 99% случаев, а 1% всё равно остаётся. И при мельчайшей смене чего-то в коде их надо напускать заново, потому что от того, что карты разложились чуть иначе, компилятор нашёл новый вариант схалявить.
> Ну не хотят люди жить хорошо, привыкли они так.
Нет «привычки». Есть понимание реальности. Вы же её не понимаете и рассказываете какие-то байки о том, что подсмотрели в замочную щёлку вместо собственного опыта.
> Другая проблема в непортабельности исходников с/с++. Код на питоне, тайпскрипте или расте можно просто взять и запустить/собрать на любой из поддерживаемых платформ и он сразу заработает.
Потому что они непосредственно стыкаются с железом, не? В отличие от прочих названных, где как раз C/C++ обеспечили им независимость и возможность поплёвывать свысока на мучения «холопов»?Gordon01
29.11.2021 18:42-2Проблема в том, что они не дают никакой гарантии — и анализаторы, и санитайзеры ловят, грубо говоря, 99% случаев, а 1% всё равно остаётся.
Если человеку нужна 100% гарантия, то он выбрал совершенно не тот язык.
И при мельчайшей смене чего-то в коде их надо напускать заново, потому что от того, что карты разложились чуть иначе, компилятор нашёл новый вариант схалявить.
Это более чем очевидно. Не думал, что такое нужно объяснять.
Другое дело, что у пользователей современных языков большинство этих проверок выполняется после каждого нажатия на клавишу в редакторе. И начинают работать сразу из коробки после установки одним кликом бесплатного плагина из стора, никогда не разваливаются, не падают и не отжирают 32 ГБ памяти в попытках проиндексировать 100 тысяч строк кода.
Нет «привычки». Есть понимание реальности. Вы же её не понимаете и рассказываете какие-то байки о том, что подсмотрели в замочную щёлку вместо собственного опыта.
Так расскажите свое понимание, только без перехода на личности.
Потому что они непосредственно стыкаются с железом, не? В отличие от прочих названных, где как раз C/C++ обеспечили им независимость и возможность поплёвывать свысока на мучения «холопов»?
Давайте расскажите как раст не стыкуется с железом)))
Типичная байка, будто на с/с++ только ОС и драйверы пишут.
netch80
29.11.2021 19:06+1> Если человеку нужна 100% гарантия, то он выбрал совершенно не тот язык.
А вы думаете, чему посвящён исходный постинг? Мы же тут обсуждаем, что можно было бы сделать, чтобы получить гарантии.
> Это более чем очевидно. Не думал, что такое нужно объяснять.
Ну вот и речь о том, что сделать, чтобы такое перестало быть «очевидным» потому, что перестало быть реальным.
А в расте вам тоже «очевидно», что изменение кода одного модуля может взорвать код другого, безо всякой видимой связи между ними? Тогда зачем тот раст был бы нужен? (заметьте, я не утверждаю)
> Другое дело, что у пользователей современных языков большинство этих проверок выполняется после каждого нажатия на клавишу в редакторе.
Потому что у них нет таких перекрёстных эффектов? Ну так, в третий раз повторяю, за то и боремся.
> Так расскажите свое понимание, только без перехода на личности.
Вы первый задали такой тон. И это не на личности, а на конкретные специфики, безусловно ввязанные в данную дискуссию. Вы сами поставили своими словами себя отдельно от мира C и высказываете при этом то, что ему не соответствует аж никак.
А по сути — уже рассказано, не вижу смысла повторяться: есть анализаторы, есть санитайзеры, есть здравый смысл авторов, и в сумме они… ну почти всё таки покрывают. И вот этот вот недостающий кусочек от «почти» до полного — предмет, в 4-й раз, обсуждения.
> Давайте расскажите как раст не стыкуется с железом)))
Сколько лет C и сколько расту? Сколько кода на C накопилось? И зачем вы мне стали тут приписывать какие-то утверждения, на которые я даже не намекал?
> Типичная байка, будто на с/с++ только ОС и драйверы пишут.
Нет, конечно. А зачем вы это говорите? Говорите тому, кто придумал эту байку (это не я, а упомянули тут её зачем-то вы — сами и придумали?)
PsyHaSTe
01.12.2021 03:52На каких холопов поплевывает раст? Гц языки-то ещё ладно, но тут вполне bare metal можно писать.
netch80
01.12.2021 10:48+1> На каких холопов поплевывает раст?
Вы про последний абзац? Там про Rust как-то очень вскользь и сомнительно. Утверждать, что на Rust может может сразу скомпилироваться и запуститься то, что на C не завелось, как-то очень странно (вопрос не в отсутствии багов или тому подобном, а именно в переносимости). Это я просто пропустил, извините, но там и так был очень объёмный комментарий. Хотелось бы обоснований и примеров, как такое может получиться, причём не в исключительном случае.
(Наперёд — случаи размеров типов не считаем. И в C есть int${N}_t давно, и случаи, где применяются всякие int, от его размера зависят в редчайших ситуациях.)PsyHaSTe
01.12.2021 13:54Вы про последний абзац? Там про Rust как-то очень вскользь и сомнительно. Утверждать, что на Rust может может сразу скомпилироваться и запуститься то, что на C не завелось, как-то очень странно (вопрос не в отсутствии багов или тому подобном, а именно в переносимости).
Я не понимаю откуда тут в комментах люди всегда додумывают так сильно. Где я писал что раст может компилировать то куда не может си? Скорее в обратную сторону, у си конечно же больше поддерживаемых платформ. Но к чему это, разве я утверждал обратное?
Я сказал лишь то что раст может запускаться на голом железе и не требует никаких холопов для работы. Единственное к чему можно (и иногда это делают) прикопаться это к "вот вы присосались к сишному llvm, ничего сами сделать не можете!!1". Ну тут уже людям объяснять нечего, конечно.
netch80
01.12.2021 14:16+1> Я не понимаю откуда тут в комментах люди всегда додумывают так сильно.
Додумывать — это нормально, мы все всегда так делаем по определению (подразумеваемый контекст — например, что я вам лучше тут отвечу по-русски, а не по-арабски;)) Но додумывать с представлением своей додумки как утверждения, как делают тут некоторые собеседники (не вы) — действительно плохо. Я же спрашиваю, угадал ли с додумкой — чтобы лучше понять собеседника, потому что плохо знаю эту тематику.
> Я сказал лишь то что раст может запускаться на голом железе и не требует никаких холопов для работы.
Спасибо, сложил в закладки. Там интересно и что явно сказано, и что не сказано. Например, я в reset_handler не увидел инициализации стека. Вероятно, это обеспечено линкером. Главное, что несколько импортированных символов не потянули за собой половину стандартной библиотеки, как часто бывает в сишных реализациях, не рассчитанных на embedded. Значит, уже есть «achievement unlocked».
> Единственное к чему можно (и иногда это делают) прикопаться это к «вот вы присосались к сишному llvm, ничего сами сделать не можете!!1». Ну тут уже людям объяснять нечего, конечно.
+100. LLVM не сишный, он универсальный, на чём бы ни был написан.
0xd34df00d
29.11.2021 19:46+9За все время работы с сишниками у меня сложилось впечатление что им нравится этим заниматься. Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него. Поговорите с любым отбитым сишником (или почитайте их в твиттере) про раст и сразу все поймете.
Да потому, что никакие pvs/санитайзеры/валгринды никогда не дадут тех же гарантий, которые даёт хотя бы раст, я уж не говорю о более хардкорной наркомании.
Другая проблема в непортабельности исходников с/с++. Код на питоне, тайпскрипте или расте можно просто взять и запустить/собрать на любой из поддерживаемых платформ и он сразу заработает. Удачи просто собрать исходники с/с++ на новой платформе.
В моей практике (особенно за деньги) на это тратилось очень малое количество времени. Возможно, специфика работы, когда ты сидишь и фигачишь один проект условный год, а не делаешь
create-react-app
илиstack new
раз в неделю.vkni
29.11.2021 21:58Да потому, что никакие pvs/санитайзеры/валгринды никогда не дадут тех же гарантий, которые даёт хотя бы раст, я уж не говорю о более хардкорной наркомании.
Тут ещё проблема в том, что pvs даст какие-то гарантии сейчас, а они должны были быть даны 5 лет назад, когда этот код писался. А сейчас уже никто и не вспомнит, как именно надо написать вот эти явно кривые 5 строк кода. И не поломает ли исправление что-то там далеко, в миллионах каталогов от места исправления.
А так - да, когда у нас более "расслабленный" язык, на нём часто легально пишут с провоцированием ошибок. Конкретный пример - функция filter-map в стандартной библиотеке Racket. Она принимает в качестве первого параметра функцию, которая должна выдавать значения разных типов: #f (false : bool), если элемент нужно выбросить, и какой-то 'a, если его нужно отобразить и оставить. Очень удобная эта filter-map, но из-за неё уже нельзя вот так просто брать и требовать у каждой функции тип возвращаемого значения.
И отделять тут козлищ (тех, кто по-ошибке возвращает значения разных типов) от агнцев можно лишь "интуитивно". Это значительно опаснее того же C.0xd34df00d
29.11.2021 22:57+2Она принимает в качестве первого параметра функцию, которая должна выдавать значения разных типов: #f (false: bool), если элемент нужно выбросить, и какой-то 'a, если его нужно отобразить и оставить. Очень удобная эта filter-map, но из-за неё уже нельзя вот так просто брать и требовать у каждой функции тип возвращаемого значения.
Эм, а чем это отличается от вполне себе существующего и типизируемого хаскелевского
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
с очевидной семантикой?vkni
29.11.2021 23:17Тем, что у вас там внутри скобочек есть тип после стрелочки, а у filter-map - принципиально нету. И ваша функция явно указывает Just/Nothing, а в Racket мы не пишем эти два конструктора.
То есть, выразительные средства языка у Racket такие, что некоторые вещи выглядят двусмысленно по сравнению с Haskell. И ничего тут стат. анализатор Racket не сделает.
0xd34df00d
29.11.2021 23:41+5Сложно-то как без статической типизации.
vkni
30.11.2021 00:45+2Некоторые из моих знакомых считают, что статическая типизация убивает креативность. :-) :-) :-)
WraithOW
01.12.2021 13:44+6Проблем у Си нет, есть проблема ухудшающегося качества программистов и проблема переоптимизации у компиляторов.
Э… ну написал с полгода назад небольшой демон, который читает конфиг, слушает сокет и в потоках раскладывает данные по файликам в зависимости от того, что в конфиге написано.
Небольшой демон можно на чем угодно написать, от lua до ассемблера (на хабре точно была пара статей про вебсервера на asm). Если вы позиционируете его как язык для небольших поделок — это одна история, если как язык для серьезных проектов на много лет и много программистов — совершенно другая.
mayorovp
29.11.2021 22:14+1А новый проект создавать не обязательно, достаточно попытаться собрать из исходников любой уже существующий чтобы напороться на проблемы.
vkni
29.11.2021 21:55Сишник/плюсовик лучше потратит несколько лет на рассказы про байки UB, священную борьбу и написание книг/статей на эту тему, вместо того чтобы взять sonar/pvs/etc и прогнать свой говнокод через него.
Проблема в том, что говнокод, как правило, не за его авторством, спецификаций не было, поэтому как и что нужно исправить для конкретного предупреждения PVS уже неизвестно.
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 уже на чистом питоне переписан?
0xd34df00d
29.11.2021 23:43+2В JB нет двух из четырх языков, что я плюс-минус регулярно использую, и еще один сделан очень криво.
cepera_ang
30.11.2021 07:20+6А вы положите на эту машину кошелек с битком на миллион долларов и выставите ваш сокет в открытый интернет и сюда ссылку приведите.
anonymous
00.00.0000 00:00Kircore
30.11.2021 09:04-1Код на питоне, тайпскрипте или расте можно просто взять и запустить/собрать на любой из поддерживаемых платформ и он сразу заработает. Удачи просто собрать исходники с/с++ на новой платформе.
Собрать код на питоне или тайпскрипте? Раст у тебя везде заработает?
DirectoriX
30.11.2021 15:41+3на любой из поддерживаемых платформ
Раст у тебя везде заработает?
Как ни странно — да. Конечно, у конкретный крейтов могут зависимости быть от библиотек, которые отсутствуют на какой-то платформе, но это проблема крейтов, а не языка/компилятора; точно так же как если я буду использовать /dev/random в качестве источника случайных значений, то на Windows ну никак не будет работать.Kircore
30.11.2021 23:37+1на любой из поддерживаемых платформ
Значит если интерпретатора питона нет на платформе или твой код использует сишную библиотеку, распространяемую в прекомпилированных бинарниках, то не заработает?
Как ни странно — да. Конечно, у конкретный крейтов могут зависимости быть от библиотек, которые отсутствуют на какой-то платформе, но это проблема крейтов, а не языка/компилятора;
Так и код на Си везде заработает, если доступны нужные библиотеки и API.
DirectoriX
01.12.2021 00:12Значит если интерпретатора питона нет на платформе или твой код использует сишную библиотеку, распространяемую в прекомпилированных бинарниках, то не заработает?
Именно так. Как и со всеми другими скриптовыми языками: нет интерпретатора — нет интерпретации.Так и код на Си везде заработает, если доступны нужные библиотеки и API.
Так и на C код не заработает, если нужных библиотек/API нет, в чём тут недостаток Rust? Пишете код на «чистом» C/Rust — перенесётся без проблем, пишете с библиотеками — уже от них будет зависеть ваша переносимость.
При этом зависимость не только от сторонних библиотек, но и от std. Банальный пример: на всяких микроконтроллерах нет stdio.h, зато есть прямой доступ к регистрам процессора (без ассембреных вставок, просто по адресам), но вы же не скажете, что C — непереносимый язык?Kircore
01.12.2021 00:26Так и на C код не заработает, если нужных библиотек/API нет, в чём тут недостаток Rust?
Изначальная претензия была к Си.
Банальный пример: на всяких микроконтроллерах нет stdio.h
Вообще-то есть. И переопределением, например, putc() можно использовать printf для вывода в последовательный порт или любой другой интерфейс.
зато есть прямой доступ к регистрам процессора
Указатели на абсолютные адреса использовать можно ("регистры" периферии), именованные регистры общего назначения без ассемблера - нельзя.
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 :(
johnfound
29.11.2021 17:37+6Нет, вообще-то я согласен. Потому что ассемблер намного лучше чем C.
arm039
29.11.2021 20:48А Vax assembler ещё лучше))
johnfound
29.11.2021 21:54+5Нельзя так говорить! Каждый ассемблер еще лучше. ????
le2
01.12.2021 01:16+2разверну вашу мысль, коллега. Если отбросить троллинг, то упомянутый уже здесь Крис Касперски писал что-то вроде «тяжело забивать гвозди, когда молоток непрерывно изменяется в руке».
Когда-то в начале века я только заработал на первый компьютер, опыта не было, и я смог найти работу только программистом в embedded на ассемблерах. Так получилось, что я был предоставлен сам себе и написал на нём все базовое с нуля: многоразрядную арифметику, копирование блоков памяти и все такое. Оказалось что это очень много кода. Потом я уже работал над большими чужими коммерческими проектами и научился писать на макросах, как писали большие пацаны древности (ассемблер превращается во что-то высокоуровневое — фортранообразное).
Короче, сила ассемблера в том что никто не нужен для понимания. Интернет тоже не нужен, потому что все однозначно. Холивара тоже нет. Нужна только документация.
Конечно, ассемблер это зло, но это то к чему нужно стремиться при разработке инструментов — инструмент не изменяется в руке. Всё однозначно понятно. Порог входа (для технарей) тоже очень низкий. То есть у новичка код будет плохой, но он будет работать.
На ассемблерах я писал годами и когда перешел на Си я в голове постоянно прокручивал как эти конструкции выглядят на ассемблере и что происходит в процессоре. И должен признать что испытывал все те же новичковые сложности с синтаксисом ссылок-указателей. Осознал всю вселенную неоднозначностей что скрывает компилятор и стандарт. Хотя казалось бы — почему? Ведь я реально знал контроллеры наизусть до последнего бита-флага, систему команд ассемблеров тоже (8051, AVR).
После этого я освоил по верхам больше десятка языков. Оказалось что ситуация еще хуже чем в Сях — критические изменения в версиях комплияторов, народ холиварит на форумах и спрашивает такие вещи которые на ассемблере прозрачны и понятны сразу. Я допускаю что официальную документацию мало кто читает, но судя по всему документация не раскрывает всего. Требуются годы боли и унижений.mayorovp
01.12.2021 13:32Это вам кажется, потому что вы уже выучили весь ассемблер. Те программисты, которые выучили свои языки программирования, насчёт основ тоже не холиварят — они их просто знают. А те области, вокруг которых идут холивары, как правило являются межязыковыми.
Я долгое время сидел на ruSO и отвечал на глупые вопросы начинающих, и могу сказать что там почти нет вопросов относящихся именно к пониманию языка. Большинство проблем растёт либо из непонимания алгоритмов, либо вовсе из неспособности провести декомпозицию задачи. Ещё есть проблемы непонимания некоторых библиотек, но это снова проблемы библиотек, а не языков.
johnfound
01.12.2021 15:52-2Если отбросить троллинг,
Никакого троллинга. Я действительно считаю, что ассемблер лучше. И что на разных платформах надо писать код отдельно и на ассемблере. Только так можно использовать хардуер полностью. Да и работы больше будет для программистов. И программисты будут более квалифицированными.
Конечно, ассемблер это зло,
Это не факт и совершенно не следует из вашего поста.
На ассемблерах я писал годами и когда перешел на Си я в голове постоянно прокручивал как эти конструкции выглядят на ассемблере и что происходит в процессоре.
Я перестал писать на ЯВУ когда осознал что все время приходится бороться с компилятором, чтобы он выдал тот код, который мне нужен. Зачем, если можно сразу написать то что нужно?
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?
Повторюсь: я не говорю про низкоуровыневые коды, я про общий случай (судя по вашим предыдущим комментариям, вы «топите» за ассемблер для всего).johnfound
01.12.2021 17:13-1Внимание, вопрос: зачем писать одно и то же 9ю (3 цикла на 3 архитектурах) разными наборами инструкций на ассемблере, когда есть ровно одна работающая конструкция на C?
По той же причине, из за которой я пишу здесь на русском, а не на эсперанто. Потому что общение на родном языке собеседника (даже если и корявенько ) обеспечивает лучшее взаимопонимание между сторонами. В частности, между машиной и программистом. ;)
DirectoriX
01.12.2021 20:07Если вы пишете код только под одну платформу, и точно знаете, что этот код никогда не будет переноситься на другую — да, конечно, ассемблер имеет намного больше смысла…
- … особенно если нужна критическая производительность
- … особенно если ваш код никто кроме вас и других товарищей по ассемблеру не читает (ассемблер сильно менее понятен, чем C, а ведь есть и гораздо более выразительные языки)
- … особенно если вы на 100% уверены, что напишете код не менее производительный и/или компактный, чем современные оптимизирующие компиляторы (а это ой как не факт)
Нет, я не призываю вас отказываться от ассемблера для решения ваших задач, но заявление «ассемблер лучше С» (без специфики, без конкретных задач) слишком смелое.johnfound
01.12.2021 22:52ассемблер сильно менее понятен, чем C, а ведь есть и гораздо более выразительные языки
Это совершенно не так. Просто ассемблер мало знают и мало пишут на нем. Для меня например Лисп совершенно не понятен. Но это не значит что Лисп менее понятен в принципе. Он только мне непонятен.
forthuser
01.12.2021 23:37+2Ассемблеров от производителей, например, разных контроллеров много (AVR, MSP430, ARM, PIC, RISC-V, 8051, STM8, Z80, M6800, DSP...) с разной системой команд и способами в целом их использования.
Как возможно безболезненно перенести код программы с одного контроллера в другой используя ассемблер?
(при том ещё и аппаратная составляющая у разных контроллеров предполагает определённые шаги по работе с ней)johnfound
01.12.2021 23:48-1Как возможно безболезненно перенести код программы с одного контроллера в другой используя ассемблер?
А кто сказал что должно быть "безболезненно"? Совершенство постигается через боль. Как говорят, через тернии к звездам!
Безболезненно они хотят!
П.С. А еще говорят – глаза боятся, а руки делают. Часто этот процесс намного легче, чем кажется.
forthuser
01.12.2021 09:48+1Один из лучших «ассемблеров» — это Forth (Форт). ????
У него, даже, и встроенный ассемблер может быть на свой лад.netch80
01.12.2021 11:02+2Для современного железа затраты на call/ret чудовищны, так что для «гражданки» Forth уже не выйдет за нишу клея для загрузчиков и тому подобного (если без перерабатывающего супероптимизирующего компилятора, после которого дух языка, считаем, подменён чем-то другим).
А вот для военного или космического применения Forth вполне вкусен — гарантированный не исправленный никакими подозрительными оптимизациями выхлоп, в сочетании с аналогичными мерами на уровне процессора (например, никаких кэшей памяти), и средствами защиты от сбоев, наверняка будет обязателен, чтобы безопасно лететь хотя бы к Марсу…forthuser
01.12.2021 12:02+1Чужие: странная архитектура инопланетных компьютеров (c RTX2010).
А, вот для «гражданки», вообще нет контроллеров с MISC построением,
если не считать экзотичный GA144 на который, кстати, делали
и проект «Си» компилятора — Chlorophyll Language and Compiler. ????
Статья из журнала «Компоненты и Технологии»:
Процессоры GreenArrays — GA144
Хабр статья: GA144: русские спецификации процессоров
P.S. Хотя в кремнии и в России есть сделанные «MISC» кристаллы К1894,
В Минском Интеграле — K1881BE1T (IN16C)
ignat99
01.12.2021 12:28+2Согласен с вами. А хорошо бы если вы статью написали по теме Fort на хабре. И можно отдельно про сшивку кода. А если бы коснулись темы малой площади занимаемомой форт-процессором на ПЛИС, то было бы совсем идеально.
История про оживление реальной железки Fort-ом так же преведствуется.
ionicman
29.11.2021 17:49+2int 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?
С чего указатели, даже содержащие одни и теже данные, должны указывать на одно и тоже место? В общем случае они всегда не будут равны. Хочешь чтобы были равны — породи их от одного адреса.
Для меня указатель — это просто адрес и размерность — и ничего больше, почему из сравнивать нельзя? Даже если этого адреса не существует? Видимо, я давно стандарты не читал )))mayorovp
29.11.2021 19:03+1Вопросы вызывает тот факт, что при некоторых ключах некоторого компилятора printf выводит сначала два одинаковых числа, а потом признак их неравенства.
ionicman
29.11.2021 19:11Просмотрел, что выводятся указатели, а не то, на что они ссылаются. Тогда вопрос снимается.
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."
Travisw
29.11.2021 20:22-1Указатель в СИ это просто какой-то набор шестнадцатеричного числа, если бы вы знали как операционная система реализуют эту фичу, вы бы знали что адрес указателя это не адрес железный, а есть прослойка в виде виртуальной памяти и получается страница с виртуальной памятью в которой находятся указатели соотносятся с памятью железа, так работает операционная система
insecto
29.11.2021 22:20+5А как насчёт систем без виртуальной памяти? А как насчёт систем где указатель это больше чем одно число? А как насчёт систем где указатели на float и указатели на функцию живут в разных адресных пространствах?
mayorovp
29.11.2021 22:24+1Я-то понимаю что такое указатель и почему оно так происходит, но это понимание никак не делает язык проще и дружелюбнее.
rg_software
29.11.2021 18:17+16Статья интересная, спору нет, но с тоном и отдельными колкостями категорически не согласен. Вы не просто описываете объективно существующую проблему и приводите примеры и мнения (что нормально), вы намеренно давите на эмоции и пользуетесь чернушными риторическими приёмами. "Видимо, Торвальдс плохо изучил Си", "Кармак плохо изучил Си" -- это что вообще такое? Кармаку не было интересно запускать свой код на пятнадцати платформах и десяти компиляторах -- он убедился, что его хак работает на конкретной интересующей его комбинации, и всё. Вероятно, он не хуже нас знает, что такой метод стоит на шатком фундаменте.
Аналогично, человек, кладущий в память два int-значения, и вытаскивающий обратно float, зачем-то это делает. Вероятно, у него есть на то хорошая причина, но нельзя же не понимать, что ты суёшься ну в очень уж тёмный угол, в котором законы зыбки. В этом отношении юзер felix-gcc из диалога ни разу не прав: у него есть легальный способ получить нужное ему поведение, но он из принципа делать этого не хочет. Он хочет, чтобы его Ариан упал, а виноваты были разработчики компилятора.
Язык C страдает от массы болезней, порождённых особенностями его развития. Некоторые вещи нельзя получить легальными способами, и на то должны быть официальные compiler-specific пути, за которые отвечают разработчики компиляторов. Некоторые беды, по всей видимости, неискоренимы, но опять-таки, люди, выбирающие Си из массы вариантов, чем-то же руководствуются. Ну а так, да, пусть будет "более лучший язык". Например, Ада специально спроектирована, чтобы подобные ошибки предотвращать. На ней, кстати, код "Ариана" написан.
Gordon01
29.11.2021 21:16+10"Видимо, Торвальдс плохо изучил Си", "Кармак плохо изучил Си" -- это что вообще такое? Кармаку не было интересно запускать свой код на пятнадцати платформах и десяти компиляторах -- он убедился, что его хак работает на конкретной интересующей его комбинации, и всё. Вероятно, он не хуже нас знает, что такой метод стоит на шатком фундаменте.
Втройне иронично, что сишный код движков Кармака — один из примеров наиболее легко портируемых проектов на этом языке за всю его историю. DOOMаю, все знают что DOOM портировали буквально на все, для чего есть компилятор си и необходимые устройства ввода.
Это уже потом новая волна с/с++ программистов начала молиться на UB и шеймить всех, кто их использует, так и не поняв, что с/с++ не являются низкоуровневыми языками и, что более хуже, не отличая их от неспецифицированного поведения и результата.
nin-jin
30.11.2021 10:04+2felix-gcc говорит, что недопустимо намеренно и неявно создавать уязвимости в уже написанном и активно использующемся коде. Независимо от того, что там постфактум написано в спецификации. Дело совсем не в том, как именно проверять переполнение. Совсем не в этом. Это больше этический вопрос, чем технический.
rg_software
30.11.2021 11:52+4Теоретически этот аргумент понятен, но решительно неясно, что это означает на практике. Есть алгоритм, который работает известным образом на указанном наборе входных данных. За пределами этого набора он по сути не тестировался и работает бог весь как -- в зависимости от оборудования и фазы Луны.
Felix-gcc прикопался к конкретному виду UB, но на практике ведь UB везде, начиная с хрестоматийных
f(++x, ++x)
, и я не удивлюсь, если даже сами авторы компилятора не знают, как именно выполняетсяf(++x, ++x)
на текущей версии, ибо зачем им это знать? То есть в мире felix-gcc мы должны сначала прогнать миллион тестов на миллионе видов UB, потом закрепить это всё в тысячестраничном документе и следить, чтобы не дай бог новая версия компилятора ничего не сломала.nin-jin
30.11.2021 12:24+1Да, представьте себе, стандартов приходится придерживаться. Будь они де-юре или де-факто. А даже если хочешь сломать обратную совместимость - делать это надо аккуратно. Либо пройдясь по всем проектам и поправив их самостоятельно, либо выпустив автомиграцию, либо кидая ошибку на потенциально опасном коде, либо предоставив флаг, который по умолчанию выключен. Но уж точно не открывать дополнительные дыры там, где их не было.
rg_software
30.11.2021 13:23Так проблема в том, что непонятно, с чем совмещать. Юзер пишет, и говорит, что в ревизии x.5 работало так, а в x.6 работает эдак. Мы чешем репу и выясняем, что действительно так, хотя мы даже об этом не подозревали. Окей, поправили. А потом находится другой юзер, который сообщает, что в ревизии x.4 было вообще иначе, и что теперь делать?
С моей колокольни тут нет никакого "де-юре" и "де-факто", потому что... гм, ну вот представьте себе ситуацию. Вы пишете инструмент и даёте какие-то гарантии, и при этом вы решительно и никак не хотите ничего обещать сверх того, потому что нынешняя реализация -- она просто вот такой получилась. Вы же не можете всё предусмотреть и на каждом углу тыкать
assert()
, если вход некорректен. А так получается, что у меня нет никакого способа защитить себя от юзеров, которые всегда сочту, что вот если оно случайно сейчас работает так, то теперь оно должно быть так отныне и вовеки веков.
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, потом закрепить это всё в тысячестраничном документе и следить, чтобы не дай бог новая версия компилятора ничего не сломала.
В моей модификации этого мира есть контекстная установка «выполнять все вычисления параметров функций слева направо», которая действует по умолчанию, но, если автор кода уверен, он её отменяет для конкретного блока (исходного файла). Далее сначала пишется корректный код, а затем после его проверки — производится разрешение оптимизаций.rg_software
30.11.2021 13:31Это не undefined, это unspecified behavior.
Да, согласен. Но я думаю что в мире felix-gcc разницы нет: если я запустил свой код, и он выдал на выходе, скажем, нуль, то теперь нуль должен быть всегда, при любых обновлениях компилятора, и неважно, UB там или UC.
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".MiIs
29.11.2021 19:24+1Выше уже написали, что уже есть паскалеподобный язык ADA, который был создан на 10 лет позже С на основе задания министерства обороны США для безопасной разработки, а также созданный на его основе язык Spark программы на котором если скомпилировались, то с большой долей вероятности они не буду содержать ошибок вообще.
Минусы этих языков - многословны (как и все паскалеподобные), серьезные инструменты и поддержка - платны, немного медленоватее С/C++ - на тестах , в связи с внутренними проверками ошибок, чуть быстрее java, но в отличие от java не жрет столько памяти и не имеет задержек сборки мусора.
Языки применяются в критических к ошибкам областях: министерстве обороны США, авиастроении, астронавтике, разработке медицинских приборов, электронике.
GBR-613
29.11.2021 20:57Многословны (как и все паскалеподобные) - потому что "explicit is better than implicit". Но я согласен, что в Аде с многословностью увлеклись и переборщили. Чем плох Free Pascal? Если не считать отсутствия long jumps и т.д.?
Серьезные инструменты и поддержка платны - потому что ими мало пользуются, в том числе в мире OpenSource. Больше бы использовались - было бы больше и бесплатных. Опять таки - чем несерьезен Free Pascal?
Немного медленоватее С/C++ - вот именно немного ! Когда-то ядро Windows писали на ANSI C, потому что C++ немного медленоватее. Но ведь уже давно не пишут!
Praksitel
30.11.2021 18:50Доработанный Паскаль - это Java.
GBR-613
30.11.2021 19:10Есть в этом нечто, но Java как таковая для ядра ОС непригодна по ряду причин. Например, классы уместны не всегда и не везде. Такая вещь как StringBuffer, не может не быть медленной по сути своей. И все, где вместо явного освобождения памяти - сборка мусора, не может не быть медленным. Есть еще причины. А если это в Java изменить, то получится обратно Pascal.
quwy
30.11.2021 22:08Исправить очевидные глупости, на которые жаловался Керниган в своей знаменитой статье
Та статья -- банальный троллинг противников по холивару (весьма горячему в те времена). Есть похожая и про C.
Вставить несколько необходимых вещей типа long jumps, которых, собственно, должно быть не так уж и много
А можете как-то обосновать необходимость сего адского извращения? Проблем от goto слишком мало, хочется еще больше? Оно без проблем реализуемо средствами встроенного ассемблера, но ЗАЧЕМ?
Чтобы была как минимум опция начинать индексы массивов с 0, как все люди делают
В паскале любой массив можно начинать с любого числа, никто не запрещает начинать с 0, если хочется.
KanuTaH
29.11.2021 19:06+9В статье много лишних эмоций, а по технической части она, честно говоря, довольно банальная. Подобное писали и 10 лет назад, наверняка писали и 20 лет назад (просто лень искать), и, что самое забавное, продолжат писать и через 10 лет, да, возможно, и через 20 лет тоже. Я такие называю "за все хорошее и против всего плохого" или "статья ради срача".
netch80
29.11.2021 19:21> Подобное писали и 10 лет назад, наверняка писали и 20 лет назад (просто лень искать)
Вот как раз и хочется, чтобы не нужно было дальше такие статьи писать, а вместо этого можно было писать хотя бы «вставьте вот эти 3 строки в начало каждого исходника, и ваши зубы всегда будут белые и пушистые». А ещё лет через 5 — «перестаньте писать эти 3 строки, уже не нужно».
Sdima1357
29.11.2021 19:19+6Смерть "C" неизбежна как крах капитализма! Победа коммунизма неизбежна! Даешь Раст на всех платформах включая 8bit AVR! . Rust on Bare metal !!!. Старичков (Торвальд и компания) на пенсию. Офигеваю я от студентов...
aversey
29.11.2021 19:30Мы вроде такого не писали. =)
Sdima1357
29.11.2021 19:54+5То что Вы написали - даже обсуждать лень. Можно сформулировать короче - серебрянной пули не существует и язык Си - не идеален. Нельзя удовлетворить всех, всегда будут недовольные.
netch80
29.11.2021 19:56+3Но когда, чтобы удовлетворить заметно больше желающих, надо сделать очень немного — то обидно, что это не делается.
Sdima1357
29.11.2021 20:09+2Это потому что сначала компилятор Си писали его пользователи, а потом абстракционисты. Но язык в этом не виноват
„Знаешь загадку про верблюда? Что такое верблюд? Это лошадь, проект которой составлял творческий коллектив.“ — Том Уэйтс
ignat99
01.12.2021 13:51-2Всё точно. Обдумываю переход на ламповую технику и жалею что в МИЭТ не пошел на Антенно-фидерные устройства, а пошёл на ПКИМС, поддавшись стадному инстинкту.
По крайней мере на ламповой технике и общий плюсовой провод и набор команд сокращённый
PsyHaSTe
01.12.2021 03:56Я бы переформулировал как то, что развитие ЯП не стоит на месте и новые языки будут скорее лучше чем старые, и чем больше времени между их появлением тем лучше в среднем будет новый язык.
Альтернативой будет предположить, что именно си это золотой стандарт эпохи, и молодежь нынче не та пошла и языки делать разучилась
Sdima1357
01.12.2021 09:15+1Молодежь нормальная, такой она и должна быть. Тема просто избитая и исхоженная как "смысл жизни" .
ignat99
01.12.2021 13:53Хорошая техника стоит дорого и по сей день. Сейчас один хороший АЦП\ЦАП стоит дороже 1500 евро. Вот такие устройства в комбинации с ламповой техникой и есть наша альтернатива.
AlexTheLost
29.11.2021 20:14+1Было бы действительно неплохо иметь язык который закрывает некоторые проблемы в Си и дает разумное кол-во синт. сахара, при этом не сильно убегая от классики. Но к сожалению такого нет. Все новое что выпускали, С++ или Rust, с моей точки зрения является синтаксическим извращением, от которых код становиться только сложнее.
В целом с Си не все так плохо. Пишу под микроконтроллеры. Особенных проблем не возникает, тем более что есть масса линтеров.
insecto
29.11.2021 22:25+3Было бы действительно неплохо иметь язык который закрывает некоторые проблемы в Си и дает разумное кол-во синт. сахара
Неплохо для чего? Зачем такой язык может пригодиться? Переписать на нём ядро Линукс со всеми драйверами? Или coreutils? Или user32.dll на нём перепишут? Кто им будет пользоваться и зачем? Работники каких областей создадут вокруг него экосистему?
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. Там, благо среда исполнения тоже более-менее отвязана от аппаратуры, и оно если скомпилировалось, то так же и работать будет.GBR-613
30.11.2021 19:21+1Я до такой степени согласен, что я бы оптимизации компилятора в С вообще запретил. В крйнем случае, разрешил бы печатать warnings. Если ты ожидаешь от компилятора оптимизации, значит тебе нужен не С, а Java или С#.
Что же касается С++, то вот его я бы убил точно. Ну кому он сейчас нужен? Как замена ассемблера он не годится, а как "С с классами" есть Java и С#, которые и лучше, и проще.Sdima1357
30.11.2021 19:29+1я бы оптимизации компилятора в С вообще запретил
Как хорошо что это не от Вас зависит. Вызывайте с -O0.
Что же касается С++, то вот его я бы убил точно
C++ закуклился и спрятал плюсы под совместимость с "C".
Mingun
29.11.2021 20:24Ответ кроется в тексте стандарта. Чтобы всё-таки дать теоретическую возможность программистам писать низкоуровневые процедуры, а значит непереносимые, было введено ещё одно понятие — неопределённое поведение (undefined behavior, раздел 1.6, "DEFINITIONS OF TERMS"):
...
В общем случае невозможно узнать, каким образом будет обработано неопределённое поведение в программе при трансляции исходного кода. Поэтому единственный способ написания переносимых программ на языке Си — это полное избегание неопределённого поведения при разработке.
Божественно
wander
30.11.2021 10:17Просто первая цитата во-первых не может использоваться без контекста, потому что говорит о том, "что" было в начале пути. Во-вторых она несколько смещает поле зрения, что приводит к искаженному восприятию - создается впечатление, будто бы UB специально придумали, чтобы им пользоваться в коде. Однако это совсем не так.
SShtole
29.11.2021 20:51-5Искушённый в вопросах оптимизации компилятор может решить, что вызов memset здесь лишний, и спокойно удалит его из тела функции.
Э-э-э… А что это за компилятор такой?
Мне кажется, это не оптимизация, а чистой воды вредительство. Что значит: «Использовать memset не хочешь ты! yoda.jpg»? Что значит «выкинуть вызов»? Если такая оптимизация входит в какой-нибудь стандартный пакет O1 или O2, пусть такой компилятор лучше выкинет себя на помойку.
Прикол будет, если мне сейчас расскажут, что так ведёт себя, например, VS и «всё, что жил — всё зря».rg_software
29.11.2021 20:58+9Так ведут себя все современные компиляторы C++ точно, и подозреваю, что C тоже. Сильно сомневаюсь, что комментатор выше прав, но его теория выглядит рационально.
SShtole
29.11.2021 21:13ruomserg Не совсем понял его/вашу мысль. Как связана оптимизация шаблонного шлака с выкидыванием вручную прописанного вызова memset? Компилятор разве не может различить эти две ситуации?
Ritan
29.11.2021 21:35+1https://godbolt.org/z/n84ej87xP
Вы видите там вызовы memset? Просто нужно знать язык, на котором пишешь и знать, что memset не даёт гарантий безопасного затирания содержимого памяти
SShtole
29.11.2021 21:41Как это связано с шаблонами, которые, якобы, спровоцировали такую агрессивную оптимизацию?
Что касается «memset не даёт гарантий» — да, теперь буду знать, спасибо.
rg_software
29.11.2021 22:02+8А что такое "вручную"? Забудем о шаблонах. В C есть простые обычные макросы. Вы в макрос запихнули несколько инструкций. Потом этот макрос оказался внутри другого макроса, а другой -- внутри третьего. И в итоге когда это всё развернётся, у вас окажется, например, фрагмент вида
a = 5; a = 15; a = 7;
И теперь по вашей логике нельзя выбросить первые две строки? Ведь программист написал, значит, знал, что делает.
ruomserg
30.11.2021 07:44+2Ну как бы я помню компиляторы, которые делали именно так — генерировали три присваивания. И если указать ключ, разрешающий арифметические оптимизации — то схлопывали первые два. Причем не всегда, а только когда все три присваивания были «достаточно локальны». Последнее, впрочем, видимо было связано с небольшой глубиной просмотра оптимизатора (все-таки тактовая частота в мегагерцах была тогда...). Точно так же осторожно компилятор выносил инвариант за пределы цикла, и так далее.
И мне кажется, что для «С» — это правильная модель. Идеология «С» в 80-е годы была именно что «высокоуровневого ассемблера». Предполагалось, что программист достаточно хорошо понимает — что и для чего он пишет — а дело компилятора, просто перевести это в команды процессора. И оно примерно так и работало — и все были более-менее довольны. Проблемы начались в тот момент, когда началась автоматическая кодогенерация. Поскольку это не живой человек — оно свой сгенерированный код оптимизировать вдумчиво не собирается. А нужно было после сборки чтобы оно хоть как-то шевелилось. Ну и всем показалось, что компилятор — самое подходящее средство чтобы им лечиться. Благо тактовые частоты подросли, теория компиляторов подтянулась — и фактически компилятор начал переводить в команды не тот код, который написал программист, а другой код — относительно которого он (компилятор) может доказать его (другого кода) эквивалентность исходному относительно некоторого абстрактного вычислителя.
Для C++, собственно, выхода не было — либо надо выбрасывать темплейтное мета-программирование (и идею SFINAE — инстанциировать тучу специализаций, и из них выбирать наиболее подходящую), либо соглашаться что компилятор будет компилировать не написанный человеком код, а эквивалентный ему над абстрактным вычислителем. Для языка «C», с моей личной точки зрения, это является большой ошибкой. В «C» нет встроенных механизмов генерации больших объемов dead code. Поэтому компилятор должен компилировать в ассемблер ровно то, что написано — возможно применяя некоторые локальные оптимизации, если таковые разрешены флагами при сборке.0xd34df00d
30.11.2021 09:34+1и идею SFINAE — инстанциировать тучу специализаций, и из них выбирать наиболее подходящую
В SFINAE как раз множество специализаций не инстанциируется. F там Failure означает.
ruomserg
30.11.2021 11:11Я не буду спорить, потому что давно не смотрел во внутренности компиляторов современных. В моем представлении, когда они начинают процесс инстанциирования шаблонов — это может порождать процесс инстанциирования других шаблонов — и они генерируют целый лес в синтаксическом дереве (если вообще используется это представление...). И дальше два варианта — либо если где-то инстанциирование наткнулось на непреодлимую проблему, то откатывать обратно дерево и начинать сначала, либо оставлять куски в надежде что они скоро понадобятся снова. Я подозреваю (это мое ничем не подкрепленное спекулятивное суждение), что для увеличения скорости компиляции — они оставляют в дереве как можно больше того что сумело инстанциироваться, и полагаются на оптимизатор, который вычистит то, что в конце-концов не пригодилось…
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 и прочих вещей, которые мы так любим. Я бы сказал, в текущей статье ещё очень малоагрессивные виды оптимизаций обсуждают.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». В этом случае он останется надолго востребован и актуален…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% этих проблем не чувствуется даже опытными программистами.ruomserg
01.12.2021 00:23Я боюсь, что мы сейчас начнем придумывать новый стандарт «C». :-) Но смотрите — моя идея примерно следующая:
1. Арифметические оптимизации разрешены — потому что арифметика является как раз подмножеством «хорошо определенного» вычислителя. Все существующие UB там переводятся в target-specific behavior. То есть компилятор генерирует команды для target-платформы, а будет ли при этом в рантайме аппаратное прерывание, потеря точности, или процессор зависнет — его не волнует.
2. Вынос инварианта за цикл — только по отдельной опции. Если надо быстро перемножать матрицы — это должна делать библиотека. При наличии указателей — программист может выразить нужное ему поведение, вынести нужные условия наружу и компилятору не стоит в это вмешиваться.
3. Константные выражения — вероятно как в случае с арифметикой, по-умолчанию можно.
В целом, я бы оптимизации начиная с reordering внутри блока начал ограждать красными флажками в виде опций. А dead code elimination — уже даже не флажками, а светящимися шарами пару метров диаметром (как провода ЛЭП в авиации :-)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. Привет от Хоара.)
rg_software
30.11.2021 16:34+1Не могу сходу ссылку привести, но в паре кликов отсюда (где описываются трюки похлеще здешних) говорилось, что отследить UB для компилятора -- это штука технически почти неосуществимая. Современный тулчейн типа llvm устроен так, что отдельные оптимизации и прочие трюки существуют во многом независимо и работают с промежуточными представлениями, которые уже очень далеко отстоят от исходного кода. Поэтому отделить "вот эту кашу, которая получилась из рукописного кода" от "вон той каши, что вышла из раскрытия макросов, ассемблерных вставок и всяких прагм" уже никак не выходит.
0xd34df00d
30.11.2021 20:23+3В целом, я бы убрал из стандарта понятие «UB» и потребовал чтобы компилятор во всех этих случаях либо выдавал синтаксическую ошибку, либо генерировал команды, выполняющие ровно те действия, которые написаны в исходном файле.
Проблема в том, что некоторые из этих случаев неразрешимы по Тьюрингу.
Например, наличие бесконечного цикла без сайд-эффектов и пары дополнительных условий — UB (и компилятор имеет право предполагать, что таких циклов не бывает), но определить, является ли конкретный цикл бесконечным, эквивалентно проблеме останова.
ruomserg
01.12.2021 00:13Ну так я и не требую от компилятора детектировать UB во всех случаях. Если компилятор не может детектировать UB — он тупо генерирует код, выполняющий то, что написано в файле. То есть если написано int x=0;float *p=&x — то берется, мать его, адрес «x» (чем бы это значение не выражалось в целевой платформе), и записывается бит-за-битом туда, где выделено место под указатель «p». Имеет ли это смысл на целевой платформе, будет ли в этом месте ошибка времени исполнения, зависнет ли после этого процессор — компилятор волновать не должно. Разработчик написал сделать — компилятор перевел. В «С» изначально предполагалось, что разработчик знает, что он делает когда пишет код. В отличие, например, от паскаля…
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 был близок к железу, а компилятор был очень прост).
А сейчас все компиляторы оптимизирующие, и перед нами стоит вопрос "как упорядочить хаос оптимизаций". На него пока нет ответа: кто-то добавляет атрибуты, кто-то пытается что-то там писать в предупреждениях.
ignat99
01.12.2021 13:59-1В сигнальных процессорах были так же две параллельные команды и иногда три. Вообщем ASM надо знать (или хотябы держать перед глазами схему регистров данного микропроцессора) перед тем как писать на СИ.
Gumanoid
29.11.2021 21:20+4Оптимизирующий компилятор старается сгенерировать такой код, который сделает как можно меньше действий, сохранив при этом наблюдаемое поведение программы. Если в memset передаётся адрес, который потом не читается, то конечно этот вызов будет соптимизирован.
Kircore
30.11.2021 09:27-1Если в memset передаётся адрес, который потом не читается, то конечно этот вызов будет соптимизирован.
В Си это невозможно предсказать.
Ritan
30.11.2021 11:19+4Если указатель не покидает пределы функции, где выделен - можно
F0iL
30.11.2021 12:36...и если он при этом не обозначен как volatile.
Ritan
30.11.2021 14:56Ну с volatile вообще мутное. Вот тут, например, вызов memset всё равно удалён. Хотя clang почему-то решил память обнулить всё же
https://godbolt.org/z/aK49aqT6d
Kircore
30.11.2021 23:59Нет, если у тебя не выделяется/очищается память при этом через malloc/free. Указателю ведь можно задать абсолютный адрес, который можно использовать за пределами функции.
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.
rafuck
29.11.2021 21:23+2SShtole
29.11.2021 21:36+1Вот оно как. Ну ладно, лучше спросить и выглядеть дураком, чем не спросить и остаться.
Но мне это всё равно кажется глубоко неправильным.
Sdima1357
29.11.2021 21:36+3Собака еще глубже. Допустим мемсет отработала и стерла секретные данные. Но нет гарантии что эта операция прошла дальше кеша если процесс к тому времени завершился. Теперь другой случай, когда эта часть памяти - ввод вывод (внешнее устройство). Вобщем на компилятор надейся , а результат проверяй!
rafuck
29.11.2021 21:54Ну, гарантии уйти дальше кеша вообще дать проблематично, если у вас процесс падает.
Sdima1357
29.11.2021 22:37Даже если не падает, а завершается законно- кеш не обязан сохраняться
rafuck
29.11.2021 23:10-1Не очень понимаю, к чему этот комментарий. Я отвечал вот на это:
Но нет гарантии что эта операция прошла дальше кеша если процесс к тому времени завершился.
Sdima1357
29.11.2021 23:20+1Комментарий к тому, что даже при нормальном завершении есть риск утечки данных
rafuck
29.11.2021 23:33-1Я бы даже сказал, что он есть всегда. Но все равно, какая-то несвязная беседа получилась… ,)
Sdima1357
30.11.2021 00:16+1Конкретно в Вашей ссылке шла речь о безопасном стирании чувствительных данных из памяти. И там рекомендация использовать вместо мемсет, которую компилятор может проигнорировать, другую, "безопасную " функцию. А я утверждаю, что они все опасные и проверять поведение программы нужно в любом случае. А "безопасные" ещё опасней, они явно указывают на опасные места и вызов легче перехватить
rafuck
30.11.2021 10:27-1А я пытался намекнуть, что это уже на грани паранойи. Ведь с чего все начиналось: компилятор может убрать вызов memset из кода. Вы в ответ говорите следующее: даже если не уберет, процессор работает с памятью через кеш, и есть вероятность, что занулится кеш-память, а до физической ничего не дойдет. Замечательно. Но, во-первых, эта проблема лежит в совершенно другой плоскости и не связана с языком программирования и компилятором. А, во-вторых, как вы это предлагаете проверять? Выдернуть планку, поместить в жидкий азот и под сканирующий микроскоп?
Sdima1357
30.11.2021 10:42Во первых я могу просканировать память рутом. Или отследить странички процесса перед выходом. Рутом или родительским процессом. Программа то моя , а не чужая, все обращения к ядру можно перехватить отладчиком.И даже если и чужая. Жидкий азот не понадобится.
rafuck
30.11.2021 11:18-1И как это поможет выявить несогласованность кеша и физической памяти?
P.S. На всякий случай. Минусую ваши комментарии не я, мне правда интересно.
Sdima1357
30.11.2021 11:26+1Элементарно. Пассворд останется в памяти. Ядро освободит страничку без коммита кеша при освобождении ресурсов процесса на его завершении
netch80
30.11.2021 11:41> Ядро освободит страничку без коммита кеша
В типовых современных архитектурах ядро просто не может управлять этим. Кэш процессора сбрасывается сам по себе, или можно ускорить этот сброс, но не отменить его.
rafuck
30.11.2021 11:47Я не уверен, что так можно (отменить сброс кеша в память).
(я всегда буду обновлять комментарии)
Sdima1357
30.11.2021 13:01+1Страница помечена для ядра как освобожденная(вот прям сейчас). В странице по определению мусор, с точки зрения ядра. Зачем ядру синхронизировать запись в кеше, которая ссылается на память в этой странице? Это лишние расходы времени и шины.
vanxant
30.11.2021 13:05Можете привести инструкцию какого-нибудь процессора, при помощи которой ядро может отменить запись процессором в write-back память из кэша?
Sdima1357
30.11.2021 15:01INVD — 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.
rafuck
30.11.2021 15:23Хм. Интересно!
Насколько я понимаю, эта инструкция используется в очень специфических случаях (например, при использовании части кеш-памяти в качестве адресуемой физической памяти).
cepera_ang
30.11.2021 15:23+2Последствия вызова такой инструкции в процессе нормальной работы будут весьма разрушительными.
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
cepera_ang
30.11.2021 16:06-1Просто мы в типичной программистской манере всё дальше и дальше удаляемся от исходного вопроса в совершенное не важные дебри и частные случаи.
— А что если нам надо память очистить, а компилятор выкидывает вызов memset, разве это дело? "
— Да пофиг, ведь у нас ядро может не дать кеш сбросить
— Эм, но ядро не будет так делать
— Ха, вот и инструкция для этого есть и вот тут есть какой-то никем не используемый процессор, где кешем надо вручную управлять, так что будет
— Эммм, а что если где-то есть процессор, который содержимое регистров на ЖК-дисплей выводит, а там камера стоит которая автоматически пароли считывает, значит ли это, что нам можно вообще не пытаться пароли в памяти зачищать?
Sdima1357
30.11.2021 16:15+1Просто мы в типичной программистской манере всё дальше и дальше удаляемся от исходного вопроса в совершенное не важные дебри и частные случаи.
Так вот без денег и остаются ,зачем обсуждать маловероятные сценарии:) .
Нет, надо просто проверять программу на целевых архитектурах. И такие экзотические сценарии тоже нужно предусматривать, если утечка данных достаточно опасна.
Современные процессоры позволяют программисту ошибаться миллиарды раз в секунду. :)
Вы не поверите сколько и каких делают проверок, например в медицинской аппаратуре, в приличных фирмах
insecto
29.11.2021 21:31+11Нуу, в такой дискуссии я на стороне стандарта и gcc. Чуваки пишут в коде какую-то чушь, и жалуются что она не работает. Например, переполнение при сдвиге знакового. Не думают же они, что компилер на каждый такой сдвиг вставит проверку на переполнение? Мы не для того используем Си.
Или вот это
int x; int *p = &x + 1;
это ж совсем за гранью, такое писать! Они ждали каких-то гарантий расположения переменных (а то и их существования) на стеке? Да ещё и с арифметикой указателей поверх этого? Безумие.Вот такой ассерт:
assert(a + 100 > a);
гдеa+100
это уже чёрт-те какое значение. Очень странной выглядит жалоба, что нельзя сравнить чёрт-те какое значение с другим значением, и на выходе не получить чёрт-те что.Я ещё понимаю Торвальдса и Кармака, у них список платформ ограничен и известен зараннее, им можно полагаться на то что int* и double* можно свалить в одну кучу, и не получить по рукам (да и то не всегда). А у комитета такой роскоши нет, им надо рассчитывать на потенциально любой компилер и платформу, даже те гипотетические, которых ещё нет.
Mingun
29.11.2021 21:42+2где
a+100
это уже чёрт-те какое значение.Ну, не факт. Если мы компилируем под определенную платформу, то это уже вполне определенное значение. Да, можно выдать варнинг, что эта конструкция непереносима, но неопределенное поведение задумывалось как возможность написать программу под специфическую платформу, а не как изощренный способ сказать "не пишите так". Если бы хотели последнее, просто сделали бы ошибку компиляции, и все. Но что-то где-то пошло не туда.
Или вот это
int x; int *p = &x + 1;
это ж совсем за гранью, такое писать! Они ждали каких-то гарантий расположения переменных (а то и их существования) на стеке?Ну вообще тут наоборот, увидев такое, компилятор не вправе размещать переменную не на стеке. Можно скопировать в регистр, но на стеке значение должно быть, т.к. берется его адрес и именно операция взятия адреса — первична, а не то, где так решил переменную разместить компилятор.
Вот уже надеяться, что
&x + 1
укажет наy
наверное странно… если только в стандарте не прописано, что переменные кладутся в стек в порядке их объявления. А то, чтоy
тоже будет на стеке следует из того, что берется ее адрес.insecto
29.11.2021 21:49+4неопределенное поведение задумывалось как возможность написать программу под специфическую платформу
Нет, для этого есть implementation-defined behaviour и unspecified behaviour.
изощренный способ сказать "не пишите так"
Нет, это изощренный способ сказать "это ваша забота делать так, чтобы этого не произошло, а не компилятора". И забота эта может состоять в явной проверке, в каких-то внешних гарантиях, в оценке стоимости ошибки, или ещё как, ваше дело.
netch80
30.11.2021 11:30+1> Чуваки пишут в коде какую-то чушь, и жалуются что она не работает. Например, переполнение при сдвиге знакового. Не думают же они, что компилер на каждый такой сдвиг вставит проверку на переполнение? Мы не для того используем Си.
А почему в таком случае переполнение при сложении или сдвиге беззнакового не происходит, остаются только N младших бит в любом случае? В чём такая принципиальная разница между знаковыми и беззнаковыми?
> А у комитета такой роскоши нет, им надо рассчитывать на потенциально любой компилер и платформу, даже те гипотетические, которых ещё нет.
Отлично — что мешает завтра появиться платформе, на которой «add a,b» вышибает нафиг, если carry flag поставился бы?
Только почему-то прогресс идёт в обратном этому направлении, в ARM и RISC-V деление на 0 проглатывается молча. Странные люди, наверно…
И платформ с тритами вместо битов никто не пишет…
А в C++20 вообще дополнительный код канонизировали как единственно возможный. Наверно, не рассчитывают, что завтра Intel скажет «всё, нафиг» и перейдёт на 1-complement…
[/sarcasm]
gregorybednov
29.11.2021 21:57+1Вспоминается статья Криса Касперски "Языки, которые мы потеряли". Тут статья немного о другом, но в целом получилось "На этот раз мы теряем и Си".
Если упрощенно (кому лень читать, но как по мне... в общем, прямо-таки рекомендую прочесть её), ту статью можно пересказать таким набором тезисов:
Сишник будет решать конкретную задачу, для чего напишет простую утилиту, а плюсист сначала придумает абстрактный API и по итогу сделает гигантский продукт, который будет от версии к версии всё жирнее и жирнее.
Си мог бы наверное развиваться в сторону полноценного метапрограммирования (с самомодифицирующимся кодом, с построением программ программами), но вместо этого мы имеем непрозрачные шаблоны C++, которые вроде как облегчают копипаст кода и... тут же создают тучу неоднозначностей, в которой вроде как "нет" ответственных, вроде того а что будет, если я попробую сравнить теплое с мягким (или хотя бы даже треугольник с прямоугольником) используя полиморфизм оператора сравнения.
Настоящие языки метапрограммирования, поддерживающие СМК (самомодификацию кода) на официальном уровне - а именно Лисп и Форт - по сути забыты (отдельное уточнение: здесь не соглашусь с Крисом - и тот, и другой язык жив, но назвать их существование достойным на фоне однотипных потомков языка C во главе "плюсами", а с учетом последних стандартов и описанных в статье фокусов с компиляторами уже и обыкновенного C, всё же язык не повернётся)
Ну и само собой пояснение: вопрос "жирности" не праздный - да, мы можем себе позволить на своих рабочих станциях такое количество вычислительных мощностей, которые Крису могли бы разве что присниться (причем, скорее всего, в страшном сне), но можем ли мы понять, как работает "жирная" программа, даже если нам полностью доступны отлично документированные исходные тексты?
rg_software
29.11.2021 22:08+2Ну если честно, все эти рассуждения выглядят как попытка всерьёз изложить тезисы известного баяна "Настоящие программисты не используют Паскаль". Ничего нового. Как говорил Страуструп, языки делятся на те, которые не любят, и те, на которых не пишут.
gregorybednov
29.11.2021 22:25-2Не могу согласиться. С такими доводами как у Страуструпа, во времена Кобола говорили бы то же самое про Кобол, и где он сейчас? Речь идёт как раз о том, и в статье Криса это подчеркивается, что в программировании решают отнюдь не люди, не технологии, а индустрия. И индустрии было выгодно, чтобы ходить в соседний ларёк можно только на космическом корабле с облётом вокруг Плутона - потребовалось делать продукты на заказ, имелись компьютеры, которые работали быстро только если программы были на Си - ну вот, как говорится, "получите, распишитесь".
Если и сравнивать "по Страуструпу", тогда уж уместнее это делать так: языки делятся на те, на которых людей всё равно заставят работать, и те, на которых они могли бы; в такой интерпретации данное суждение действительно неоспоримо.
rg_software
30.11.2021 07:24+1Кобол -- это немного другое. Кобол был изначально создан как нишевый язык для бизнес-приложений, и вам надо было днём с фонарём искать людей, которым он нравился даже тогда. Соответственно, в наши дни предприняты большие усилия по продвижению .NET/Java в качестве "дефолтного" языка для таких случаев.
То есть имелась ясно очерченная проблема (предметная область без приличного языка) и были сделаны хорошо подготовленные усилия для её преодоления.
Сейчас мы наблюдаем совершенно иной процесс: "маленький и приятный" язык, который изначально создавался в пику чему-то монструозному тоже превращается в монструозность, см. Python. А если не превращается, то остаётся нишевым. Можно посмотреть, как растёт с годами объём документации.
При этом распухание языка -- это ни разу не про распухание кода. Это про (а) реализацию модных фишек, потому что вот в языке X есть лямбды и дженерики, я в своём тоже такое хочу и (б) как раз про эффективность кода как по объёму, так и по скорости.
Заметьте, мы тут обсуждаем оптимизации gcc, которые все как раз направлены на рост производительности итогового приложения. Расширения стандарта C++ (move semantics, в частности) тоже про более эффективный код. Так что мы видим попытки сделать софт не толще и медленее, а ровно наоборот. Нередко скорость достигается ценой разбухания, но это отдельный вопрос.
WhiteWhiteWalker
30.11.2021 00:10-4Непонятно, почему разрабы гцц решили, что поведение можно менять от версии к версии? Была бы альтернатива, гарантирующая постоянство - и про гцц бы забыли как про страшный сон, но увы. целое сообщество позволяет группе людей портить всем жизнь в угоду удовлетворения своих нездоровых фантазий этих людей,
где-то я уже такое видел. Было бы неплохо запилить форк со стабильным поведением, возможно даже с адаптированным под современные архитектуры стандартом, собрав на это дело донаты на краудфандинге.kmeaw
30.11.2021 01:21Если gcc нетривиальным образом обновился, то он поменяет своё поведение. Иначе зачем его вообще обновлять?
Каким образом разработчики gcc должны отличать "хорошие" изменения поведения от "плохих"?
WhiteWhiteWalker
30.11.2021 19:41Имеются ввиду изменения в поведении, когда по стандарту UB, а по логике всё в порядке. примеры вроде как есть в статье.
Solovey572
30.11.2021 01:23Я сначала подумал, что это какой-то азиатский язык, а потом как вспомнил...
Sergey_zx
30.11.2021 02:03Одним из главных аргументов наезда на си является то, что он не так хорошо заточен для оптимизации как предлагаемая альтернатива.
Но странно. Почему то на тех самых альтернативных языках собранная программа, типа "hello word" занимает сотни килобайт. Причем работать автономно оно не может, а требует дополнительно всякого разного.
Но зато оптимайзер сэкономил сто байт памяти переменных.
По моему цель не оправдывает средства.
Верно что оптимайзер может перекрутить код самым причудливым образом. Но верно и то, что если программист не предполагает особых трюков, то он и не пишет неоднозначные конструкции.
А если предполагает что то недоступное пониманию компилятора, типа обнуления неиспользуемых переменных, то на это модуль оптимизация отключается.
Си, он как скальпель. Можно делать тончайшие операции, а можно сдуру зарезаться. Нет опыта и мастерства владения скальпелем, используй картофелечистку. Ей не зарежешься, но и ничего кроме ее задач и не сделаешь.
samoreklam
30.11.2021 07:00Полезная статья! (для меня)
Показало, ещё одну возможную проблему в моём коде. Хотя, я уже сталкивался с "strict aliasing" . Исправлял код, не вникая в суть проблемы. Теперь отнесусь более внимательно к стандарту. (и снова, долго и упорно буду проверять свой код)
А PVS-Studio обнаруживает подобные ошибки? (наверно да, но хочется узнать точно)
И раз уж здесь такая тема, то хочу задать вопрос!
Я сейчас пишу программу (сервер) и делаю тесты производительности с компиляцией через gcc без оптимизации и с -O3 . И по результату, скорость работы почти одинаковая. Что это означает? (код хорошо/плохо написан? компилятор не находит то, что можно оптимизировать? или ещё что?)vanxant
30.11.2021 08:59+1Подозреваю, что если "сервер", то основную часть времени он сидит в ядре (epoll или что там у вас). Эффективно перекладывать байты с диска/БД в пакеты можно и на PHP, и на node.js =)
app-z
30.11.2021 07:27+1float 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
piratarusso
30.11.2021 09:08Некоторая ясность наступает, если мы начинаем различать язык как спецификацию и компилятор. Я припоминаю историю про то как один программист взялся за переписывание программы с на ... с. С одного компилятора на другой.
Современный компилятор GNU C++ можно заменить разве что на LLVM. И обе эти программы написаны на с и обеспечивают широкую поддержку современных платформ . Так что если вы хотите избавиться от с, а вам следует предложить ЯП, пригодный для написания компилятора. Но поскольку в этом направлении никто не двигается, то очевидно все новые языки будут лишь сокращать область применения с, но заменить его полностью не смогут. до тех пор, пока не будет реализован набор компиляторов, написанных на них с достаточным охватом современных платформ
netch80
30.11.2021 09:11> вам следует предложить ЯП, пригодный для написания компилятора. Но поскольку в этом направлении никто не двигается
В каком смысле «никто»? Подавляющее большинство новых языков после C++ пригодны к тому, чтобы сделать на них свой компилятор.
> то очевидно все новые языки будут лишь сокращать область применения с, но заменить его полностью не смогут. до тех пор, пока не будет реализован набор компиляторов, написанных на них с достаточным охватом современных платформ
В данном случае это факт инерции — уже есть две мощнейших реализации на C/C++ и переписывать их на нечто новое никто просто так не будет.piratarusso
30.11.2021 09:33Вот и получается, на текущем уровне развития технологий с незаменим.
netch80
30.11.2021 09:58> Вот и получается, на текущем уровне развития технологий с незаменим.
Если «уровнем развития технологий» называть таковой чисто из-за лени, то да, согласен. Но это не значит, что C/C++ не могут уйти в свою специфическую нишу «для компиляторов», где доживут ещё лет 20. Всё может — как только появится полноценная замена, поддержанная кем-то толстым.piratarusso
30.11.2021 12:59«Уровнем развития технологий» я называю доступность для практической работы инструмента, иногда употребляют слово "экосистема". Универсальных инструментов на самом деле не так уж и много. С на текущий момент пригоден для большого количества платформ и задач, он обеспечен своей экосистемой для широкого применения, экосистема rust, для примера, куда более скромна, потому и применимость его куда более ограниченна_. Ну а, скажем, более экзотические языки, при всех своих достоинствах, обычно не выходят за пределы своих ниш.
0xd34df00d
30.11.2021 09:36+4Так что если вы хотите избавиться от с, а вам следует предложить ЯП, пригодный для написания компилятора.
Любой язык из ML-семейства куда более пригоден для написания компиляторов, чем C.
piratarusso
30.11.2021 13:01-1Потенциально пригоден - возможно.
0xd34df00d
30.11.2021 20:27+3Почему потенциально? Компиляторы на нём вполне пишутся. Компиляторы всяких ML'ей написаны на этих ML'ях, компилятор хаскеля написан на хаскеле. Компиляторы всяких ресерч-языков пишутся на том же хаскеле. Я для своих проектов, где надо было написать компилятор или его кусочки, тоже выбирал совсем не С.
DirectoriX
30.11.2021 23:08+2Компиляторы на C пишут потому что а) так исторически сложилось и б) потенциально можно добиться очень высокой производительности.
Кстати, а вы можете назвать хотя бы один язык, который был бы принципиально непригоден для написания компилятора, учитывая что языки Тьюринг-полные, а значит на них можно написать эквивалентную GCC/Clang логику?
Я даже не очень сильно удивлюсь, если лет через 5 в новостях будет «игрок собрал компилятор подмножества C в Minecraft»piratarusso
01.12.2021 10:02Компиляторы давно пишут на чём попало. Это просто праздник какой-то . Особенно в последние пару лет популярна идея source-to-source compiler. Из ruby в javascript (opalrb.com)), ещё WASM кое где пропагандируют. Я не знаю, может быть в будущем появятся операционные системы, написанные на интерпретируемом языка.
Разногласия у нас только в пункте б)
б) потенциально можно добиться очень высокой производительности.
И тут я бы переформулировал. Существует toolchain, который обеспечивает цикл разработки ядра операционной системы, системных и прикладных программ. Если мы собираемся убить с, то мы должны это чем-то заменить. Ключевая часть этого toolchain - это компилятор с.
permeakra
01.12.2021 12:18Ключевая часть этого тулчейна - это форматы объектных файлов, утилиты для работы с ними плюс соглашения типа call conventions. От языка и компилятора Цэ они вполне отдираются, тот же gnu ld нормально работает c сlang
Другое дело, что Цэ - это как бы не единственный распространенный язык, символы которого один-в-один отображаются в символы объектных файлов. С этим да, надо что-то делать.
volodyaleo
30.11.2021 09:13На счет "пароль останется в регистре процессора". Размер регистра не позволит пароль там хранить из-за размера регистров. Абсурд в квадрате.
Разыменовывание нулевого указателя тоже идея сомнительная. Можно же просто этого не делать, что человек пытается этим добиться? Но на некоторых аппаратных платформах по абсолютному адресу 0 можно найти таблицу векторов прерываний. В эмбеддед большинство железа имеет абсолютные адреса (без MMU) обращения к ним может иметь смысл.
Человеку, допустившему такие ошибки, ничего не помешает при портировании кода на Rust просто взять и затолкать в блок unsafe всё (взяв первую ссылку которая бы посоветовала это сделать и не дочитать). Если следовать той же логике - rust тоже дает себе в ногу стрелять и поэтому надо заменить. + большой пласт ошибок с дедлоками и конкаренси никак не наловишь.
Если интересно писать на си безопасно, обычно применяют базовые вещи - юнит тесты, и санитайзеры: undefined behavior, address sanitizer, memory sanitizer, thread sanitizer. Однако если стиль кода хороший, чаще всего ловятся логические ошибки, от которых ни один язык программирования не сможет защитить, только тестирование. Также существуют стат анализаторы которые большой пласт ошибок ловят.
Аргументация слабая и если рандомно стучать по кнопкам явно не следует ожидать чего-то хорошего. Раньше автор умрёт от старости чем си.
П.С. я не против раст, а против слабых аргументов.
netch80
30.11.2021 09:17+5> На счет «пароль останется в регистре процессора». Размер регистра не позволит пароль там хранить из-за размера регистров. Абсурд в квадрате.
В 64-битном регистре помещается 8 символов ASCII. У многих пароли не длиннее. Если у кого-то длиннее — то знание первых 8 символов вместе с 1% личной информации может помочь догадаться об остальных.
В XMM, YMM соответственно 16 и 32 (через них любят копировать память). Редко какие пароли длиннее.
Прежде чем рассказывать про «абсурд в квадрате», примените арифметику.
> Разыменовывание нулевого указателя тоже идея сомнительная. Можно же просто этого не делать, что человек пытается этим добиться?
Прочитайте, пожалуйста, исходную статью ещё раз. Там есть все необходимые данные, чтобы понять автора.volodyaleo
30.11.2021 09:388 символов влезет, но если пользователь 9 впишет? И куда терминальный символ делся? Как быть с фактом, что пароль в регистре ничего не дает? При свитче контекста ос должен восстанавливаться контекст другой задачи автоматом операционной системой. Пароль в регистре что-то может дать, если подключился дебаггером и бог ассемблере, который может весь фотошоп отладить внимательно смотря в регистры и аллокации. Но от такого вообще ничего не спасёт.
netch80
30.11.2021 10:04+2> 8 символов влезет, но если пользователь 9 впишет?
Попробуйте внимательно прочитать мой комментарий.
> И куда терминальный символ делся?
Для задачи типа «подобрать пароль с 10 попыток» он не критичен.
> Как быть с фактом, что пароль в регистре ничего не дает?
Такого факта нет. Ознакомьтесь, пожалуйста, с проблемами неочищенной памяти в теме секьюрити.
> Пароль в регистре что-то может дать, если подключился дебаггером и бог ассемблере, который может весь фотошоп отладить внимательно смотря в регистры и аллокации.
И всё-таки ознакомьтесь с проблемами неочищенной памяти. Минимальный комплект проблем:
— Нарушение секьюрити в другом компоненте (библиотеке), через которую утекают данные (например, недочищенный padding).
— Дампы всяких падений, которые увидит админ.
— Spectre и тому подобные средства на чтение памяти.
Полный комплект сильно шире, но sapienti sat.volodyaleo
30.11.2021 12:37Не думаю, что другие языки не используют регистры и RAM. Соответственно это применимо ко всем языкам. Чем тогда именно си плох? Если в rust (да любой язык вместо) создать переменную строку, потом сравнить с другой, где строки будут?
И сколько байт от пароля окажется в регистре - детали имплементации. Может один байт. В других языках как будет дело?
Нарушение секьюрити в другом компоненте - также при чем тут си? В других языках такого нет? Например в Rust подключенная либа с unsafe (любой язык кроме rust также)
volodyaleo
30.11.2021 12:49Spectre - это аппаратный баг, который позволяет влезть в кеш процессора и считать мусор другого процесса вне каких-либо условий. Любые данные любого бинаря на любом языке даже с очисткой можно получить. Тут вообще его приплетать не стоит. Кеш процессора ни один код не контролирует. Также этот баг из-за механик предсказания перехода и спекулятивного выполнения. Это просто то, что вне языка программирования.
netch80
30.11.2021 13:32> Spectre — это аппаратный баг, который позволяет влезть в кеш процессора и считать мусор другого процесса вне каких-либо условий. Любые данные любого бинаря на любом языке даже с очисткой можно получить. Тут вообще его приплетать не стоит.
Стоит, потому что мы уже говорим о security, а не просто о функциональности. А она включает в себя то, что данные надо чистить как можно раньше, чтобы случайные каналы утечки, как по аппаратным багам, так и по ошибке, влияли как можно меньше.
> Это просто то, что вне языка программирования.
Да, поэтому в набор требований security входят и внепрограммные методы, например, вплоть до полной радиоизоляции отдельных компонент.
Но они выдвигают требования и на программный уровень, даже если эти требования только смягчают возможные последствия.
volodyaleo
30.11.2021 13:35+1Я понимаю, что си не без грешка. Но он и применяется сейчас в основном в эмбеддед. И да, рядышком улеплены везде ассемблерные вставки обычно в sdk так как работа в эмбеддед касается железа напрямую. Зачастую больше кода там "unsafe" так как приходится обращаться напрямую к регистрам, аппаратной части, DMA итд. Там и аут оф баунд используют на структурах во благо (в парсере протоколов кастуют буфер на структуру с массивом единичной длинны и вкладывают в структуру байтик длины, не безопасно, но видал такое часто...). Поэтому аргументы кажутся слабыми ибо рассматривался язык как высокоуровневый. Но это давно не так.
В мире эмбеддед еще есть свои ограничения по памяти, например 1кб под ROM. Есть специфические требования, например DMA не умеет работать с ROM и вызовет хардфолт. Иногда требуется вообще к какому-нибудь адресу пригвоздить данные. Я не пробовал rust, но подозреваю, что в тех местах, где меня надо "спасать", он не поможет. Ибо эмбедеры обычно берут готовые абстракции для очередей и мем пулов. Выключают heap. И работают на стат буферах, где ошибиться довольно сложно... Таблицу векторов прерываний на си пишут или asm, там вообще функции строго по адресам должны быть. Пишут стартап файлы свои, чтобы из своих секций загружать данные правильно.
В сухом итоге- из проекта безопасного кода не очень много. И он обычно просто дёргает библиотеки и обертки, там уже сложно косякнуть.
А spectre не страшен, если ты экзекьюшен с RAM выключил и отключил debug интерфейс (аппаратная защита).
netch80
30.11.2021 13:43> Я понимаю, что си не без грешка. Но он и применяется сейчас в основном в эмбеддед.
Ну с точки зрения application development — вероятно, да. С точки зрения остального… Linux на толстом сервере это embedded? А какая-нибудь Oracle DB?
Вы слишком сбоку смотрите.
> И да, рядышком улеплены везде ассемблерные вставки обычно в sdk так как работа в эмбеддед касается железа напрямую.
Очень мало где. И везде где можно вместо этого применить интринсики — переходят на них, потому что безопаснее, переносимее и понятнее.
> Поэтому аргументы кажутся слабыми ибо рассматривался язык как высокоуровневый. Но это давно не так.
Как правильно говорят рядом в комментариях, он остаётся высокоуровневым, но иначе.
volodyaleo
30.11.2021 09:41Прочитал, кто в своём уме будет сначала разименовывать указатель, потом проверять на ноль?
0xd34df00d
30.11.2021 09:45+1Например, результат инлайнинга одной функции в другую.
netch80
30.11.2021 10:06Ну в случае инлайнинга, скорее всего, в компиляторе эта логика не сработает (хотя кто их знает, этих писателей). А вот если код подставлен макрой — я бы дал 100%…
0xd34df00d
30.11.2021 10:24+1Ну это же легко проверить. И gcc, и clang ломают ваш код.
netch80
30.11.2021 11:11Ну это «немного» не то. Оно только удаляет бесполезную (по его мнению) проверку в контексте, где по условиям вызова «гарантировано» за счёт разыменования, что указатель не пустой. Так как разыменование безусловное, я бы это не считал диверсией.
А вот если бы был код типаint val = *ptr; if (!ptr) log();
в одной функции (существенно), и оно, определив, что есть проверка ptr, сочло, что чтение значения *ptr в val — UdB, и на этом основании что-то сломало (например, записало бы всегда 0 в val) — было бы интереснее. Но такого я добиться не смог.0xd34df00d
30.11.2021 11:18+1Как это не то? Изначальный тезис:
Прочитал, кто в своём уме будет сначала разименовывать указатель, потом проверять на ноль?
В коде выше сначала разыменование, а потом проверка. Вопросу удовлетворяет, ИМХО.
То есть, пример, конечно, глупый — я уже к вечеру перепутал, что надо инлайнить, и по-хорошему надо считывание вынести в другую функцию, но там всё будет то же самое.
Но такого я добиться не смог.
Оно ж в таком случае просто вырежет проверку — то есть, сломает код.
volodyaleo
30.11.2021 12:40Инлайн никогда порядок не меняет. Частенько приходится глядеть в asm или бинари. Если человек грамотный, то проверки делает в начале функции перед всеми вызовами. Или сначала делает проверку на ноль, потом вызывает верификаторы использующие адреса.
cepera_ang
30.11.2021 09:36+5Интересно сравнивать языки, один из которых нужно обмазать юнит-тестами, санитайзерами, стат. анализаторами, стилем кода, запинить версию компилятора, добавить фаззинга и всё равно не получить никаких конкретных гарантий и другой, в котором нужно специально постараться, чтобы конкретных гарантий языка избежать.
Было бы интересно посмотреть на процент проектов на С, в которых строго применяется все вышеперечисленное и процент проектов на Rust, в которых всё просто запихнуто в unsafe блок.
PsyHaSTe
01.12.2021 04:03+2Во-первых ансейф блок не гарантирует отсусттвия проверок: https://steveklabnik.com/writing/you-can-t-turn-off-the-borrow-checker-in-rust
Во-вторых стд останется в сейф блоках, да ещё и провалидированная сверху.
Так что даже в таком сравнении раст может выглядить интереснее
Flux
30.11.2021 09:42+1Мы без конца ругаем язык С, и, разумеется, за дело. И все же я хочу спросить — кто написал четыре миллиона строк говнокода?
Критика конечно уместная, но не раскрыта тема того как С должен умереть. В плане, куда денутся десятки миллионов строк легаси кода которые нужно поддерживать и что дадут людям вместо С? Только не предлагайте раст, компании не готовы к массовым суицидам сишников пытающихся написать на расте двусвязный список.
cepera_ang
30.11.2021 09:49+3как С должен умереть
Да также как Кобол, который вверху обсуждают. Конечно сам язык никуда не денется, и проекты на нём никуда не денутся, и никто их напрямую переписывать не будет. Просто всё больше и больше новых проектов будет создаваться на чём-то другом и со временем количество новых проектов на С упадёт до пренебрежительного уровня.
компании не готовы к массовым суицидам сишников пытающихся написать на расте двусвязный список.
Дались всем эти двусвязные списки… Мало того, что достаточно его один раз написать и всегда использовать, так это ещё и максимально неудобная для железа структура данных — скакать по всей памяти, вымывая кеши и таблицы ради непонятно чего.
redsh0927
30.11.2021 12:08+2так это ещё и максимально неудобная для железа структура данных — скакать по всей памяти, вымывая кеши и таблицы ради непонятно чего
Не обязательно выделять тупо каждый объект маллоком! Это лишь способ связать объекты в некий ряд по каким-либо правилам для мгновенной навигации между объектами без пложения всяких дополнительных таблиц (в которые именно что и придётся лазить, портя кэши, прежде чем всё равно обращаться к нужному объекту). Сами по себе объекты могут быть выделены как угодно и где угодно. Разумеется, объект не обязан быть элементом только одного списка и вообще структура быть именно линейным списком тоже не обязана. Ссылок из объекта может быть произвольно много на родительские, дочерние, соседние, связанные, и ещё какие угодно объекты. Двусвязность же даёт возможность мгновенно убирать объекты из структуры, имея только указатель на объект.
Можно наплодить сколько угодно «более лучших языков», но пока в них надо строить структуры данных из убогих «контейнеров», никуда сишечка гарантировано не денется.AnthonyMikh
30.11.2021 20:46+1Двусвязность же даёт возможность мгновенно убирать объекты из структуры, имея только указатель на объект.
При условии, что объект реально лежит в каком-то списке.
При условии, что вы это делаете из одного потока.
При условии, что указатели могут не указывать на сам этот объект.
При условии, что сам объект не меняет своего положения в памяти.
Ещё раз, для чего вам потребовались интрузивные списки?
PsyHaSTe
01.12.2021 04:06+3Двусвязный список в сишке сделать просто только потому, что на часть проблем структуры ответ дается в стиле "да мамай клянус такого не будет", а раст в маму не верит, ему нужно доказать. А вот написать всегад корректный список который с помощью произвольного количества вставок/удалений не выйдет развалить — уже дроугая задача.
Ну и да, в реальности я связных списокв не видел ни разу в итоге — везде их выпиливали в пользу какого-нибудь вектора или кольцевого буффера.
Kubera2017
03.12.2021 15:52-1Это коммент 100%)) я попытался граф на расте написать, вообще непонятно кто чем владеет. а уж если гиперграф то полный аут
warlock13
03.12.2021 18:15+1Если граф нужен именно domain-specific, то есть если нельзя его как-то факторизовать от domain-specific кода, то стоит рассмотреть вариации на тему ECS, generational arena. Например, см. мою библиотеку https://crates.io/crates/components-arena Понятно, что у такого подхода есть определённые минусы, но если они существенны, то без подсчёта ссылок скорее всего всё равно не обойтись - что раст у тебя, что не раст.
Kubera2017
07.12.2021 23:49Спасибо. Хочется прям adjacency list на поинтерах конечно, про контейнеры я уже думал. Меня, если честно, еще очень волнует вопрос можно ли как то обойтись без итераторов, neo4j тоже вся на итераторах, реализации графов на расте, которые я нашел поиском тоже
bm13kk
04.12.2021 01:28Есть вероятность что я сейчас начну холивар. Но мне кажется эта тема стоит рисков.
Возможно проблема не в расте\графе. А в коде, который пользуется графом. Попробуйте подход, который использует лисп к графам. И к сожалению, для этого надо выучить весь лисп.Kubera2017
07.12.2021 23:24А что имеется ввиду под этим кодом? Код, который пользуется графом максимально прост: 1) добавить вершин штук n со свойствами 2) добавить граней между ними со свойствами на гранях 3) найти вершину по значению свойства и от нее сделать 10 хопов с фильтром по значению свойства на грани
bm13kk
08.12.2021 10:47Ваш "интерфейс" управлением графа - загоняет меня в ступор. Отдельно добавлять грани от вершин??? В теории программирования (подчеркиваю - даже в теории) не способов писать оптимизацию которая такое поддержит. Интерфейс должен быть во много-много раз проще. Оптимизировать можно только то, что имеет ограничения.
Например. Чтобы SQL был быстрее обычного чтения с диска, на данные накладывают гигантские ограничения. Они хранятся в столбиках, типы данных вылизаны, есть ключи и индексы и тп и тд. Мы даже придумали дополнительные правила, чтобы держать в голове, а не коде - нормальные формы.Kubera2017
08.12.2021 13:08Ну я практик, графовые базы моя специализация. Примеров можно придумать много. Например, граф телефонных номеров и звонков между ними - абоненты вершины, на них свойство "номер", факт звонка грань, на ней counter сколько раз звонили. Каждый звонок может добавлять вершины (если абонента еще нет в графе), создавать грани (если до этого еще не звонили друг другу), и обновлять грани (инкрементить counter для уже существующих). И в реальном времени это все пишем.
bm13kk
08.12.2021 13:40Извините, я с графовыми БД плохо знаком. Что почитать о том как графовые БД хранят данные?
Потому что я тут вижу типичную SQL: таблица вершин и таблица граней.Kubera2017
08.12.2021 14:22-1ну это самый дубовый подход, скажем так, на каждом шаге обхода графа нужно дергать следующую грань по индексу, т.е. получится ничем не лучше табличных баз. Пока что два академических подхода существует - adjacency list и adjacency matrix. В первом объект вершины хранит на себе указатели на соседей (что я пытаюсь сделать, это подход neo4j https://youtu.be/LSKa3as_S7I?t=635), второй пока что только Редис прорабатывают, там можно почитать https://oss.redis.com/redisgraph/
Kubera2017
07.12.2021 23:28По Лиспу буду рад каким-то пояснениям или ссылочкам, я знаю подход TerminusDB, она на прологе, там скажем так "версионирование" (ничего никогда не удаляется), но это нереально сейчас с нормальным размером графа
bm13kk
08.12.2021 10:57Если я не ошибаюсь, я эту читал https://en.wikipedia.org/wiki/Structure_and_Interpretation_of_Computer_Programs
Подход у лиспа такой. Верхина владеет всем ниже. Если Вам надо читать - берете вершину и идете вниз. Если надо поддерево добавить в вершину - поддерево никак не меняется, меняется только вершина (в ней ссылка на поддерево). Если надо изменить лист - строите новое дерево.
disputant
30.11.2021 11:47+1С завидной регулярностью ходят слухи о скорой смерти С/С++...
Просто есть языки для начинающих, в которых компилятор + машина помощнее служат памперсом для программиста, а есть языки для... (ну, слово "профессионал" страшно не нравится — часто вот тот в памперсах, получая за код деньги, "профессионал", в отличие от знающего и умеющего "любителя") ну, для взрослых дядь...
Просто не давайте спички детям без присмотра :)
byko3y
30.11.2021 15:49Настоящая история и мотивация создания языка Си практически не отражена, приведена книжно-википедийная версия пересказа кума подруги тёлки брата. Даже языки B и BCPL не упомянуты.
Абстрактная машина должна была решить две проблемы одновременно
И создать новую проблему -- проблему "что такое абстрактная машина и как она работает?". Во всем стандарте нет ни малейшего намека, как работает абстрактная машина, но при этом весь стандарт построен вокруг неё, в частности, многие вещи подразумевают некий "доступ". Например, "a = &b->field" -- я делаю "доступ" к b? Отсутствие определения абстрактной машины -- это причина, по которой компиляторы ломают ранее работавшие программы. Отмаза одна и та же -- "стандарт не запрещает". Да, он и не разрешает. Абстрактная машина -- это одно сплошное undefined behaviour.
Эта свобода интерпретации стандарта привела к тому, что отдельные неадекватные вахтеры из команды разработки GCC забаррикадировались от здравого смысла и отстреливаются лишь дежурными "я так увидел стандарт". Что их фичи превращают написание и отладку программ в кошмар -- их не волнует. Слава богу, что у нас есть ключи "
-fwrapv -fno-strict-aliasing"
anonymous
00.00.0000 00:00cepera_ang
30.11.2021 16:08+5т.к. в коректной программе на Си никогда не может быть неопределённого поведения.
И поэтому не существует ни одной корректной программы на Си, верно?
ncr
30.11.2021 16:23+8Вижу статью с:
— Кликбейтным заголовком.
— Кучкой классических примеров UB.
— Ссылками на людей, не понимающих смысла слова «undefined».
Ожидаю увидеть в комментариях:
— Множество экспертов, точно знающих, как на самом деле надо правильно писать компиляторы.
— Комментаторов, в очередной раз тщетно пытающихся донести до экспертов смысл слова «undefined».
Так и случилось.
firehacker
30.11.2021 19:25Так как мы разыменовываем указатель до его проверки, то компилятор спокойно решает, что сам указатель никогда не будет нулевым.
Важно подчеркнуть, что обе этих оптимизации являются верными.А можно выдержку из стандарта, где зафиксирована легитимность такого вывода?
vkni
30.11.2021 19:44Выдержки из стандарта не будет, да она и не нужна, т.к. такое поведение компиляторов - это объективная реальность. Вы же встретившись с гопниками в подворотне не будете рассказывать им про УК?
wataru
30.11.2021 20:22+1В стандарте написано, что разыменовывание нулевого указателя — UB. Все. После этого компилятор может считать что разыменовываемый указатель никогда не будет нулевым. Если программист не допустил UB и компилятор угадал правильно — программа работает, никаких проблем нет. Если допустил — то компилятор прикрыт бумажкой "при UB я могу хоть диск форматировать".
4p4
30.11.2021 22:35Естественный цикл "крутости" и "правильности" языков так устроен, что любой язык крут и правилен когда на нём создают системы с нуля, а когда творцы уходят и новичкам из следущего поколения надо самостоятельно майнтайнить тысячи проектов по всему миру на этом языке, он становится скучным и неправильным.
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 ... являются локальными переменными в этой функции. Что вообще автор хотел этим сказать? Видимо он хотел избежать утечки каких-то данных через стек, и даже возможно через регистры. Компилятор это зануление локалок конечно имеет право выкинуть, но ДОПУСТИМ он не выкинет их, и действительно занулит соответствующие регистры и стек. Только в стеке и в регистрах могут быть записаны какие-нибудь промежуточные результаты вычислений, а их как вообще занулить? Писать специализированный компилятор, который все использованные функцией адреса в стеке и все использованные регистры (кроме возвращаемого значения) забивает нулями?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
Насчет хэшей - мы использовали пароль, чтобы не вдаваться лишний раз в подробности. Это простой пример, смысл которого в том, что порой бывает важно очищать все места хранения потенциально опасных данных, и что Си в этом вопросе никак помочь не может.
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" не упоминается - об очищении чего тогда вообще может идти речь?
А где она разрешима? Кроме ассемблера, я не слышал о других языках, которые бы давали такой уровень контроля. Как такой язык должен выглядеть, если это будет не ассемблер?
Но вообще можно придумать некую специальную прагму, которая заставит всё тот же компилятор Си генерировать код зануления стекфрейма и регистров по выходу из функции.
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
andmemmove
. 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 Си и вводить в заблуждение кучу людей?
А где она разрешима? Кроме ассемблера, я не слышал о других языках, которые бы давали такой уровень контроля. Как такой язык должен выглядеть, если это будет не ассемблер?
Любой, в документации/спецификации/стандарте которого написано, что он на такое способен. И да, скорее всего это будет только ассемблер, но это не такой уж и плохой вариант. Никто не мешает вам линковаться с объектными файлами, написанными на асме. Решение с флагами компилятора тоже неплохое, но только при условии, что вы действительно уверены, что оно сработает как нужно. Но в рамках самого языка Си решить эту проблему невозможно - в этом и был смысл примера, который мы дали в тексте.
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."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 предполагало понимание того, какой код генерирует компилятор под целевую архитектуру. Это было хоть как-то возможно потому, что сами компиляторы были гораздо проще, да и портировать нужно было намного меньше. Но бесконечно так продолжаться просто не могло. Для Си того времени просто немыслима возможность наличия двух указателей с одинаковым содержимым, проверка на равенство для которых возвращает ложь. Разумеется речь идет про плоскую память. А вот для стандартного Си - это вполне нормально, так как подобное сравнение - это неопределенное поведение. И на самом деле еще хорошо, что такое сравнение ложь возвращает, а ведь может еще, если верить стандарту, вам исходный код удалить или монитор сжечь.
ruomserg
02.12.2021 20:39+2+1 Мне не нравится определени UB в стандарте, и мне совсем не нравится текущая интерпретация UB современными компиляторостроителями. Понятно, что в стандарт вводили UB для того, чтобы дать возможность компилятору разрешить сложную ситуацию тем способом, который наиболее подходит для конкретной платформы. Почему нельзя было написать вместо UB — «поведение зависит от платформы», я не понимаю. Но они выбрали написать UB, и стало допустимым любое поведение. А потом случилась автоматическая генерация кода и dead code elimitation. И UB внезапно стали все понимать как "… и тогда код можно вообще не генерировать". Для C++ — это понятно, и он без этих оптимизаций не жилец. Но «C» еще можно спасти, чтобы UB перестали быть UB, а стали platform-specific задокументированным поведением.
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.
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Т.е., если компилятор Паскаля явно не указал в своей документации иное поведение, он обязан сообщить об ошибке либо во время исполнения программы, либо еще во время ее компиляции.
В любом случае Паскаль, в отличие от Си, никогда за свою историю не определялся как язык низкого уровня. Поэтому то же знаковое переполнение всегда было ошибкой, и никто из людей, кто знаком с документацией языка, не будет спорить об обратном. Попытки обойти это ограничение посредством инструментов компилятора являются хаками и к самому языку отношения не имеют.
Какие компилируемые в машинный код языки программирования, сопоставимые по уровню абстракции с тем же Си или паскалем
Где в стандартах Си или Паскаля указано, что они компилируется в машинные коды? Для языка высокого уровня безразлично, каким образом будут транслироваться написанные на нем программы. Тем более, что интерпретаторы Си тоже существуют.
Не получится одновременно писать и на низкоуровневом языке Си, и на высокоуровневом - это оксюморон. Печальные последствия такого программирования мы наблюдаем постоянно. Именно это, а не что-то другое, и явилось причиной, почему мы хотим, чтобы Си умер.
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. Я о том, что Си и Паскаль можно компилировать в достаточно эффективный машинный код для фон-неймановских архитектур.
Для языка высокого уровня безразлично, каким образом будут транслироваться написанные на нем программы.
А для языка низкого уровня не это безразлично? Писать интерпретаторы ассемблера незаконно? Что значит "для языка безразлично", у него какое-то свое мнение есть? Что вообще такое язык высокого и язык низкого уровня? Я язык Си не считаю языком низкого уровня, язык низкого уровня для меня это ассемблер, машинный код.
Cerberuser
04.12.2021 11:13программы на языке Паскаль могут перестать работать из-за того, что тип Integer вдруг стал другим, и не вмещает в себя нужный диапазон.
Но они это сделают поняным и задокументированным образом, а не "как оптимизатору в голову взбредёт", по идее, в этом разница?
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 лет жизни языка появилось огромное количество программ, которые в момент выхода стандарта оказались формально нерабочими. Причем из-за особенностей текста стандарта (в том числе благодаря постоянному использованию неопределенного поведения) это произошло практически незаметно. Разгребать последствия этих решений мы будем еще очень долго.
j123123
06.12.2021 16:19Хорошо, раз уж вы обратились к истокам, то вот вам, к примеру, мануал по языку Си, написанный Ритчи до того, как был принят стандарт:
Там, к примеру, рукой создателя языка написано следующее: ...
Это очень похоже на текст стандарта?
Может и похоже чем-то, но нет, текстом стандарта это не является. Например вот фрагмент
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. Уже тогда Си должен был умереть?
Amomum
01.12.2021 01:37+3И кстати пароли в открыдом виде не хранят, а хранят их хэши.
Да, но чтобы посчитать хэш - пароль сперва таки нужно получить в открытом виде.
Pasha4ur
01.12.2021 15:38Здравствуйте
Не вдохновляющая статья для новичков. Хоть и полезная.
Неужели, если делать все по новым стандартам, то будут такие же косяки? Что делать новичкам?
Смотрю курсы для новичков по С для STM32 на Udemy, потому что Ардуино без отладки очень неудобный, а тут такое ) Хочу потом еще попробовать Unreal Engine c С++ для визуализаций.
Очень давно когда-то начал читать книги по программированию, дочитывал и досматривал большинство курсов, а потом понимал, что в языке творится какой-то ад и содомия, в которых я не хочу участвовать, поэтому шел обратно в фотошоп, где все понятно и работает адекватно.
Знакомый сказал, что со всеми языками какие-то адские проблемы, с которыми программистам приходится жить.
П.С. Кстати, в курсах для новичков и даже в начальных 2 томах книг Столярова о входе в программирование (на 3-ем томе с сокетами ос и сети я уже отложил это чтиво на потом, потому что куча теории без практики не интересна) про такие проблемы не рассказывают.
Pasha4ur
01.12.2021 15:46Так уже хочется, чтобы создали новые адекватные современные языки без долбанных легаси-проблем каменного века программирования.
cepera_ang
01.12.2021 16:54Модные стильные молодежные TypeScript, Python, Go, Swift, Kotlin, Rust — не подходят?
Pasha4ur
01.12.2021 17:06Смотрел интервью по питон у айтибороды. Говорили, что медленный и с многопотоком беда.
Я для распберри пи немного пробовал его. Не очень зашел синтаксис. Очень не хватает классического for(;;), i++ и т п.
Ну, и вряд ли вы от же распространный микроконтроллер STM32 (интересно сделать дома автоматизацию) будете программировать на питоне с возможностью отладки.
Игровые движки что-то тоже пишут на С++ и C#.
Хотя в планах питон есть. В тех же 3д редакторах много чего пишется именно на нем.
Про тайпскрипт, гоу и раст ничего не знаю. Почитаю. Спасибо за совет.
cepera_ang
01.12.2021 17:33+2У каждого языка есть ниша. Питон медленный и с многопотоком беда, но компьютеры быстрые и весь код, которому нужна скорость и параллельность утрамбован в библиотеки, которыми замечательно и легко пользоваться. Зато он простой для старта и с него могут начать даже те, кто от тонкостей Си сразу скажет "до свидания, компьютеры это не моё". Поэтому он и занял сначала такую нишу "клея", а потом компьютеры ещё ускорились и во все области пролез и в машинном обучении взлетел, где программирование не самоцель, а лишь инструмент для проверки своих идей.
Тайпскрипт — это для веба, JS как его изначально стоило сделать. Гоу — для сетевых сервисов хорош. Раст — для "системного" программирования, но на самом деле вполне взлетает во многих областях потихоньку.
Ну и у вас немного каша в голове от всех этих "мнений экспертов", потому что тут и программирование STM32 и движки игровые и распбери — но это всё прикладные вещи, где с одной стороны домен самой проблемы нужно знать, а с другое — универсальные практики разработки (контроль версий, тесты, и т.д.), а сами языки программирования имеют значение только в последнюю очередь. Хотя конечно у каждой области и каждого языка есть своя "культура разработки" и иногда они друг-другу хорошо соответствуют, а иногда не очень.
Вообще, если нет проблем с английским, то я бы с какого-нибудь https://teachyourselfcs.com/ (перевод самого гайда) начал разбираться, а не с того, что на ютубе рассказывает какая-то борода.
Pasha4ur
02.12.2021 02:55Спасибо.
"Ну и у вас немного каша в голове от всех этих "мнений экспертов"
Есть такое. Часто общался с программистами, которые хвалят используемые ними языки, а другие ругают. Хотя есть единичные знакомые, которые говорят, что все текущие реализации всех языков программирования не очень.
"Вообще, если нет проблем с английским, то я бы с какого-нибудь https://teachyourselfcs.com/ (перевод самого гайда) начал разбираться, а не с того, что на ютубе рассказывает какая-то борода."
Я на Udemy смотрю курсы по STM32 на англ от индуса. Все понятно.
Также читал Столярова (2 тома из первого издания прочитал полностью). Тоже все было понятно. Только раздел с кодом по Ассемблеру я пролистал, потому что это было слишком жестко.
samoreklam
05.12.2021 08:52int *p = malloc(64 * sizeof(int));
int *q = malloc(64 * sizeof(int));
if(p < q) /* Undefined behaviour! */ do_something();
Меня интересует этот случай.
Если p и q преобразовать в size_t после получения памяти, и делать сравнение. То оно корректно будет? (у меня в коде, есть такая же конструкция, где я определяю смещение данных. И по всем тестам, какие делал, всё работает правильно.) Может ли случиться "аномалия" при таком определении смещения данных?mayorovp
05.12.2021 10:46+1Да, может, если модель памяти отличается от плоской (в таком случае ваш указатель внутрь size_t просто не влезет). Если уж приводить к числу — надо приводить к uintptr_t (но его на платформе может и не быть).
ciubotaru
Правильно ли я понимаю, что:
разработчики научили компилятор предотвращать выстрелы в ногу и убрали из него саму возможность выстрелить себе в ногу
пользователи требуют вернуть возможность стрелять в ногу, на том основании, что (1) раньше можно было, (2) из-за предыдущего пункта многие программы по-прежнему стреляют в ногу, (3) из-за предыдущего пункта есть необходимость тестировать такие выстрелы.
?
amarao
Переполнение знакового - это такая бездна, в которую даже приближаться не хочется.
aversey
Если бы проблема была только в переполнении знакового... Но да, уже это весьма неприятно. =)
amarao
Си должен умереть, да, и пока что Rust - самое близкое из того, что у нас есть для замены Си. Однако, я с большим интересом смотрю на срачики вокруг Rust-драйверов в ядре. Претензии, которые предъявляют к Rust'у весьма любопытны. Ядро не должно падать по "одной ошибке", даже если это логическая ошибка, а в Rust весьма трудно избежать паник при нарушении инвариантов (а драконы из земли uninitialized memory весьма хотят их нарушить...)
apro
Это на мой взгляд самый слабый аргумент против. В изначальном ядре, без патчей для добавления поддержки Rust, есть функция "panic", и куча ее вызовов с помощью макросов BUG/BUG_ON и так далее. Поэтому почему Rust в отличие от С должен быть особенным и не содержать неявных вызовов
panic!
совершенно непонятно.aversey
В Rust много здравых идей, но мне он кажется избыточно сложным. А сложность языка имеет свойство перекладываться на сложность программ, что в свою очередь провоцирует ошибки. Кроме того, про Rust важно понимать и помнить, что он не избавляет от всех ошибок, а только от некоторых (прошу прощения если оскорбил -- есть знакомые растовцы, которые считают что их код абсолютно безопасен и корректен ведь написан на Rust).
Что касается замены Си, Rust мне кажется больше заменой плюсам, но если потянет -- интересно будет посмотреть на мир, где железо заржавело. =)
amarao
Не совсем так. Насколько я понимаю, речь идёт о том, что в rust некоторые виды действий не имеют Option, и если условие не выполняется, то BUG_ON/panic! единственная опция. А в Си с этим умудряются жить.
Если ядро обнаруживает, что pagetable коррапченная, то это BUG_ON. Но если драйвер GP устройства обнаруживает, что ему не дали 4кб памяти, то это не BUG_ON никаким образом (на что именно ругались я не смотрел, я читал сам текст ругани).
В принципе, на Rust'е можно писать полностью низкоуровневый код (включая naked functions для обработки прерываний). Хотя я погуглил, плюсы тоже умеют naked. Так что разница только в здравом смысле Rust'а (наличии safe-подмножества).
... В то же самое время, как в условиях ядра справятся с ownership - это интересно.
apro
А какие именно "действий"? В голову приходит только "fallible allocation", но обертку над менеджером памяти ядра все равно нужно будет делать отдельно, стандартные функции выделения памяти использовать вряд ли удастся, например из-за наличия kmalloc и vmalloc. А раз придется делать заново, то в чем сложность добавить Result/Option в новые функции выделения памяти не очень понятно.
domix32
Собственно первый ржавый патч c драйвером как-то так и делает. Там макрос типа try_allocate! который вернет либо результат аллокации либо ошибку.
vkni
Именно так.
Я сильно сомневаюсь, что простому системному язычку нужно 5 (пять) видов макросов. И никакой язык не может избавить от всех ошибок. Это просто невозможно.
Учтите, что для языка - замены С нужна возможность писать компиляторы по любому чиху для чего угодно. Это, в сущности, должен быть уровень магистрской.
А если язык сложен как Rust, написать второй компилятор очень тяжело.
amarao
В целом, требование второго компилятора вполне обосновано, и я его много раз слышал в контексте "коммититься в язык".
В целом, вот, люди пытаются. https://github.com/thepowersgang/mrustc
vkni
Нет, "я требую" не второго компилятора, а двадцать второго.
То есть, для замены текущего С, как lingua franca современных ЯВУ, должна быть спецификация, компилятор по которой усердный студент 6 курса реализует ну за пол года. Без оптимизаций, разумеется, без хитрых проверок, но вполне рабочую. Стандартной библиотеки тоже несколько реализаций.
Ну и, раз можно помечтать, набор стандартных тестов к компилятору.
cepera_ang
А си-то сможет усердный студент реализовать за полгода? Прямо по стандарту или просто "какое-нибудь минимальное" подмножество языка?
vkni
89 сможет, кмк.
cepera_ang
И как много кода сейчас пишется или, прости господи, компилируется в режиме С89?
BioHazzardt
я под тайзен в апреле на C89 писал
vkni
Пишется, кмк, достаточно много того, что легко отпортируется на С89. Тот же ocamlrun - интерпретатор Ocaml до недавнего времени был на C89. И, не сказать, что это достоинство новых ревизий С - относительная сложность спецификации.
Кроме того, C вроде бы множество компиляторов уже есть. А для предполагаемой перспективной замены С как системного языка - нет.
Вот простейшая штука - SPARC 64. Где для него компилятор Rust'а? Хоть какой-нибудь, пусть без borrow-checker'а, без оптимизаций, но способный запустить экосистему? А как там у Эльбрусовцев - оно работает или нет?
amarao
sparc64-unknown-linux-gnu
входит в список плохо поддерживаемых платформ. Поддержка Эльбруса растом - это вопрос к эльбрусовцам, потому что продукт маргинальный и за них поддержку llvm (а это основное требование) никто не будет писать для всякой экзотики.
vkni
Ну вот плохо - это как?
Вопрос к Эльбрусовцам в то и упирается, что коллектив разработчиков нового ЦП должен малыми силами суметь разработать компилятор системного языка. Этот самый lingua franca.
А ведь Эльбрус - это мощнейшая контора с гос. финансированием, долгими традициями и т.д. А что могут авторы какого-то исследовательского процессора?
Это, собственно, и ответ на вопрос - почему мне нужна в идеале возможность студенту написать хоть какой-то, но компилятор.
amarao
Портировал gcc и llvm, дальше оно само подтянется.
mrust как раз хорошая цель для начала портирования. Хотя без llvm ничего не выйдет, извините.
Вообще, задача неоптимизирующего минимального компилятора Rust с нативной кодогенерацией (без LLVM) - это отличная интересная область. Например, все generic'и (в вопросах кодогенерации) превращаются в динамическую диспетчеризацию... Хотя, может, лучше llvm спортировать?
vkni
llvm и gcc в отличие от спецификации c89 эволюционируют. И у маленького коллектива может просто не хватить ресурсов на поддержку.
cepera_ang
Не хотелось бы расстраивать стройную картину мира, но Эльбрус это никому неизвестный мелкосерийный проект из страны третьего мира с тиражом в десяток тысяч экземпляров за всю продолжительную историю, а не "мощнейшая контора".
И если кто-то не может позволить себе портировать даже самые популярные тулчейны, а могут осилить только наколеночный Си, то ждёт такой проект быстрая и мучительная смерть.
vkni
Ну, то есть, все группы с исследовательскими ЦПУ идут в известное место?
cepera_ang
Что за группы такие? Давайте предметно разговаривать, пока получается гипотетический разговор в пользу бедных — какие-то мифические группы, которые не могут осилить порт gcc/llvm, но могут осилить свой процессор и компилятор. И чего дальше они с этим компилятором делать будут? Любоваться на свой процессор? Давайте ссылку на пример таких исследователей, о которых вы говорите.
gecube
че-то ору....
cepera_ang
Так у С каждый отдельный компилятор — это считай отдельный язык с поверхностным сходством и собственными граблями. И вы это видите как достоинство, что любой студент может накидать свою версию и получить стопятьсотый глючный диалект? А чем вообще ценность такого упражнения, кроме занятости студента?
Я может не туда смотрю, но rustc для SPARC 64 и есть компилятор раста для SPARC 64. А насчёт эльбрусов — они что-то там хвалились насчёт поддержки ещё в начале года, но это ж чисто гипотетическая платформа, зачем ей какая-то поддержка Раста :)
vkni
Возможность разработки какой-то новой экосистемы, с новыми процессорами, новыми шинами и т.д.
Иначе мы завязнем в болоте x86. О, блин.
cepera_ang
Вот что говорит о поддерживаемых платформах тулчейн LLVM у меня на компе, на который опирается компилятор раста:
Мало? А теперь давайте компилятор С, кроме gcc и clang'a, который может без переписывания половины кода такой же список показать.
inferrna
Справедливости ради, переписывание кода на расте, всё-же, может потребоваться.
cepera_ang
Конечно, запустить все что угодно на любом железе — это фантазии, не более. Программа размером сто мегабайт на восьмибитном контроллере не особо запустится, даже если её и возможно было бы каким-то чудом под него скомпилировать :)
netch80
> Программа размером сто мегабайт на восьмибитном контроллере не особо запустится
Вообще-то банально — для этого нужно банкирование памяти :) (которое и так очень часто есть на таких контроллерах, но не в таких масштабах).
Но в здравом уме и твёрдой памяти, конечно, никто так делать не будет (превышение в 64 раза, как на старших PDP-11, вроде был максимум достигнутого), поэтому это замечание чисто вскользь.
cepera_ang
В теории есть, а на практике такие компьютеры существуют? А что на них будет делать эта гипотетическая гигантская программа — запускаться первые N лет после включения устройства? :)
BigBeaver
Емнип, была где-то статья про линукс на ардуино.
inferrna
Не, я про использование, к примеру, 64-битных примитивов на 32-битных платформах и тому подобное.
GarryC
А почему, собственно, без gcc ? А если я потребую у Вас Rust без llvm ?
cepera_ang
Мой комментарий был исключительно в контексте предыдущего обсуждения о том, какой Си замечательный, что его может любой студент накостылить, отчего предположительно возникает замечательная поддержка различных платформ.
А если окажется, что замечательная поддержка различных платформ у Си благодаря gcc и llvm, то это уже не так уж и сильно отличается от раста :)
ApeCoder
А почему надо писать для новой экосистемы весь компилятор а не только бекэнд?
domix32
Весь раст прячет за абстракциями llvm, так что порт кодогенерации из llvm IR в спарковский байткод вполне решит проблему. Расту останется только указать необходимый бэкэнд. Есть неофициальная поддержка и 16-битных досов и под амигу помнится что-то делали. Это уже не говоря про зоопарк embed микроконтроллеров, часть из которых потом в спутниках вокруг земли вертится.
vkni
И вы начинаете зависеть от llvm. Нет, это путь в никуда для небольшой группы.
Кроме того, системный язык на то и системный, чтобы быть близок к текущему железу => у него должен быть достаточно простой компилятор.
cepera_ang
Выбор небольшой — или стоять на плечах гигантов, или валяться на кладбище истории. Можно называть это "зависеть", а можно называть это "пользоваться плодами". С тем же успехом ваша небольшая группа будет "зависеть" от фаба, производящего железку физически, но уверен, что вы не будете говорить, что единственно верный путь — это использовать техпроцессы, которые может повторить студент за полгода.
Непонятно как одно следует из другого.
domix32
Насколько должна быть небольшой группа, чтобы ей понадобился отдельный компилятор под собственное не мейнстримное железо и при этом страдало бы от зависимости от llvm? Звучит как мифический персонаж из африки у которого из девайсов только телефон с парой мегабайт памяти и ллвм туда просто не влезет. Опять же встаёт вопрос каков шанс, что такая мифическая железка нужна в массовых количествах и будет иметь хоть какие-то требования к безопасности и отказоустойчивости? Мне кажется более вероятным что кто-то будет писать сразу на ассемблере для такого, чем пытаться изобразить ещё один компилятор Си.
BigBeaver
Емнип, я перешел на С99 только из-за каких-то нюансов с хвостовой рекурсией (надо было сэкономить десяток то ли байт, то ли тактов), а так в embedded C89 вполне норм себя чувствует.
RomanArzumanyan
Много чего для видео. FFMpeg, например. До сих пор ругается на смешанные объявления переменных и код.
amarao
Вопрос интересный. Я, с одной стороны, понимаю мотивацию требовать простой для реализации язык. Но, с другой стороны, вы же этого не требуете от ОС? Времена, когда вы могли нашкрябать аналог MS-DOS за пол-года 6 курса давно прошли, и чем дальше, тем сложнее, однако, никто не выкидывает новые компьютеры на том основании, что студент под новую умную сетевуху драйвера уже не осилит написать. Аналогично с компиляторами. Альтернативные (условно простые) реализации компилятора - да, требование охватности компилятора неподготовленным мозгом - нет, это произвольное требование.
Альтернативную библиотеку к расту уже написали/пишут (насколько оно применимо не смотрел): https://github.com/eloraiby/alt-std
BigBeaver
Потомучто далеко не все процессоры предназначены для запуска ОС.
amarao
Разумеется. Хотя нижняя планка "можно запустить ОС" (в центах на штуку) с каждым годом всё ниже и ниже.
BigBeaver
Можно не означает нужно. От применения зависит.
cepera_ang
Amomum
Простите, но зачем?..
Миллионы несовместимых компиляторов С (и стандартных библиотек, не забывайте еще и про них) - это огромная проблема, причем это проблемы программистов, вы вспомните, сколько ифдефов нужно написать, просто чтобы обеспечить совместимость с 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 remove
s that end up becoming permanent fixtures for 17 years.Насчет vendor-specific компиляторов - это ведь реально так, даже ARM сливает свой armcc и вместо него переходит на форк clang'a, потому что сделать свой нормальный компилятор для своей же архитектуры оказалось слишком сложно!
Простите, что-то у меня пригорело слишком сильно..
Да, так вот. Я реально не вижу никаких причин активно поддерживать множество компиляторов для одного языка, это только распыляет усилия людей и создает проблемы переносимости на ровном месте, поэтому мне очень интересно, почему вы считаете, что это хорошо :)
DustCn
Ну сложно не сложно, Интел бросил свой ICC в пользу Clang по одной простой причине - Clang ковыряют тысячи бесплатных разработчиков и миллионы тестировщиков. И если стандарты на С++ будут продолжать печь с той же скоростью, то чтобы обеспечить полный охват Интелу надо будет продавать свое процессорное подразделение и нанимать больше программистов компилятора. А не потому что запил архитектуры сложен...
Утрированно конечно, но в целом описывает проблему новых стандартов в целом в отрасли.
Amomum
По-моему это тоже самое другими словами :) Сложно => дорого и/или долго => экономически нецелесообразно.
Alex_ME
А зачем писать целиком компилятор, включая парсер, все эти IR, MIR, borrow checker и даже пролого-подобный солвер (https://github.com/rust-lang/chalk), если можно портировать LLVM?
domix32
А почему именно это свойство профитное? Я бы предпочел чтобы студент 6 курса смог за то же время написать формализатор на каком-нибудь Coq и мог гарантировать, что конкретная имплементация компилятора гарантирует такой-то набор гарантий пускай без все тех же оптимизаций. Такое по крайней мере будет относительно безопасно использовать.
vkni
Приоритетное свойство - простота системного языка. Из неё следует очень много, в том числе и возможность написать 22 компилятора.
cepera_ang
Скажите, а вы железо производите или гадаете за других? Я там кидал ссылку на презентацию от реального разработчика железа, так он почему-то едва ли не молится на существующую открытую инфраструктуру и говорит, что начинать новые низкоуровневые проекты на Си — самоубийство. И это чувак, который уже лет двадцать пять программирует на этом самом Си.
domix32
Писать компиляторы ради того чтобы писать компиляторы не звучит как здравая цель. В прошлом веке конечно был бум на такое, потому что архитектуры тогдашних устройств отличались, но каждый компилятор имел кучу своих проблем, как с UB так и с производительностью, не говоря уже о специфичных багах, которые могли окуклить устройства из-за нюансов компилятора. Да и за безопасность в то время особо не думали. Ну подумаешь можно всю память компьютера прочитать вместе с паролями. Хакеров еще толком нет, худшее что могло быть — залетный фрик (phreak) который взял себе бесплатный доступ к телефонной линии. Сейчас же мир страдает от того что половина устройств в сети торчит со вшитыми дефолтными паролями, получить доступ к которому плёвое дело. Есть огромный спрос на секурность и надёжность и самописный компилятор едва ли поможет с решением этих проблем в 99.99 процентах случаев.
Вторая причина почему простота системного языка имела значение в прошлые года — нишевость программистов. Проще научить чему-нибудь простому и понятному, чтобы бизнес мог делать ХХП и грести баблишко. Риски от ошибок были относительно маленькими, да и вариантов получше не существовало в принципе. Так что простота софта и разработки это скорее исторический этап, нежели какое-то неотъемлемое свойство. Простота написания Hello world не гарантирует примерно ничего, кроме собственно вывода Hello world. Так что велик шанс что стоит пересмотреть приоритеты.
Mingun
А зачем?
vkni
Ну это же системный язык, на котором можно писать OS для любой экзотической архитектуры, для которой нет хорошо отлаженной системы llvm. Например, для Эльбруса. ;-)
amarao
Потому что люди положили все яйца в корзину Watcom C, и где он теперь? А если бы он был единственным компилятором, то и вся кодовая база превратилась бы в стремительно устаревающую тыкву.
Требование более чем разумное.
cepera_ang
Но в итоге сейчас всё скатывается в лучшем случае в дуополию gcc/llvm, а все остальное звучит как заявка на выстрел в ногу уже через пять-десять лет.
amarao
Ну, как минимум, есть MS и LCC, из того, что я могу назвать.
0xd34df00d
Если бы он был единственным компилятором, то, вероятно, не умер бы.
В мире линукса gcc долгое время был де-факто единственным компилятором сей, и нормально все. В том же хаскеле ghc тоже по факту единственный компилятор, и реализовать друой с поддержкой всех расширений практически невозможно, и это тоже далеко не самая болезненная точка хаскеля.
amarao
Компании, которые выдвигают требованием наличие хотя бы двух независимых реализаций перед адоптацией языка, на Хаскель даже не смотрят.
0xd34df00d
А на плюсы 5-10 лет назад смотрели?
amarao
Смотрели и даже используют. Компиляторов С++ достаточно много.
0xd34df00d
Можно примеры двух альтернативных ненаколенных компиляторов C++, скажем, в 2010-м под линукс?
vkni
Я не уверен насчёт OpenWatcom, но ICC точно был.
0xd34df00d
ICC был, но я его вообще ни разу нигде не видел в бою.
Де-факто gcc альтернатив не было.
Sdima1357
Я видел. В 2000 он крешил ядро операционки на темплейтах - сам компилятор,kernel panic при запуске компиляции от юзера 8). Где то в 2004-2007 давал очень неплохую SIMD оптимизацию, раскладывал циклы в 4xfloat32, выигрывая у gcc до 40 процентов. На немного более нетривиальном коде с ветвлениями слегка медленнее чем gcc. Потом AMD немного поучаствовала в GCC и он стал побыстрее. А так было время что часть кода у нас собиралась под icc , а часть под gcc.
vkni
В известной ветке на IXBT про системы команд процессоров народ наоборот, ничего никогда с помощью gcc в PROD не компилирует. :-) Мир большой.
Sdima1357
1 . g++ -GNU
2 . icc -Intel
cepera_ang
А за пределами x86 жизни нет?
Sdima1357
??? 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
amarao
Не альтернативы:
gcc
clang
Альтернативы:
intel C++ compiler
AOCC
arm compiler
cl (MS)
Gordon01
А помните был такой IAR embedded compiler?)
Не под линукс и только под arm, но все же)))
0xd34df00d
Да, и на совместимость с новыми фичами сишки или плюсов я плевки от народа слышал постоянно.
Amomum
А что, IAR тоже свой компилятор дропнули?
netch80
Intel C++ (ICC) уже упомянули. Ещё был open64, причём в двух инкарнациях — одна сильно подточенная AMD и одна полунезависимая.
Я тогда работал на HPC тематику (хоть и вскользь) и мы его там пробовали. На каких-то математических пакетах он давал код быстрее и GCC, и (тогда ещё только начинавшегося) Clang, и ICC.
cepera_ang
Можно поспорить, что Rust переживёт некоторые из таких компаний :)
amarao
Некоторые - конечно. Но кто-то будет на нём (или другом языке) писать код с сроком сопровождения 50+ лет (условная электростанция или метро). И их соображения вполне разумны.
cepera_ang
И поэтому код условной электростанции будет написан один раз на %vendorname% версии С, который собирается единственным компилятором от производителя использованных микроконтроллеров, который однажды засертифицировал конкретную версию, а потом скончался через 25 лет.
:)
Но требование разумное, я его вполне понимаю.
romanetz_omsk
А вы точно уверены, что некая АСУТП система сопровождается 50 лет? Насколько я знаю, к тому же самому технологическому оборудованию подключают новые шкафы, написанные на другом языке, стандартном на момент внедрения. Срок службы автоматики - 20 лет.
vkni
Это просто часть кластера болезненных точек, не причина, разумеется, но единственность ghc сильно связана с:
Производительность ghc - это единственный компилятор, поэтому невозможно сделать быстрый компилятор Хаскеля. (быстрый компилятор почти Хаскеля возможен - это cocl, компилятор Клина - 0.2 сек полная сборка страниц 10 текста, использующих стандартные библиотеки на Core 2 Duo T9500).
Портируемость на всякое экзотическое - это единственный компилятор, поэтому увы и ах.
Простота разворачивания - это единственный компилятор, поэтому об "./configure; make world opt -j 40" забудьте.
Удаление невзлетевших фич, вроде backpack - это единственный компилятор.
Эксперименты с разными подходами, например в духе MLton/Stalin - забудьте. Хотя в случае ленивых языков это могло бы быть крайне интересно.
Где встраиваемый Хаскель, который может занять место Lua? GHC - единственный компилятор.
-----------------------
Разумеется, часть вещей реализована в том же Клине или Ocaml, которые тоже единственные компиляторы. Но к ним я могу тоже набросать массу претензий.
0xd34df00d
Тормозят всякие продвинутые фичи в системе типов. Что-нибудь тупое вроде haskell98 компилируется весьма быстро — модуль с 250 функциями вида
foo_k n = n + k
у меня собрался за 0.8 секунд, пустой модуль — за 0.6 секунд (там линковка отчего-то долго занимает).А, по-вашему, легче портировать имеющийся компилятор, для этого предназначенный, или написать с нуля под новую платформу?
Ну это увы, да, согласен.
Почему? Фичи вполне выпиливаются и меняются, иногда со сломом обратной совместимости. Просто оказывается, что выпиливать надо не так много фич (а то, что надо — оказывается, что проще сделать новый язык с нуля и назвать его, например, идрисом).
Есть огромная куча экспериментальных форков, а лёгкость написания плагинов означает, например, что вы можете под тот же блокчейн (cardano) писать код на более-менее обычном хаскеле, а потом отдельный плагин сгенерирует код для блокчейна. Отдельный компилятор не нужен.
vkni
Я же говорю, что это не причина, а просто часть узла проблем. С моей точки зрения, для промышленного (не экспериментального) компилятора спецификация лучше единственной реализации.
В частности потому, что изменения промышленного языка очень часто болезненны.
Кмк, Хаскель давно надо было "заморозить". И работать с новыми языками.
Это означает, кстати, болезненную зависимость от upstream'а.
0xd34df00d
Смотря для чего. Для ресёрча — хз. Для промышленного применения — очевидно, нет, там, вопреки стереотипам, уже достаточно кода написано.
Так в чём болезненность?
vkni
И это вопрос сейчас, после того, как SPJ ушёл? :-)
0xd34df00d
Я за политикой слежу мало, а в чём вопрос?
vkni
Ну нет. Компиляция сгенерированного кода для одной и той же xsd схемы на Ocaml PPX и на Template Haskell отрабатывают за секунду и 30 минут соответственно. Причём что там, что там отображения с одних и тех же алгебраических типов. Просто огромного кол-ва.
Ну и компиляция строк 1000 за 0.6 секунд - это тоже перебор. Я прекрасно знаю, что это именно проблема культуры группы вокруг Ghc - те же Clean/Ocaml собирают такое мгновенно.
Просто Ocaml'щики маниакально оптимизируют компилятор и экосистему для скорости (к той же dune масса претензий, но не скорость). А люди вокруг Ghc забивают - stack на собранном проекте секунду что-то внутри себя думает. Хотя make и dune в той же ситуации отрабатывают мгновенно.
0xd34df00d
Так в TH небось обмазывание всякими дженериками потом и прочим в
deriving
, в отличие от окамля. Вот это уже будет тормозить, да, но дженерики — это уже продвинутая фича.Это проблема приоритетов. Запилить новые фишки в систему типов интереснее и, я бы сказал, важнее, чем улучшить скорость компиляции. Но и последнее не забывают и над этим работают, и скорость компиляции в TODO-списке всяких важных людей и организаций.
И я бы сказал, что фишки в типах действительно важнее. В конце концов, есть репл, есть LSP, скорость компиляции в процессе разработки не особо важна, на самом деле.
stack делает далеко не то же, что make.
vkni
Клин с дженериками не тормозит.
До REPL тоже надо добраться. В вышеупомянутом случае я ждал эти 10 минут/пол часа.
Вот к этому и претензии. Конкретно в случае stack build, когда всё собрано, он должен проверить даты и выйти. А он занимается ещё какой-то фигнёй.
Это в первую очередь проблема единого компилятора. Было бы их два, второй мог быть промышленного качества и не тормозить.
AnthonyMikh
Я что-то сомневаюсь, что вы под дженериками понимаете одно и то же. Дэдфуд под ними понимает вот это.
vkni
Зря. Теперь сравните вот с этим - https://clean.cs.ru.nl/download/html_report/CleanRep.2.2_9.htm#_Toc311798067
AnthonyMikh
Понял, был неправ, беру свои слова обратно.
0xd34df00d
такое
vkni
Очевидно, это можно было как-то оптимизировать по скорости. Было бы желание. Но у сообщества вокруг Ghc этого желания нет.
Без nfs сейчас dune отрабатывает за
Это шутка?
Фичи, разумеется, не аналогичные, далеко не аналогичные. Но конкретно на сборках быстрее.
0xd34df00d
Вообще-то есть, и stack несколько лет назад работал сильно медленнее, чем сейчас. А по сравнению с олдовым cabal, который на любой чих задумывается на пару минут со словами «Resolving dependencies…», он вообще молниеносный.
Так и не понял, за сколько :]
Нет. Что проще — запилить компилятор, включая тайпчекер, или запилить только конкретные улучшения по производительности в уже готовый и оттестированный компилятор?
Кстати, сколько компиляторов у clean?
vkni
Core2 Duo T9500, Linux 64-bit; 0.14 сек первый запуск на ppx (3 файла), 0.04 сек - последующие. Проект собран. А я - лох, конечно.
Я же уже писал, что один. Есть ещё где-то компилятор eClean,но он закрыт и точно без генериков и динамики. Это один из недостатков Клина, а их вообще немало.
Но Клин показывает, что можно сделать быстрый компилятор Хаскеле-подобного языка, выдающий более-менее сносный код. Точно также, как Caml Light показывал, что можно сделать хороший компилятор для языка CAML.
Хотелось бы ещё иметь аналог MLton для Клина или Хаскеля. Но очевидно, что такой компилятор не должен быть единственным.
0xd34df00d
Ну вот видите, наличие одного компилятора не помешало клину иметь быструю скорость сборки.
Олсо, компиляторов хаскеля когда-то было дофига (и сейчас они всё ещё технически есть, но именно что технически). Просто оказалось, что одного ghc достаточно.
vkni
Зато помешало иметь много другого. Нельзя одним компилятором охватить всё. Зачастую к ним предъявляются совершенно противоположные требования. В частности скорость и оптимизация на уровне всей программы.
Я уверен, что какой-нибудь аналог Stalin'а смог бы выжать из Хаскеля больше за счёт хорошего понимания того, где там ленивость нужна, а где - нет.
0xd34df00d
К слову об отсутствующем желании, ковырял тут поддержку ghc 9.2 для hls — наткнулся на такое, тут чувак очень сильно ускорил скорость HLS. Что, в принципе, разумно, фокусироваться на language server'е куда разумнее, чем на самой сборке, на удобство и удовольствие от разработки оно влияет куда больше.
Кстати, чувак на зарплате у фейсбука. Походу они там не только антиспам в команде Марлоу пилят.
vkni
Так Linux был далеко не монопольной OS.
Mingun
A Watcom C был open-source? Если вдруг (хотя это очень-очень-очень маловероятно) развитие раста повернет куда-то не туда, не проще ли форкнуть уже существующий отлаженный компилятор, чем писать собственный? Хотя я все равно не понимаю, зачем вам иметь зоопарк компиляторов, чтобы потом мучится и писать проекты, которые должны будут компилироваться под всеми из них и в итоге не использовать ни один из них на полную катушку, зато иметь лес затычек багов то одного, то другого.
nick758
Watcom C не был opensource, но в 2003 году он стал OpenWatcom, некоторое время даже развивался. Последний стабильный релиз от 2010 года.
0xd34df00d
Зависит от языка, конечно, но в среднем не устал. Напротив, нередко с нетерпением жду релиза новых версий компиляторов с новыми фичами.
Производительность тоже растёт. Я как-то просто пересобрал код компилятором после примерно двух лет его развития, и получил прибавку процентов в 20-30 в скорости выполнения.
Новые оптимизации компилятора завозят, оптимизации рантайма завозят, и так далее.
PsyHaSTe
Вас под дулом пистолета заставляют обновляться? Если не видите необходимости не пользуйте новые плюшки, видите — используйте. Все просто же
PsyHaSTe
Не превращается, кроме того, что если трудно обновить версию компилятора это может намекать на кодсмелл и то что используются какие-то недокументированные хаки.
netch80
> Если не видите необходимости не пользуйте новые плюшки
Это некорректный подход. Обновление может быть вызвано совершенно посторонним фактором: например, для нового целевого устройства нужно новое ядро, дистрибутивы с ним содержат новые версии binutils и прочего обрамления, а старый компилятор с ними уже несовместим. Так бы компилятор никто не трогал, он устраивает, но компилятор сейчас не живёт один на голом железе.
> Все просто же
Нет.
PsyHaSTe
Желание собирать под новое целевое устройство и есть "видеть необходимость в обновлении"
netch80
> Желание собирать под новое целевое устройство и есть «видеть необходимость в обновлении»
Когда кажется, что обновление незначительно, а на самом деле за собой потянуло апгрейд половины галактики, это всегда неожиданно и больно.
Сочувствую промышленникам, их любой мельчайший вопрос разрывает тут пополам.
amarao
Нет. PL/I с нами на веки.
vkni
"Кресты" вот в области стандартов и обновлений очень хороши. У них море недостатков, но вот конкретно стандарты, обратная совместимость на уровне.
netch80
> но вот конкретно стандарты, обратная совместимость на уровне.
Хорошая шутка, спасибо.
Можно начинать смотреть отсюда (всю ветку, там много весёлых гитик).
Или на cppreference.com отменённые совсем в каких-то ревизиях возможности (например, auto_ptr в C++17 уже нет — ну да, GCC рисует для совместимости).
vkni
Я сформулировал таки требование, которое у меня есть - простая для реализации стабильная (на ближайшие 5-10 лет) спецификация языка и ядра системной библиотеки. Если это системный язык, то, кмк, это вполне обосновано.
Из этого следует возможность этих 22 компиляторов и прочего. Завязанность на единственный llvm, приведёт к тому, что модульная независимая система превратится в кубик-рубик-монолит. Это закроет возможность развития.
amarao
У языка есть поддержка стабильных редакций. Условно говоря, в 2021 вы как писали на Rust-2018, так и пишите, и даже линковаться с либами на rust-2021 можете.
Вот llvm вопрос открытый. Уж очень удобно иметь единый бэкэнд.
vkni
В инженерии всё всегда имеет свою цену. Если мы/вы думаем, что что-то бесплатно, это повод её поискать.
Можете называть это эстетическим чувством, но мне не нравится, когда экосистема превращается в кубик-рубик-монолит. С единым компилятором, большим кол-вом пакетов в cargo, и llvm оно неизбежно превратится в нечто такое необозримое и неизменное.
cepera_ang
И какая цена отказа от того, во что вложены миллионы человеко-часов? :)
domix32
Это создает некоторую конкуренцию, что лучше чем монополия.
AnthonyMikh
Как вы их столько насчитали? В Rust есть только две разновидности макросов: декларативные и процедурные.
Cerberuser
Декларативные двух видов (macro_rules! и нестабильный macro), процедурные - трёх (derive, attribute, function-like).
AnthonyMikh
С точки зрения писателя макроса — два с половиной вида, с точки зрения пользователя — один (function-like, а как именно реализован — неважно).
cepera_ang
Раст конечно не панацея, но кажется что его сложность — это просто показ сложности реальных программ в явном виде, в сравнении с тем же Си, где все те же сложности просто заметены под коврик и может быть повезет и обнаружатся до того, как код задеплоится на миллионы или миллиарды машин.
indestructable
Вот именно. Си намного сложнее Раста, не в плане синтаксиса, а в понимании того, как на самом деле будет работать программа
inferrna
>сложность языка имеет свойство перекладываться на сложность программ
В расте очень много сахара, который частично компенсирует сложность bc.
Зависит от масштаба. Мелкая утилита/библиотека на расте будет чуть сложнее. Зато на крупном проекте будет меньше граблей и головной боли с отладкой сегфолтов.
PsyHaSTe
Ключевое слово — кажется. Раст просто в язык добавляет явно некоторые концепции (вроде времени жизни объекта) которые и в Си и в Сипипи существуют, но на уровне "в голове у разработчика". Шаг же от динамики (в голове у разработчика) к статике (записано в типах) всегда же должен приветстоваться, тем более в таких важных вещах как системное ПО.
artemisia_borealis
Ну, справедливости ради, самое близкое это всё же режим Better C в Dlang. Там часть самострелов в ногу устранено. Но главный плюс это то, что перетекать на этот компилятор можно начать плавно с уже имеющейся кодобазой.
amarao
Имеющаяся codebase, которая основна на цементировании UB, это, скорее, минус. Собственно, поговорка, что Rust - это C++ из которого убрали С.
bm13kk
Чисто ради интереса. А как оно происходит в Си?
Программа ождидает что здесь по адресу лежит положительное число. Пришло отрицательное. Код не упал и продолжил работать с отрицательным числом.
Как это работает?
amarao
Если это энкапсулированное число (т.е. код его не использует), то просто оставляет как есть. Если использует, но в виде "просто данных" (например, делает +1), то из -1 становится 0. Если это смещение к указателю, то UB. Что будет, то будет, а CVE не миновать.
(Это в отсутствие явной проверки в коде)
netch80
> Программа ождидает что здесь по адресу лежит положительное число. Пришло отрицательное. Код не упал и продолжил работать с отрицательным числом.
А общего рецепта не будет, всё зависит от конкретного кода.
Ну например вы пишете
будет пустой цикл.
А если вы сделаете
то оно пойдёт выполняться по всем положительным, заврапится и на отрицательные числа (4 миллиарда проходов не хотите?)
А если компилятор опознает, что там отрицательное — то может вообще выкинуть цикл как неисполнимый. Вопрос, дадут ли ему это опознать — зависит от погоды на Юпитере и фазы лун Альдебарана (точнее, от всего входного кода и версии компилятора).
Но специфика оптимизации тут только в этом выкидывании, код и так был некорректный. В идеале надо было бы какой-нибудь assert на предусловие n_elems >= 0… хотя <= и так помогает неплохо. Поэтому при любой возможности тут лучше делать такие проверки.
(GCC, что характерно, обычно превращает такой цикл в сначала проверку что n_elems >= 1, а затем уже в теле цикла меняет <= на !=. Вот удобнее ему так. Но имеет право. А вот если бы совсем выкидывал проверку — был бы неправ.)
bm13kk
cc @amarao@vanxant
Я не, видимо, не правильно задал вопрос что его смысл потерялся.
Я немного писал код на С в студенчестве. У меня есть идеи, как себя поведет компилятор.
Вопрос именно в контексте аргументов Торвальдса. Как пишут драйверы, что они устойчевы в повреждению памяти? Весь мой опыт (10 лет, 3 основных и десяток вспомогательных языков) - говорит что оно все равно упадет. Что даже, если каким-то чудом, драйвер продолжит работу - вероятность поломки/потери чего-то ценного огромна.
Можно, например, написать какой-то глобальный цикл (или демона де-факто) на 10 строк. Он никогда принципиально не упадет. И любую логику делать внутри цикла и трая. Тогда ошибки перехватываются и *формально* драйвер продолжает работать. Но этот паттерн применим к любому языку.
Я уже молчу про контраргумент в дргом сообщении - из С тоже можно вызвать аналог паники в любом месте.
netch80
> Вопрос именно в контексте аргументов Торвальдса. Как пишут драйверы, что они устойчевы в повреждению памяти?
Они не устойчивы. Но в Linux есть защита, что если драйвер повредил только собственные структуры, то ядро хотя бы успеет сказать «мяу» и скинуть лог проблемы. Срабатывает не всегда, но достаточно часто, чтобы собирать трейсы, логи и прочие данные, по которым можно понять, что случилось.
А в остальном — повезёт или не повезёт.
Проблема с Rust, насколько я вижу, в интеграции его рантайма. Пока что есть с этим проблемы — в отличие от C, которому в варианте ядра Linux достаточно было настроить стек на кусок RAM, argc+argv и какой-то простейший malloc, дальше он уже работал.
Но, думаю, их решат без особых затяжек.
Если я всё ещё не на то отвечаю — переформулируйте.
bm13kk
Нет, теперь понятно, спасибо.
Но опять же получается что это не проблема Раста - а лок линукса на си
b-s-a
Нет, нет никакого лока. Ядро стартует когда нет ничего, кроме непрерывного куска памяти с кодом и процессора. В этом режиме очень хочется не отвлекаться на всякие магические процедуры, которые непонятно когда запускаются и сколько времени тратят. А вот когда ядро стартануло, запустило все дрова и инициализировало вверенное оборудование, то тогда можно и свистелки-перделки запускать.
bm13kk
"Лок" не всмысле управления памятью и процессами. А в смысле кода захардкодженного на определенное поведение определенной реализации. Которое, как я понимаю, еще и не является частью стандарта.
b-s-a
memset - это стандартная функция. Она есть в стандарте. Более того, сам Линукс к подобным функция не привязан. Более того, если не ошибаюсь, там вообще стандартная библиотека в ядре отсутствует.
А причина, по которой не хотят переносить ядро на другой язык, кроме того, что придется все переписывать, заключается в том, что другие языки требуют для работы программы поддержку со стороны стандартной библиотеки (runtime), а вот программа на Си не требует - все конструкции языка будут компилироваться даже без библиотеки.
bm13kk
Мне (и возможно не только) очень интересно за что минус. Потому что аргументация в комментарии мне понятна (хотя я все еще не согласен с Торвальдсом).
Но я не сишник, чтобы самому знать что в нем не верно.
b-s-a
Тут кто-то очень щедро минусы расставляет. У меня почти все комментарии с минусами тут. :-)
netch80
Ну скорее не на C, а на конкретные ABI его реализации. Но C тут тем хорош, что его рантайм имеет очень ограниченные требования — фактически, только обеспечение стеком, правила передачи аргументов/результата и явно вызванные функции библиотеки. И под конкретный ABI на конкретной платформе можно уже и подстроить заточку. Уже с C++ так легко не получилось (хотя в причинах я не уверен — можно было бы уточнить в режиме без исключений и RTTI).
codelock
Вы не поняли. Ядро это приложение не "пользовательского пространста", где аварийное завершение работы, в общем, не является катастрофой. Как само ядро, так и его модули должны иметь возможность продолжать работу даже при "обстоятельствах непреодолимой силы", до тех пор, пока позволяет железо, а неотключеамое состояние паники совершенно не вписывается в сценарий использования.
kmeaw
Но ведь такие обстоятельства существуют и для C, например UB. Почему вызов паники из C-кода ядра - хорошо, наличие UB в C разработчики ядра терпеть готовы, а панику рантайма Rust - нет?
lorc
Причины для паники могут быть очень разными.
Как тут пишут - Раст паникует если не может выделить память.
Если же драйвер в ядре не может выделить память - он просто возвращает ошибку и все работает дальше.
Но на самом деле проблема с паниками - это не самое страшное. Я тут недавно читал цикл постов про различия в моделях памяти между растом и ядром. Там все куда сложнее. Хотя и менее очевидно.
codelock
Эти штуки из разных плоскостей. Undefined Behavior не означает аварийное завершение работы, тем более в пространстве ядра, а является формальным описанием области значений, при нарушении инварианта, которые будут отличаться от ожидаемых. Например, если Вы нарушаете закон, то Вас могут найти правоохранители и наказать, а могут не найти и всё будет ок.
Вообще, даже в высокоуровневых языках, при нарушении инварианта, можно получить неожидаемое поведение и это не означает, что все языки нужно отменить.
vanxant
А процессору пофиг, он вообще не знает, знаковые у него там числа или беззнаковые (исключая инструкции умножения и деления)
Ну а так, мусор на входе - мусор на выходе.
Потом, правда, ракеты падают и десятичная точка в сумме при банковском переводе сдвигается.
netch80
Ракета упала там, где C как раз не виноват :(
Накосячить таким методом можно с любым языком.
b-s-a
Процессору пофиг. А вот компилятору нет. Сегодня ты компилмруешь для процессора с int=int32, а завтра int=int64, и твоя кривая проверка работать перестанет. Поэтому разработчики стандарта языка и компиляторов делают такие вещи. По мне, если ты делаешь какую-то дичь, которая не соответствует стандарту языка, ты обязан обложить ее проверками на архитектуру системы и написать кучу комментариев, почему ты это сделал, и как оно вообще работает. По-хорошему, компилятор должен ругаться, когда выкидывает часть кода. Ну, ненормально это в 99% случаев.
vanxant
Прикол в том, что в Си, когда я последний раз в него смотрел, нет стандартного способа сделать проверку переполнения. В отдельных компиляторах есть свои методы типа
__builtin_add_overflow_p
, но не во всех, и главное в стандарте языка их нет. Крутитесь как хотите.При том что спор, тащем-та, о трёхстрочных функциях. Допустим, у нас есть
Для проверки на то, не было ли переполнения при выполнении условной операции ⊕, достаточно написать
netch80
> Для проверки на то, не было ли переполнения при выполнении условной операции ⊕, достаточно написать
> (long)(a ⊕ b) == ((long) a) ⊕ ((long) b)
А теперь повторите этот фокус, например, с 64-битным long при отсутствии поддержки 128-битного типа.
А потом вспомните цену поддержки 64-битного long long на 32-битной платформе и, соответственно, цену такой проверки, по сравнению с более близкой к возможностям машины (которые обычно делают проще).
vanxant
Заметьте, я сказал написать (на Си), а не скомпилировать под конкретный процессор. В комитете не совсем идиоты сидят, наверное, что столько лет не пускают это в стандарт.
netch80
> Заметьте, я сказал написать (на Си), а не скомпилировать под конкретный процессор.
Отлично, и как вы обеспечите этот фокус на long значениях, если вдвое более широкого типа данных просто не дают (по стандарту)?
> В комитете не совсем идиоты сидят, наверное, что столько лет не пускают это в стандарт.
В комитете сами по себе — безусловно, не идиоты. А люди, защищающие бизнес-интересы конкретного представителя (чаще всего — компиляторов) так, как эти интересы понимают владельцы их фирм. И эти интересы не совпадают в подавляющем большинстве случаев.
vanxant
Все реально используемые процессоры умеют в произвольное удлинение целых. Хоть с флагом переноса, хоть с трёхместными инструкциями, хоть как. Ну просто потому что без этого тот же финансовый софт не запустишь. Бэкэнд компилятора должен в такое уметь, даже если на его фронте таких типов нет.
netch80
> Все реально используемые процессоры умеют в произвольное удлинение целых.
1. Это сильно дороже.
2. Вы сказали «все реально используемые процессоры», но не сказали, что это обеспечено библиотекой. Это значит, что тот, кто заботится о проблеме, должен сам писать такой код? Ещё и продираясь через то, что некоторые фишки процессора (типа флагов CF, OF) недоступны в C и их функциональность надо эмулировать?
Ладно, со сложением или вычитанием — там достаточно просто написать что-то типа
а с умножением как? Приведите тут (хотя бы для себя), как это будет выглядеть в коде. Можно в отдельную функцию.
> Бэкэнд компилятора должен в такое уметь, даже если на его фронте таких типов нет.
Вы сами сказали в своём предыдущем комментарии:
> Заметьте, я сказал написать (на Си)
Какой бэкэнд? Какой фронтэнд? Вы или крестик снимите, или трусы наденьте ©, или мы говорим про язык, или про свойства и возможности конкретных компиляторов.
vanxant
Воу-воу, палехчи. Я, похоже, в отличие от вас, читал исходники gcc.
Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например a = b = INT_MIN. Вы забываете, что инструкция сравнения это "вычитание без запоминания результата". Если в процессоре нет флагов, как в RISC-V, то ваш код не взлетит (а если флаги есть, то он не нужен).
умножение двух интов сразу даёт long (и гарантированно в этот long влезает что для знаковых, что для беззнаковых). Тут даже делать особо ничего не требуется, просто не упаковывать long обратно в int.
netch80
> Воу-воу, палехчи. Я, похоже, в отличие от вас, читал исходники gcc.
Значит, вы утверждаете, что я не читал их.
Это попросту неправда, и продолжение дискуссии с вами будет только тогда, когда вы извинитесь за подобные предположения в публичной дискуссии.
Не вижу смысла продолжать по сути, пока вы не в состоянии это делать со своей стороны.
(Не вспоминаю пока, зачем тут вообще нужны исходники gcc к данной дискуссии. Но это мы обсудим после ваших извинений.)
vanxant
Слово "похоже" вам ни на что не намекает?
Ну раз читали, освежите память. Исходники gcc вот: https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/gcc/internal-fn.c
начиная с 686 строки.
Конкретно для сложения и вычитания int-ы и вообще signed типы достаточно расширять до соответствующего unsigned (что само по себе CWE-195, но компилятору можно :)
netch80
Мой пример на проверку до собственно сложения. Вы сказали:
> Нет, недостаточно, в вашем подходе нужно рассмотреть все варианты, ну например 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: скосил нетехническую часть, так лучше для долгой истории. Всё остальное осталось в личке.
mayorovp
Посмотрите на код ещё раз — этот вариант там обрабатывается.
vanxant
Да, посмотрел ещё раз - этот код работает. Хотя и использует 2 ветвления вместо одного.
ainoneko
b-s-a
Речь про процессоры, а не про языки программирования. Если вам нужно сложить два числа разрядностью 32 бита на 8-ми битном Z80, например, то вы делает это так:
Как не трудно заметить, сложность тут чисто линейная, чуть дороже, чем сложение 4-х 8-ми битовых чисел. И никакие библиотеки тут не нужны. А вот на языках высокого уровня сделать это значительно сложней, так как ни один язык, что я знаю, не предоставляет информацию о переполнении.
mayorovp
Речь именно про различие процессоров и языка программирования Си.
Отлично, вы написали код сложения двух "длинных" чисел на ассемблере. А теперь попробуйте повторить тот же самый способ сложения на Си.
b-s-a
Согласен, это только на ассемблере можно так просто сделать. На Си будет чуть сложней.
Да, это несколько дороже... Возможно, компилятор при оптимизации сможет что-то сделать, но не уверен.
mayorovp
А теперь сложите не 8 по 32 разряда, а 4 по 64.
b-s-a
С практической точки зрения складывать надо числа на 1 шаг разрядности меньшие, чем поддерживаются языком. Иначе будет то, что вы ожидаете от меня получить.
ЗЫ: Я знаю, как будет выглядеть код, если пытаться складывать 64-х битные числа.
mayorovp
Вот-вот, "на процессоре" можно, как правило, складывать числа максимальной разрядности, а в языке Си — на 1 шаг меньше.
И после этого кто-то ещё пытается утверждать что язык Си к железу близок!
vanxant
FYI, в питоне целые сразу произвольной длины "из коробки". Типа как строки. Поэтому для вычислений там все юзают numpy и прочие написанные на С либы:)
b-s-a
Я вот не знаю Питона :-)
cepera_ang
Не поэтому :)
netch80
> Речь про процессоры, а не про языки программирования.
Процессоры ой разные бывают. Вот смотрим на GMP код для MIPS, со строки 64 — метод может быть повторен 1:1 с использованием беззнаковых целых на каждом шагу, типа такого:
Проще никак — флагов нету, всё съели. RISC-V — точно так же.
> А вот на языках высокого уровня сделать это значительно сложней, так как ни один язык, что я знаю, не предоставляет информацию о переполнении.
Увы, придётся повторить тот же метод. Но это сработает.
Для знаковых — сравнить знаки аргументов со знаком результата, тоже легко.
Несколько неформальным тут может быть побитовая конверсия между знаковым и беззнаковым одинаковой ширины, но это сейчас реально всеми допускается, а с учётом обязательного дополнительного кода в C++20 (и ожидаемом таком же в C) — проблемы не составит.
flx0
__builtin_add_overflow есть в gcc и clang. Да, этой фичи нет в стандарте, но она есть в реальном мире.
adeshere
Я не настоящий сварщик, но в 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.
AnthonyMikh
Rust предоставляет.
b-s-a
Тогда понятно, почему продвигают Rust для разработки ядра.
b-s-a
Цена 64-битнго сложения на 32-битной машине равна двум 32-битным сложениям: сложение и сложение с переносом. Ничего тут страшного нет. Другое дело, что неправильно так делать для проверки, с учетом того, что сам процессор, обычно, знает, когда произошло переполнение.
b-s-a
Согласен с тем, что в Си нет проверки на переполнение. Было бы очень здорово, если бы оно было в виде int add_int(int *a, int b, int с) { ... *a += b + c; ... } , где возвращаемое значение 0 - если все в порядке, -1 если переполнение в отрицательную сторону, 1 - в положительную. В этом случае очень удобно бы было делать операции с многоразрядными числами.
netch80
> Было бы очень здорово, если бы оно было в виде int add_int(int *a, int b, int с) {… *a += b + c;… }
Почти то же (без знака переполнения) есть в GCC, Clang и вслед за ними ICC. Осталось протолкнуть в стандарт :)
Знак можно вычислить по другим признакам (например, если вообще произошло переполнение, то в ваших терминах b!=0 и знак b соответствует направлению переполнения). Этого достаточно.
mva
Вот бы ещё у раста с портабельностью (в т.ч. на "старое" железо) было получше...
А ещё со временем сборки. Как самого тулчейна, так и программ на нём...
А ещё с весом конечных бинарников...
INB4: "в 21 веке считать ресурсы".
amarao
Размер бинарников во многом зависит от runtime'а (который предоставляет много сервиса, типа разворачивания стека при паниках и т.д.). Рантайм в расте опциональный, и люди вполне пишут под embedded без него.
Вторая проблема - generic'и, которые сильно раздувают код (хотя и обещают работать быстрее). Возможное решение - использование динамической диспетчеризации (чуть медленее, зато компактнее).
В целом, я не видел, чтобы rust генерировал код существенно больше, чем Си (для этого надо смотреть на размер кода функций, а не итогового бинаря, потому что, повторю, опциональный рантайм толстоват).
А вот с поддержкой странных систем... Это да.
С другой стороны, там иногда такая поддержка Си, что попытка там собрать хоть что-то большое вызывает страх и ужас.
PsyHaSTe
У меня знакомый пишет под старое железо с DOS и микроконтроллеры — все отлично с портабельностью у раста. Не си конечно, но вполне достойно
Время сборки — в релизе с lto да, печально, есть куда улучшать. В остальном — вполне можно жить
Jipok
А zig?
netch80
В GCC/Clang это уже частично решили через __builtin_${op}_overflow(). Пока только в избранных местах, без левого сдвига, но это уже хоть какая-то поддержка (можно написать хотя бы переносимый между процессорами враппер).
Теперь ждём ещё 30 лет для пробивания этого в стандарт.
amarao
Я не настолько глубоко в gcc, чтобы понять о чём речь. Грубо говоря, если я сделаю int(max_int)+1 - оно такое отловит? А int(max_int/2)*2? А int (min_int)*int(min_int)?
netch80
> Я не настолько глубоко в gcc, чтобы понять о чём речь. Грубо говоря, если я сделаю int(max_int)+1 — оно такое отловит?
Да, отловит. Если вы сделаете, например,
ovf будет установлено в 1 — случилось переполнение.
> А int(max_int/2)*2? А int (min_int)*int(min_int)?
Точно так же, через __builtin_mul_overflow().
И, так как все три параметра могут быть разного типа, можно ловить неудачную попытку сужения значения:
поставит 1 — потому что 999 не помещается в int8_t.
Эти же интринсики есть в Clang. В GCC есть ещё _p вариант, который позволяет проверить, например, что значение влезло в битовое поле (Clang это себе ещё не перенёс).
amarao
Спасибо.