LINUX.ORG.RU

GCC 15 ломает код сишникам

 , ,


1

6

Привет, ЛОР!

Из-за того, что GCC по умолчанию переходит на стандарт C23 для компилируемого кода, возникли некоторые интересные нюансы. Так например, начиная с C23 пустой список аргументов у функции теперь будет считаться объявлением с void. То есть, следующие два объявления будут эквивалентными:

void f1() { }
void f2(void) { }

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

typedef int (*fptr)();

int f1(void) { return 1; }
int f2(int a) { return 2 + a; }

int main(void) {
  fptr f;
  int r = 0;
  f = f1;
  r += f();
  f = f2;
  r += f(1);

  return r;
}

Подобный код больше не будет собираться. Сломанными оказались такие программы как: Linux (ядро), bash, iwd, samba, bluez, rustc, gnupg, vde2, sudo, gdb, postgresql, guile, w3m, freeglut, neovim, dnsmasq и куча других.

Бонусом к этому будет добавленный в mbedtls (и не только) баг, вызванный иным порядком инициализации union.

int main() {
  union {
    int dummy;
    struct { int fs[4]; } s;
  } v = { 0 };
  printf("%d\n", v.s.fs[3]);
  return 0;
}

В gcc 14 и ранее код выведет 0, начиная с gcc 15 – мусор.

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

За наводку можно поблагодарить забавнишегося @sf:

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

Люблю сишечку за возможность очень вариативно, изобретательно и с огоньком стрелять себе в ногу.

И ни одного, сцук, предупреждения.

vadim@aquila:/tmp/1$ cat args.c 
#include <stdio.h>

#ifdef BOOM
__attribute__ ((stdcall))
#endif
void f() {
  printf("Hello, LOR!\n");
}

int main(void) {
  int i = 0;
  while (i < 1000 * 1000) {
   f(1,2,3);
   i++;
  }
  return 0;
}
vadim@aquila:/tmp/1$ gcc -m32 args.c -o args -Wall -Wextra  
vadim@aquila:/tmp/1$ ./args | wc -l
1000000
vadim@aquila:/tmp/1$ gcc -DBOOM -m32 args.c -o args -Wall -Wextra  
vadim@aquila:/tmp/1$ ./args | wc -l
697685
vadim@aquila:/tmp/1$ 
wandrien ★★
()
Ответ на: комментарий от hateyoufeel

А и страдали, пока массовый софт не перекатился на 64 бита. Там под 32 бита разных calling convention наплодили как у дурака фантиков. А вменяемой возможности работать с переменным числом аргументов даже под 64 бита до сих пор не сделали, кстати.

И как говорит один блогер в ТГ, «и никто, никто в этой цепочке не задался хрестоматийным вопросом, а не херню ли мы делаем», когда позволяем объявлять функцию, которая сама чистит за собой стек, как foo() с пустыми скобками.

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

-Wall -Wextra не гарантируют включение всех возможных варнингов. Eсли собираешь без -Wstrict-prototypes, то ты ССЗБ. Во всех адекватных проектах, что я видел, эта опция используется.

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

то ты ССЗБ

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

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

Нет, несоответствие количества аргументов падает и в -std=c89 и даже в древних gcc, и везде вроде неотключаемо. В c23 изменилось то что пустые скобки стали означать ноль.

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

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

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

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

Для статических переменных ситуация другая - они инициализируются на этапе компиляции и никакого неявного кода не генерят

Ещё как генерят. Как они, по твоему, инициализируются при запуске программы? Открой для себя секцию .init и этап выполнения pre main. В эмбеддинге, скажем, этому оччень большое внимание уделяется, по очевидным причинам.

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

Кажется что компилятор должен по умолчанию собирать с нормальными флагами

Компилятор по умолчанию должен соблюдать совместимость с юниксовыми скриптами, в которых может быть echo "main(){}" > test.c && cc -o test test.c, написанное когда-нить в 70-е года. Не забывай, что в юниксах, в отличие от оффтопика, компилятор это не какое-то дополнительное приложение, а часть интерфейса ОС.

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

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

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

Компилятор по умолчанию должен соблюдать совместимость с юниксовыми скриптами, в которых может быть echo «main(){}» > test.c && cc -o test test.c, написанное когда-нить в 70-е года. Не забывай, что в юниксах, в отличие от оффтопика, компилятор это не какое-то дополнительное приложение, а часть интерфейса ОС.

Кому должен? Новые релизы GCC регулярно ломают сборку всего подряд, включая ядро, libc и firefox.

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

Компилятор по умолчанию должен соблюдать совместимость с юниксовыми скриптами, в которых может быть echo "main(){}" > test.c && cc -o test test.c, написанное когда-нить в 70-е года.

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

f() {}
f(i) int i; {}

Зачем и для чего, история умалчивает…

Компилятор по умолчанию должен

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

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

Пару лет назад креативщики из GNU сломали egrep и не крякнули. Народ схавал, а фигли делать.

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

Как это не сделали, многоточие же.

Нормально - это вот так:

import std.stdio;

void foo(string[] args...)
{
	foreach (arg; args)
	{
		write("=> ");
		writeln(arg);
	}
}

void main()
{
	foo("Привет", "ЛОР", "!");
}

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

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

Компилятор по умолчанию должен соблюдать совместимость с юниксовыми скриптами, в которых может быть echo «main(){}» > test.c && cc -o test test.c, написанное когда-нить в 70-е года

Какое говно из 70х, тут сишную прогу из 2005 собрать проблема.

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

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

Великая совместимость хрюниксов это поздний миф среди людей, которые настоящих хрюниксов в глаза не видели.

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

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

Дядя, а ты точно читать умеешь? Про этот миф упоминалось в комменте, на который я ответил.

А как в этом списке оказался rustc? Он что не компилирует сам себя?

Стопудов какие-нибудь обвязки вокруг сишного системного говна и/или llvm.

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

Сломанными оказались такие программы как: Linux (ядро), bash, iwd, samba, bluez, rustc, gnupg, vde2, sudo, gdb, postgresql, guile, w3m, freeglut, neovim, dnsmasq

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

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

Ты из альфы то сначала выпусти свой яп, потом рассказывай про экспертную экспертность в компиляторах

А как связана экспертность в <компиляторах> с дизайном <ЯП>?

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

До C23 было так:

If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not equal the number of parameters, the behavior is undefined. If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, …) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined.

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

Если аргументы в вызове функции соответствовали аргументам в её определении, то UB не было. Если не соответствовали, то UB.

Теперь понятие частичной декларации убрано. Стало соответствовать принципу наименьшего удивления.

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

long int maxl();

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

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

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

Получается сейчас этот трюк с «полиморфным» указателем на функцию провернуть невозможно? Надо кастовать перед вызовом к корректному типу?

Просто могли бы придумать синтаксис вроде f(...) вместо старого варианта. От этой концепции всё равно избавиться невозможно, printf никуда не денешь.

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

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

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

Надо кастовать перед вызовом к корректному прототипу?

Ага.

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

На самом деле они именно так и сделали… xD

vadim@aquila:/tmp/1$ cat varargs.c
#include <stdio.h>

int f(...)
{
	return 42;
}

int main(void)
{
	printf("%d\n", f(1, 2, 3));
}

vadim@aquila:/tmp/1$ gcc -std=c23 -o varargs varargs.c 
vadim@aquila:/tmp/1$ ./varargs 
42
vadim@aquila:/tmp/1$ 
wandrien ★★
()
Ответ на: комментарий от vbr

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

Ну теперь это проще нагрепать по сорцам хотя бы.

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

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

typedef int (*fptr)(...);

int f1(void) { return 1; }
int f2(int a) { return 2 + a; }

int main(void) {
  fptr f;
  int r = 0;
  f = (fptr) f1;
  r += f();
  f = (fptr) f2;
  r += f(1);

  return r;
}

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

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

К слову, в C23 это теперь легальное поведение для

int f(...)
{
}

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

gaylord
()

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

thegoldone ★★
()

Это крохотное изменение внезапно сломало вагон и маленькую тележку программ под Linux

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

Жаль, что под Си нету утилиты, которая бы просто best effort fix делала под новую версию.

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

К сожалению, это невозможно в силу ограниченности выразительности C.

int f(const char **x);

Что это? Указатель на строку? Массив строк? Массив батойвых строк, который в данном контексте интерпретируется как массив строк?

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

Это указатель на указатель на const char.

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

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

А это точно - что в старых стандартах было такое требование (наличие аргумента перед ...)? Просто тот же gcc прекрасно компилирует такой код и со старым стандартом.

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

Никакого легитимного применения у него нет.

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

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

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

Как ты средствами языка реализуешь функцию печати типа printf без этой возможности?

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

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

А может и не указатель. А может это указатель на указатель на один символ. Nobody knows. Поэтому автоматический рефакторинг в C это боль и страдания.

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

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

Это не относится с ioctl() никоим образом, там va_arg():

int ioctl(int fd, int req, ...)
gaylord
()
Ответ на: комментарий от gaylord

Это не относится с ioctl() никоим образом, там va_arg():

Все такие функции за исключением с пустым списком аргументов можно сделать как va_arg, но конкретно с ioctl() va_arg там начал декларироваться исторически недавно, так как va_arg задумывался как произвольное количество аргументов и сделан был для v-функций, потому у ioctl было кривое декларирование при её реализации, долгое время там красовался указателсь и в реализации — каст указателя в int.

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

Все такие функции за исключением с пустым списком аргументов можно сделать как va_arg, но конкретно с ioctl() va_arg там начал декларироваться исторически недавно, так как va_arg задумывался как произвольное количество аргументов и сделан был для v-функций, потому у ioctl было кривое декларирование при её реализации, долгое время там красовался указателсь и в реализации — каст указателя в int.

Разве что исторически, в musl он такой с самого начала.

gaylord
()