LINUX.ORG.RU

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

 , , ,


1

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)
Ответ на: комментарий от monk

Linux/x86-64 assembler, в котором действительно указатели являются 64-битными числами

И даже на этой платформе есть понятие «канонического» адреса. А соответствующих «канонических» чисел нет.

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

Что именно запустить?

int main(void) {
  (unsigned*)0x40021014U;
  return 0;
}

Вот такая программа никаких ошибок не сделает нигде думаю.

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

Получишь «расстояние» между x и y.

Читай описание языка. Получишь UB. То есть, если такая операция встречается на какой-то ветке кода, компилятор вправе считать, что эта ветка никогда не выполняется.

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

А соответствующих «канонических» чисел нет.

В смысле? Условие «биты от 63 до наиболее значимого реализованного бита установлены либо во все единицы, либо во все нули» для числа применимо.

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

Где я такое писал?

Так в этом сообщении я отвечал r–r–r–.

Линейность в данном случае означает что виртуальные адреса монотонно возрастают от 0 до 2^32. И любой указатель может быть представлен как (uint32_t).

Это верно для ассемблера x86 на Linux. Кстати, для 64-битного режима уже не верно. Для Си это неверно никогда, так как в стандарте линейной памяти нет.

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

Программа на Си является переносимой, если соответствует стандарту Си.

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

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

Спасибо, интересно.
«Многа букав» - даже просмотр по диагонали отнимает немало времени. Буду вникать постепенно.

Пока картина такая: сложность кода это понятная мне метрика, то что ее можно считать - это очень полезно. «Уровневость» языка - это схоластика. Есть универсальная (и понятная) метрика - соотношение «удобство - польза». И понятно почему тот же brainfuck на этой шкале находится где-то глубоко внизу. Потому что он мозг взрывает, это да, но непонятно ради чего. С другой стороны, любой язык, который прост для освоения, но приносящий много практической пользы, взмывает в небеса. (Питон тот же, к примеру). И понятно почему, и это согласуется с внутренними, чисто интуитивными представлениями.

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

Программа на Си является переносимой, если соответствует стандарту Си.

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

int main(void) {
  (unsigned*)0x4001210;
  return 0;
}
Хотя есть маленькое исключение: через return из main-а можно попытаться передать наружу одно 7-битное число. Но это максимум, чего может передать наружу переносимая программа.

Под MSDOS разыменование NULL компиляторами Си обычно разрешалось

Разрешалось не только под MS-DOS, разрешается и сейчас везде или почти везде. И даже -O3 этому не помеха: https://godbolt.org/z/rPc9xnG7v

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

это выражение вычислимо и равно 1 или -1.

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

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

Так и есть. Посчитай разницу между двумя соседними элементами в одном массиве.

Не понял утверждение.

Потому что ты смешал в одну кучу гарантии линейности и допустимое множество операций. Не надо так делать.

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

Для Си это неверно никогда, так как в стандарте линейной памяти нет.

4.2

6.5.9 Relational operators

6
When two pointers are compared, the result depends on the relative locations in the address space
of the objects pointed to, if any. If two pointers to object types are both null pointer values, both
point to the same object, or both point one past the last element of the same array object, they
compare equal. If the objects pointed to are members of the same aggregate object, pointers to
structure members declared later compare greater than pointers to members declared earlier in the
structure, and pointers to array elements with larger subscript values compare greater than pointers
to elements of the same array with lower subscript values. All pointers to members of the same
union object compare equal. If the expression P points to an element of an array object and the
expression Q points to the last element of the same array object, the pointer expression Q+1 compares
greater than P. In all other cases, the behavior is undefined.

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

Программа на Си является переносимой, если соответствует стандарту Си.

Это миф. Если в программе используется, например, printf - она уже не переносима, её нельзя запустить в no-libc-окружениях.

  1. Переносимость программы определяется стандартом на язык, а не твоим богатым внутренним миром.
  2. Под "переносимостью" программы на си понимается возможность успешно скомпилировать исходник, не более.
r--r--r--
()
Ответ на: комментарий от wirewalk

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

Плохой пример. Питон динамический язык и для больших проектов не годится. Ява и C# намного лучше.

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

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

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

Переносимость программы определяется возможностью её работы на разных платформах,

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

Вот давай быстренько протестируем твоё понимание "переносимости" smoke test’ом:

Переносима ли программа, если она работает на двух любых разных платформах?

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

Где здесь про линейную память? Стандарт описывает правила сравнения указателей и их инкремента, но не делает допущения про линейную структуру адресов.

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

When two pointers are compared, the result depends on the relative locations

Где здесь про линейную память?

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

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

«Линейное пространство», «линейная алгебра», вспоминаем шкалку и марьиванну.

В линейном пространстве мариванны сумма и разность любых элементов линейного пространства — тоже вполне определенные элементы этого же линейного пространства.

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

Комитеты те ещё казуисты и обобщатели

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

Стандарты те ещё зверушки

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

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

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

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

Вы же сами пишете:

In all other cases, the behavior is undefined.

Для линейной памяти была бы арифметическая разность адресов In all other cases.

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

Просто человек не видел ничего кроме.

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

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

Для случая массива атомов - да линейно - ты частный случай считаешь универсальным

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

Что-то противоречащие параграфы. То «реализация си не даёт никаких гарантий в отношении адреса отдельного символа», то «объекты в памяти расположены линейно".

Посчитай разницу между двумя соседними элементами в одном массиве.

То один объект, а не объекты.

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

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

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

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

Если на множестве определено расстояние, то не будет ли оно метрическим пространством?

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

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

Это неверно для целого ряда платформ, не надо полагать, что то как работает для прикладных программ на совремённых x86_32 в 4-м кольце и на x86_64 так и везде.

Немного погуглил, найдя примеры.

1) В старом, добром x86 в real-mode указатель - это два числа: сегмент и смещение. Например, адрес какой-то структуры может быть в регистрах DS:SI, допустим DS = 0x1000, а SI = 0x1234, физический адрес равен 0x1000 x 16 + 0x1234 = 0x11234, но число 0x11234 нигде явно не фигурирует, да и как видно, может быть получено несколькими способами. В protected mode (начиная с 286) вместо сегмента - селектор, являющийся индексом в таблице дескрипторов. Но логика точно такая же.

2) Архитектура CHERI (Capability Hardware Enhanced RISC Instructions): Указатели тут - это т.н. capabilities. Объединяют в себе адрес, права доступа и аппаратные границы буфера. По размеру в два раза больше обычного адреса (например, 128 бит вместо 64). При попытке модифицировать такой указатель как простое число аппаратно сбрасывается тег валидности, и обратиться по нему становится невозможно. Есть Risc-V реализации. У российского Эльбрус (E2K) тоже наверчено всякого с указателями.

3) Память вообще может быть нелинейно организована даже на аппаратном уровне: например, в микроконтроллерах с гарвадской архитектурой, в мейнфреймах IBM (z/Architecure) память организована логическими сегментами (не путать с x86 сегментами) с изоляцией сегментов. На ARM с тегированной памятью. ARM MT она тоже фактически нелинейная.

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

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

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

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

потому что это твоё особое уличное понимание «переносимости» понятно только тебе

Нет, это как раз единственное вменяемое понимание переносимости.

Переносима ли программа, если она работает на двух любых разных платформах?

Она переносима между этими двумя разными платформами, но не переносима в абсолютном смысле. Чем больше платформ поддерживается - тем более переносима.

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

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

Ср. буквальное определение линейного пространства марьиванны

https://mathprofi.com/uploads/files/2179_f_41_lekcii-po-analiticheskoi-geometrii.pdf

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

Ну так, разыменование NULL-а так никто и не запретил.

А твой пример показывает артефакты оптимизатора при проверках ==NULL - это другая тема. Впрочем, при -O0 и их нет.

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

её нельзя запустить в no-libc-окружениях

Всё верно. В таком окружении можно запустить программу на Си с конкретными ограничениями и это явно указано в стандарте. Можно считать freestanding C его ограниченным диалектом. Который, в свою очередь, даёт гарантии переносимости между всеми freestanding окружениями.

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

А твой пример показывает артефакты оптимизатора при проверках ==NULL - это другая тема. Впрочем, при -O0 и их нет.

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

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

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

Можно вспомнить про определение линейного пространства. Как минимум в стандарте Си не соответствует этому определению то, что а) отсутствует нейтральный нулевой элемент, т.к. Pointer + NULL = UB, а не Pointer, 2) Отсутствует противоположный элемент, такой чтобы Pointer + (-Pointer) = NULL и 3) вообще не определено умножение указателя на число.

Так что указатели в Си по стандарту не образуют линейного пространства. Даже алгебраического кольца не образуют.

Про приращение на натуральную единицу есть только, что «If the expression P points to an element of an array object and the expression Q points to the last element of the same array object, the pointer expression Q+1 compares greater than P. » по русски «Если выражение P указывает на элемент объекта массива, а выражение Q указывает на последний элемент того же объекта массива, то выражение указателя Q+1 сравнивается как «больше», чем P.» Но это не про инкремент, это обозначает, что указатели можно упорядочить по возрастанию и не более того.

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

Разумеется. Компилятор можно вообще сделать без UB как сделал автор Fil-C.

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

Вообще польза таких споров в интернете в углублении понимания предмета.

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

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

Любой Си - какой-то диалект. Ты (и другие тоже) совершенно безосновательно придают документам ISO Cxx какой-то особый статус среди диалектов языка. Это просто ещё пачка диалектов и не более того. Никакого «просто Си» в строгом смысле не существует, Си это собирательное название для группы похожих языков, с часто существующей ограниченной взаимной совместимостью.

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

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

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

а) отсутствует нейтральный нулевой элемент, т.к. Pointer + NULL = UB, а не Pointer, 2) Отсутствует противоположный элемент, такой чтобы Pointer + (-Pointer) = NULL и 3) вообще не определено умножение указателя на число.

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

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

в стандарте Си не соответствует этому определению то, что а) отсутствует нейтральный нулевой элемент, т.к. Pointer + NULL = UB, а не Pointer,

Да и вообще:

error: invalid operands to binary + (have ‘char *’ and ‘char *’)
  b + c;
    ^

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

Компилятор контролирует объект. «Ведет» его. В рамках одного объекта сработает. При выходе за рамки – уже не обязано.

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

Кстати, по-моему, одни из методов проверки кода – выделить тебе объект по границам страницы и посмотреть, выйдешь ты за него или нет. Выйдешь – ну все, хана котенку.

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

Любой Си - какой-то диалект.

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

Если диалект «ISO Cxx hosted» или «ISO Cxx freestanding», то будет переносимо между всеми компиляторами, которые реализуют соответствующий стандарт.

Никакого «просто Си» в строгом смысле не существует

Так обычно называют «C89 hosted».

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

Давал расскажи как компилятор контролирует объект возвращенный вызовом malloc()? И объект ли это вообще? Правильно ли я думаю, что libc на всех архитектурах должна работать так как написано в man? И прикладной программист в линукс вообще не должен парится как у него устроена память и линейна ли она?

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