LINUX.ORG.RU

Авторы Си — наркоманы?

 , , ,


2

5

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

Дистиллированный пример кода, который это демонстрирует:

#include <stdbool.h>
#include <stdio.h>

#define IS_HEX(x) \
    _Generic((x), \
        unsigned int: true, \
        long: false \
    )

#define X 0x80000001
#define I 2147483649

int main(void) {
    if(X == I)
        puts("X == I");

    if(!IS_HEX(I))
        puts("I is not hexadecimal");

    if(IS_HEX(X))
        puts("X is hexadecimal");

    return 0;
}

Все три сообщения будут выведены на экран.

Зачем это сделано? Кому от этого легче? Какие оптимизации это позволяет проворачивать, кроме оптимизации отстрела ног программистам? Непонятно! В общем, стремлюсь поделиться своим негодованием здесь и предостеречь будущие поколения от наступления на эти грабли.



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

Большая часть сишного кода где? Точно не в юзерспейсном OSS-софте.

Примеры из головы:

  1. Ядро Linux (да, не юзерспейс, но это один из самых жирных сишных проектов в принципе)
  2. GTK и вагоны софта на GTK.
  3. Qemu

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

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

Такая гарантия есть только потому что переполнение является UB. То есть гарантии нет.

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

Программист гарантирует

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

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

Это диалект Си с доопределённым UB.

Почему диалект? Это вполне валидный случай обработки UB – притвориться, что никакого UB нет. Так же как GCC спокойно хавает и компилирует код без ‘\n’ в конце файла, хотя может выдавать int main(){system("rm -rf /*");} вместо него.

В смысле? Вот цикл for(unsigned char i = x; i!= y; i++) { …a[i]… }. Он завершается. Но оптимизировать такой цикл нельзя в прочитать в кэш a[x..y-1]; обработать последовательно. Потому что может быть x = 200, y = 5 и тогда надо дробить на прочитать в кэш a[x..255]; обработать последовательно; прочитать в кэш a[0..4]; обработать последовательно;. Или вообще забить на оптимизацию и читать в кэш линию при чтении очередного элемента массива.

А причём тут компилятор? Компилятор кэшем не рулит никак, ололоэ. Хуже того, gcc для int и unsigned int вообще идентичный код выдает.

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

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

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

Почему диалект? Это вполне валидный случай обработки UB – притвориться, что никакого UB нет.

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

А причём тут компилятор? Компилятор кэшем не рулит никак, ололоэ.

Зависит от процессора. https://www.felixcloutier.com/x86/prefetchh

Хуже того, gcc для int и unsigned int вообще идентичный код выдает.

Потому что может доказать, что переполнения нет. Доступ к array[i] гарантирует, что i не больше 1024.

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

Для for(int i = x; i != y; i++) { … a[i] … } есть гарантия, что массив a читается последовательно (можно предсказуемо подгружать кэш и использовать всякие SIMD).

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

В смысле? Вот цикл for(unsigned char i = x; i!= y; i++) { …a[i]… }. Он завершается.

Потому что может доказать, что переполнения нет. Доступ к array[i] гарантирует, что i не больше 1024.

Ладно про стандарт, но с компиляторами-то ты свои фантазии как-то пытался соотнести?

  1. В реальности для знаковых компилятор тупо выбрасывает проверку недостижимого (с его, компилятора, точки зрения) условия , превращая цикл в вечный не зависимо от sizeof(array) и "Доступ к array[i] гарантирует, что i не больше 1024."©™.

  2. Если компилятор НЕ может доказать, что нет переполнения, так как sizeof(array) не известен, то они генерируют идентичный код для знаковых и беззнаковых счётчиков.

Для for(int i = x; i != y; i++) { … a[i] … } есть гарантия, что массив a читается последовательно (можно предсказуемо подгружать кэш и использовать всякие SIMD).

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

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

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

r--r--r--
()
Последнее исправление: r--r--r-- (всего исправлений: 2)
Ответ на: комментарий от r--r--r--
  1. Это я писал про то, что для беззнаковых иногда можно оптимизировать в диапазон. Например, если индекс unsigned, а по нему обращение к массиву меньше UINT_MAX, то i+1 > i.

  2. Потому что диапазон явный от 0 до 200. Там нет переполнения. Вот с неизвестным диапазоном: https://godbolt.org/z/a5j46Mbcs

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

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

Вот. Посмотри на код для clang: https://godbolt.org/z/aaKabe81W

Он для foo как раз пытается выделить гарантированно линейные куски. А gcc просто выключает оптимизацию.

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

Вот с неизвестным диапазоном:

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

В общем случае - код идентичный.

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

Вот. Посмотри на код для clang

Это обычные попытки loop unroll’инга не имеющие никакой связи с твоими фантазиями о работе компилятора.

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

В общем случае - код идентичный.

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

Это обычные попытки loop unroll’инга не имеющие никакой связи с твоими фантазиями о работе компилятора.

Они существенно отличаются для unsigned и int.

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

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

Так смысл UB именно в том, чтобы можно было использовать эти отдельные ветки оптимизации. Типа «обнулить диапазон от a[x] до a[y]». Если цикл с знаковым счётчиком, это гарантирует (программистом), что y > x. Можно писать оптимально (каким-нибудь аппаратно-ускоренным rep movsb). Если с беззнаковым, то надо или делать две ветки выполнения и проверкой, или не оптимизировать, а честно писать по одной ячейке за раз.

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

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

Так смысл UB именно в том, чтобы можно было использовать эти отдельные ветки оптимизации.

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

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

Не фантазируй. Компилятор не отличает вызов "медленной" функции от вызова "очень медленной" функции.

Они существенно отличаются для unsigned и int.

В твоём примере там разница в первую очередь связана с расширением до 64-ёх бит.

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

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

r--r--r--
()
Последнее исправление: r--r--r-- (всего исправлений: 4)
Ответ на: комментарий от r--r--r--

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

Для существования достаточно одного примера. Компиляторы, разумеется, знают только конкретные шаблоны. int можно транслировать в memset/memmove, а unsigned запрещено.

Не фантазируй. Компилятор не отличает вызов «медленной» функции от вызова «очень медленной» функции.

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

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

Для существования достаточно одного примера.

И ты его не привёл.

int можно транслировать в memset/memmove, а unsigned запрещено.

Ты точно не хочешь прекратить рассказывать ерунду^W^W своё 4.2?

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

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

И ты его не привёл.

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

Поставь 32 бита и будет использование UB.

https://godbolt.org/z/6sqzjYno1

Для 64 бит предполагаю, что компиляторы пользуются фактом, что длина указателя 64-бит, а значит 64-битный номер 4-байтного элемента никак не может переполниться.

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

Поставь 32 бита

Теперь ты начинаешь понимать, в чём реальная разница на твоих примерах.

Для 64 бит предполагаю, что компиляторы пользуются фактом, что длина указателя 64-бит, а значит 64-битный номер 4-байтного элемента никак не может переполниться.

Да, но нет. На самом деле компиляторы конечно же не пользуются никакими "фактами".

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

Чтобы включить вставку memset’а надо делать дополнительную логику анализа кода на невозможность переполнения беззнаковых меньше регистра. Этой логики в компиляторах нет. Компиляторы просто в соответствии со стандартом расширяют signed int до int64_t и в случае сравнения между signed int’ом и unsigned int’ом ты получаешь разницу в поведении на разной размерности, а вовсе не какую-то глубокую философию между знаковыми и беззнаковыми, про которые ты тут с упоением рассказываешь. Для unsigned int, который на самом деле uint32_t, компилятор обязан сгенерировать код, допускающий переполнение.

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

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

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

Нет никакого оправдания, чтобы не использовать rust в user space.

Есть оправдание - сборка самого Rust сложная, и не все её осилили (орастили?).

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

По миллиардам устройств,

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

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

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

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

и даже у тебя таких устройств несколько.

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

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

Нет, я имел в виду Java Card, на которой написаны прошивки сим-карт и банковских карт. Правда, интерпретатор для этой Java Card таки написан на Си, если диписик не врёт. Но это в любом случае не означает, что Си является каким-то единственным магическим языком, на котором можно написать системное ПО, а на других - нельзя. Системное ПО можно написать на разных языках и оно будет работать в любом случае. Как минимум, на Ассемблере, Обероне, Аде, Лиспе, Эль-76, Паскале, Форте - это я говорю только про старые языки, не упоминая про Раст и Зиг. В Си нет никакой такой особой магии, которая делает его уникально подходящим именно для низкоуровневого кода. А вот магия, которая позволяет в нём хорошо скрывать CVE, в Си есть, и именно она является наиболее яркой чертой Си, если мы поставим Си в ряд аналогов.

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

Я не полезу проверять, но, по памяти, старые ОС от ИБМ были написаны на ассемблере и занимали в своё время весьма почётное место, а может быть и по сей день занимают.

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

Нет, я имел в виду Java Card,

Я тебе про это и написал:

апплеты на банковской карте

Нигде больше широко Java Card не используется. При этом:

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

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

В сухом остатке, все миллионы устройств в мире пока что работают на прошивках, написанных на си / си++.

Ждём раста.

r--r--r--
()
Ответ на: комментарий от den73

старые ОС от ИБМ были написаны на ассемблере и занимали в своё время

Все старые ОС были написаны на ассемблере и занимали в своё время. А потом пришёл си и их время прошло.

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

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

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

апллеты на Java Card не имеют отношение к Java, они его имеют.

Какая Java-среда исполняет апплеты Java Card? На каком языке пишутся апплеты Java Card?

Не нужно притягивать за уши и врать,

Пока что врёшь от незнания ("и даже у тебя таких устройств несколько.") и притягиваешь за уши какую-то лабуду тут только ты.

С тебя ещё миллион устройств с прошивками из Эль-76, звиздунишка.

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

Java Card исполняет Java-байткод, на этом нет смысла дальше обсуждать твоё враньё, и именно в этом его отношение к технологии Java. В целом соглашусь, что мой пример был не совсем удачным. Обзываться я тоже умею, вообще-то, но то, что ты без этого не можешь обойтись, не говорит в твою пользу. А зачем тебе нужен миллион устройств с прошивками на Эль-76, какой вывод ты сделаешь из их предъявления? Равно как и какой вывод надлежит сделать из засилья софта на Си? В общем-то, даже если я не знаю примеров устройств без Си с миллиардными тиражами, я верю, что они есть, просто их тиражи меньше. Мне не нужно их искать, потому что в моей картине мира ничего от их наличия или отсутствия на данный момент не изменится. Достаточно того, что они были раньше, и даже того, что они могут существовать в теории. А зачем это нужно тебе, кроме повода кого-нибудь обозвать или поддеть?

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

Java Card исполняет Java-байткод,

Нет, двоичное представление java-аплетов для smartcard так же не имеет никакого отношения к java, как и среда исполнения с языком разработки.

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

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

А зачем тебе нужен миллион устройств с прошивками на Эль-76,

Что бы ты доказал свой тезис:

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

который пока висит в воздухе без единого подтверждения.

Обзываться я тоже умею, вообще-то

Я уже заметил:

Не нужно притягивать за уши и врать, … Раз ты начал вратЬ, видимо, почувствовал неуверенность в том, что ты пытаешься защищать.

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

Что бы ты доказал свой тезис:

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

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

Нет, двоичное представление java-аплетов для smartcard так же не имеет никакого отношения к java, как и среда исполнения с языком разработки.

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

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

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

Я вернулся и не нашёл ни одного устройства.

Зачем тебе именно миллион?

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

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

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

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

Во всяком случае, из более-менее промышленного, я могу назвать вот это: http://www.radiar.ch/page_en.html, полностью написано на Обероне, хотя, поскольку оно на ПЛИС, то без Си и здесь не обошлось. В том числе я видел и ковырял код A2, там нет ни строчки на Си, и тем не менее это вполне полноценная прошивка. В тулчейне тоже нет ни строчки на Си. Насколько эта штука промышленно применяется - я не знаю, но тем не менее она заявляется как промышленная и выпущена в этом тысячелетии.

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

Если не ограничиваться этим тысячелетием, то на Эль-76 было что-то для советского ПРО Москвы и наверняка для каких-то других не менее серьёзных проектов, о которых нам знать не положено.

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

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

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

Ну вот, к пример, Дипсик заявляет, что в 2014 году была осуществлена посадка на комету Чурюмова — Герасименко, использовался процессор RTX2010, который на железе исполняет Форт.

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

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

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

О... Школота открыла препроцессор... Странно что так поздно, обычно об этом рассказывают на уровне #include <stdio.h>

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

да это какой-то старпер решил тролльнуть

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

то это проблема этой промышленности, а вовсе не свидетельство какой-то богоизбранности Си

другой планеты с промышленностью не на С у нас для вас нету

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

У меня лично один преподаватель информатики ушел с того что не смог объяснить using namespace ; школьникам. С тех пор у меня с C++ и не ладится…

Это кажется был 8 или 9 класс.

Он пыталсО обьсяснить ХэлоуВорлд.

К счастью потом был преп аспирант МФТИ.

И в самом МФТИ попались хорошие преподаватели.

С чего я хорошо знаю С99. И чуть хуже современный C++

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

Я ещё в первом классе хотел отсюда улететь, даже не знал про Си. Но, видимо, предчувствовал.

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

Как ты взведёшь 32 битовый флаг в знаковом целом?

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

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

А в чём проблема? Пишешь -0x80000001, компилятор это спокойно переваривает.

а для какого типа ты такое можешь сделать? поскольку для int32_t это является UB.

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

(И позволяет определить нижнюю границу int без хитрых трюков вроде -0x7fffffff - 1)

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

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

если нужен тип — ты используешь:

const TYPE constantN = VALUE;


или начиная с c23:
constexpr TYPE constantN = VALUE;

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