LINUX.ORG.RU

Если вам не хватало UB в C, то вам принесли ещё

 ,


1

3

Привет, мои дорогие любители сишки!

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

— zero-sized reallocations with realloc are undefined behavior;

То есть вот это валидный код:

void *ptr = malloc(0);
free(ptr);

А вот это – UB:

void *ptr = malloc(4096);
ptr = realloc(ptr, 0); <-- хаха UB

И это несмотря на то, что в манах уже давно написано следующее:

If size is equal to zero, and ptr is not NULL, then the call is equivalent to free(ptr)

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

В тред призываются известные эксперты по C: @Stanson и @alex1101, возможно они смогут нам объяснить, зачем разработчики стандарта C постоянно пытаются отстрелить себе обе ноги самыми нелепыми способами.



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

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

Почему скрывают? Rust.

Страшный заговор спонсируемого некрософтом глубинного комитета по низвержению сишки и навязыванию миру Б-гомерзкого руста-педеруста.

Hertz ★★★★★
()
23 марта 2024 г.
Ответ на: комментарий от fsb4000

@cumvillain @monk @Werenter @soomrack @X512 @hateyoufeel @firkax @MOPKOBKA @Forum0888 @

Вот новый пропозал увидел от JF Bastien, скорее всего будет принят в С++26:

P2809R3
Trivial infinite loops are not Undefined Behavior

https://isocpp.org/files/papers/P2809R3.html

По крайней мере по второй ревизии проголосовали «за»

Poll: forward P2809r2 “Trivial infinite loops are not undefined behavior” option #2 to CWG for inclusion in C++26.
SF 	F 	N 	A 	SA
6 	11 	3 	1 	0

Result: Consensus.
fsb4000 ★★★★★
()
Ответ на: комментарий от hateyoufeel

Почему нельзя было 40 лет назад это всё сделать?

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

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

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

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

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

void f(void) {
  char arr[1000000];
  sprintf(arr, "a");
}
По стандарту тут походу перед вызовом sprintf должно идти бесполезное зануление целого мегабайта стека. Ладно, про sprintf компилятор может догадаться что ему это не нужно, но может быть какая-то другая похожая функция. Если при этом оно ещё и много раз в цикле вызывает, всё совсем печально будет.

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

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 2)
Ответ на: комментарий от firkax

Я подозреваю, что ув.тов.@hateyoufeel чистый Си не интересует. Но и чистый Си с отставанием в десяток лет что-то из C++ перенимает. Так что может и до условного C36 что-то такое дойдет :)

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

По-моему интересует. Да и тема, в которой мы находимся, именно про Си.

Так что может и до условного C36 что-то такое дойдет :)

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

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

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

што? Лмао

Почему не сделать обращение к неинициализированном переменной ошибкой сборки? Тоже мощностей не хватало? Или мозгов? Эта штука является UB уже 35 лет, можно было бы при компиляции без проблем ловить.

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

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

Починил.

В Си же неявного поведения быть не должно

Си весь и целиком состоит из неявного поведения.

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

Почему не сделать обращение к неинициализированном переменной ошибкой сборки?

Ставь -Wuninitialized -Werror, всё в твоих руках. Но вообще - компилятор 100% надёжно это определить не может даже сейчас, спорные случаи разумеется трактуются в пользу «проблем нет».

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

Почему не сделать обращение к неинициализированном переменной ошибкой сборки?

не сборки, а компиляции.

опять же случай вида

int x;
fun ( &x );

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

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

Всегда инициализирую переменные и non problem.
Си и C++ не любят «расхлябонность» (тех кто ходит/кодит с незавязанными шнурками на ботинках).

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

Эта штука является UB уже 35 лет, можно было бы при компиляции без проблем ловить.

В MSVC есть такое предупреждение: https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-1-and-level-4-c4700

И оно появилось довольно давно. В VS 2005 оно есть, а более старой MSVC у меня нет, чтобы проверить когда это предупреждение появилось.

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

Пост для форумчан.

https://learn.microsoft.com/ru-ru/cpp/build/reference/compiler-option-warning... /w, /W0, /W1, /W2, /W3, /W4, /w1, /w2, /w3, /w4, /Wall, /wd, /we, /wo, /Wv, /WX (уровень предупреждения)

https://dzen.ru/a/YCADH_Klbw6qr_wC Конфигурация компилятора: Уровни предупреждений и ошибки

https://russianblogs.com/article/62291613498/ Параметры отображения информации о компиляции Visual Studio

Forum0888
()
Последнее исправление: Forum0888 (всего исправлений: 2)
Ответ на: комментарий от fsb4000

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

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

Making this behavior undefined continues to allow existing implementations to do as they please, but provides a clear warning to developers to guard against zero-byte reallocations.

Звиздец.

Они дебилы и не лечатся?

«to allow existing implementations to do as they please» — должно являться implementation defined behavior.

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

Только щас увидел эту относительно старую тему. Читаю комменты. Много разумных мыслей высказано здесь.

Теперь у нас есть старая версия стандарта, которая делала IDB, и есть новая, которая делает UB.

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

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

wandrien ★★
()
Последнее исправление: wandrien (всего исправлений: 1)
Ответ на: комментарий от no-such-file

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

Зато я знаю программу, которая при наличии UB передаёт управление в случайную функцию, которая оказалась в бинарнике рядом. Годится как пример потенциального «форматирования диска»?

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

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

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

новая версия прямо называет это UB и таким образом понуждает разработчика к проверке параметра перед подстановкой.

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

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

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

Нет. При наличии UB никто варнинг давать не обязан. Это и есть фишка UB. То есть если он будет – компилятор молодец. Если не будет – пишите письма в спортлото.

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

То есть если он будет – компилятор молодец. Если не будет – пишите письма в спортлото

ЛОЛ, а если компилятор форматирует диск вообще по желанию левой пятки, даже если это не UB, то кому писать? Подобные вещи лежат вообще вне контекста софта, а в контексте отношений и права. Как бы gcc поставляется с ABSOLUTELY NO WARRANTY, да и остальные компиляторы тоже. UB или не UB разницы нет никакой.

no-such-file ★★★★★
()
Ответ на: комментарий от hateyoufeel

So glad you asked! The best ghc bug ever involved a dev version of the compiler deleting your source file if it contained a type error.

Считаю, сишные компиляторы должны сделать это фичей.

Бомбически!

А @Syncro еще жаловался, что git портит ему рабочую копию. Нужно удалять её к чертям!

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

У Си тоже прибитость к рантайму (libc) и модулям (A-Out, COFF, ELF) и на ранних этапах он был ни с чем не совместимой закрытой системой. Это уже потом в виду популярности Си, рантайм и модули изначально разработанные специально для Си начали использоваться в других языках. Печальное наследие этого наблюдается и чейчас в убогом и крайне хрупком механизме связывания символов ELF. В DLL (NE, LE, PE) это сделано намного лучше потому что формат ориентировался на Паскаль у которого есть явные модули с отдельными областями видимости.

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

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

Clearly the ‘fix’ is to stop using dev before it is tested, but building with -fno-delete-null-pointer-checks flag at least makes it harder to abuse.

В принципе, разумно.

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

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

Почему не сделать обращение к неинициализированном переменной ошибкой сборки?

Патамушта.


extern void foo(int * p);

void bar()
{
    int a;
    foo(&a);
}

А так в целом очень правильное направление. Но опоздавшее лет на 20.

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

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

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

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

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

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

— Я правильно интерпретирую семантику вопроса, но полностью игнорирую его суть.

— Не могли бы вы привести пример?

— Мог бы.

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

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

Свежий шаг комитета С++ – шаг в правильном направлении. Хотя он также подвержен той же проблеме.

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

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

Вот поэтому и пришлось ввести понятие UB. Программист отвечает за то, чтобы роботы не навредили.

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

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

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

Вот поэтому и пришлось ввести понятие UB.

Тут, наверное, важно разграничить.

Часть UB — это операции, которые проверить можно, но дорого. Например, переполнение числа при каждом арифметическом действии.

А часть UB — проверить в рамках заданной семантики языка и конкретной реализации в принципе нельзя. Например, разыменование мусорного указателя в Си для типичного компилятора.

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

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

Тут, наверное, важно разграничить.

Ну а вот сабжевая история с realloc() ни под один из двух вариантов не подходит. Это случай, когда нечто объявили UB просто чтобы показать кукиш всем разрабам реализации. Типа вы между собой не договорились, а мы вас за это поленом.

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

По стандарту тут походу перед вызовом sprintf должно идти бесполезное зануление целого мегабайта стека.

Для таких случаев лучше всего в dlang сделали, там нужно явно указывать что переменная не инициализирована (Void Initialization) (псевдо)код выше для D будет такой:

void f(void) {
  char arr[1000000] = void;
  sprintf(arr, "a");
}

В rust тоже подобный механизм есть MaybeUninit и тоже явный и unsafe.

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

Часть UB — это операции, которые проверить можно, но дорого. Например, переполнение числа при каждом арифметическом действии.

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

А часть UB — проверить в рамках заданной семантики языка и конкретной реализации в принципе нельзя. Например, разыменование мусорного указателя в Си для типичного компилятора.

Опять же, зачем это делать UB?

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

А похрен. Такой код легко чинится.

Что именно чинится?

ИМХО, оно бы чинилось, если бы для параметров-указателей принудительно задавалось бы in, out или inout.

Тогда бы:

extern void foo(in int * p);

void bar()
{
    int a;
    foo(&a);
}

легко трактовалось бы как ошибка, тогда как вот это было бы OK:

extern void foo(out int * p);

void bar()
{
    int a;
    foo(&a);
}

Но в стандарте подобного нет. И вряд ли будет.

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

Да нет, просто зануляешь переменную и всё.

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

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

Опять же, зачем это делать UB?

В смысле «зачем»? Это просто по факту UB, приводящее к непредсказуемым последствиям. Проверить его можно только в рамках интерпретатора, но при не компиляции в привычный вид с сырыми указателями.

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

Вот это уже другого рода вопрос. Тут нет объективной причины, кроме «так исторически сложилось».

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

  • Wrap around. (unchecked)
  • Checked с проверкой и броском исключения.
  • Целые числа с NaN. (По сути, более эффективный аналог Optional.)

И без всяких UB на этот счёт.

Собственно почти так сделано в C#. Там есть checked и unchecked. MS тут отработали хорошо.

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

В смысле «зачем»? Это просто по факту UB, приводящее к непредсказуемым последствиям. Проверить его можно только в рамках интерпретатора, но при не компиляции в привычный вид с сырыми указателями.

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

Вот это уже другого рода вопрос. Тут нет объективной причины, кроме «так исторически сложилось».

Объективная причина тут в том, что раньше существовали платформы с разными вариантами знаковой арифметики. Сегодня их не существует, всё вокруг является two’s complement. А значит в этом говне смысла нет и можно объявлять знаковое переполнение вполне определённым хоть завтра. Просто сишники любят, когда им жопу отрывает.

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

Да нет, просто зануляешь переменную и всё.

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

[[force_uninitialized]] unsigned char data[100500];
read_from_file(file, data, 100500);

Но почему так долго ничего в этом направлении не могли сделать… Загадка, однако.

eao197 ★★★★★
()