Почему побитовый или не приводит к константному выражению, но сложение делает - PullRequest
1 голос
/ 22 октября 2019

В одном из моих файлов C я объявляю массив foo. Затем я присваиваю адрес этой переменной целочисленному типу и хочу установить битовую маску с 3, чтобы установить младшие два бита. Тем не менее, битовая маска не удается во время компиляции, но добавление +3, кажется, работает. Почему?

uint64_t foo[1];
uint64_t bar = (uint64_t)foo | 3;

Это не с:

main.c:6:16: error: initializer element is not constant
 uint64_t bar = (uint64_t)foo | 3;

Но это работает:

uint64_t foo[1];
uint64_t bar = (uint64_t)foo + 3;

Насколько я понимаю, местоположение foo неизвестно ввремя компиляции, потому что оно глобально (будет в разделе .data или .bss). Тем не менее, запись помещается в раздел перемещения, чтобы компоновщик мог исправить адрес во время компоновки.

Как он обрабатывает побитовое добавление или добавление? Почему один работает, а другой нет?

Ответы [ 3 ]

2 голосов
/ 22 октября 2019

Начальные значения для статических объектов должны быть константными выражениями или строковыми литералами. (C 2018 6.7.9 3: «Все выражения в инициализаторе для объекта, который имеет статическую или потоковую продолжительность хранения, должны быть константными выражениями или строковыми литералами».)

6.6 7 определяет формы константных выражений для инициализаторов:

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

- выражение арифметической константы,

- константа нулевого указателя,

- константа адресаили

- адресная константа для полного типа объекта плюс или минус целочисленное константное выражение.

Рассмотрим uint64_t bar = (uint64_t)foo + 3;. foo номинально является статическим массивом, объявленным ранее, который автоматически преобразуется в указатель на его первый элемент. Это квалифицируется как адресная константа (6.6 9: «адресная константа - это ... указатель на lvalue, обозначающий объект статической длительности хранения, ... Однако, он приводится к uint64_t, который больше не квалифицируетсякак константа адреса, константа адреса плюс или минус выражение константы или константа нулевого указателя.

Является ли это выражение арифметической константой? 6,6 8 исключает ее:

… Castоператоры в выражении арифметической константы должны преобразовывать только арифметические типы в арифметические типы,…

Таким образом, (uint64_t)foo + 3 не квалифицируется как любая форма константного выражения, требуемая стандартом C. Однако 6.6 10 говорит:

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

Таким образом, реализация C может принять (uint64_t) foo + 3 или (uint64_t) foo | 3 какконстантное выражение. Тогда возникает вопрос: почему ваша реализация на C принимает первое, а не второе?

Общая особенность линкеров и objФормат модулей ect заключается в том, что объектный модуль может записывать заполнители для определенных выражений, а компоновщики могут оценивать эти выражения и заменять заполнители вычисленными значениями. Основная цель этой функции - позволить коду в программе ссылаться на мест в данных или другом коде, чьи местоположения не полностью известны во время компиляции, но это будет решено (по крайней мере, относительно некоторой базовой ссылкиточка) во время связывания.

Места в данных или коде измеряются относительно символов (имен), определенных в объектных модулях (или относительно начала секций или сегментов). Таким образом, место может быть фактически описано как «34 байта после начала обычной подпрограммы» или «8 байтов после начала объектной базы». Таким образом, объектный модуль поддерживает заполнители, которые состоят из смещения и имени символа. После того, как компоновщик назначает адреса символам, он просматривает каждый заполнитель, добавляет смещение к назначенному адресу и заменяет заполнитель вычисленным результатом.

Похоже, ваш компилятор, несмотря на приведение uint64_t,способен распознавать, что (uint64_t) foo по-прежнему является адресом foo, и, следовательно, (uint64_t) foo + 3 может быть реализовано путем регулярного использования одного из этих заполнителей.

В отличие от этого, побитовый оператор ИЛИ неподдерживается для использования в этих заполнителях, и поэтому компилятор не может реализовать (uint64_t) foo | 3. Он не может оценить само выражение (поскольку он не знает конечный адрес для foo), и он не может написать заполнитель для выражения. Поэтому он не принимает это как константное выражение.

1 голос
/ 22 октября 2019

Когда вы говорите

sometype *p = f(x);

, где p - глобальная переменная (или переменная со статической продолжительностью), а где f(x) - не фактический вызов функции, а некоторая последовательность времени компиляции. Операции, связанные с адресом другого символа x, который не будет известен до времени ссылки, компилятор, очевидно, не может сразу вычислить начальное значение. Фактически он испускает директиву на ассемблере, которая заставляет ассемблер создавать запись перемещения, которая заставляет компоновщик вычислять f(x), когда известно окончательное местоположение символа x.

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

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

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

Ваш вопрос имеет дополнительное осложнение, заключающееся в том, что вы на самом деле инициализируете не переменную-указатель, а целое число. В этом случае вы получаете, по сути, худшее из обоих миров: вам либо вообще не разрешено это делать, либо, если компилятор позволяет вам, инициализатор справа (поскольку он включает в себя адрес / указатель),ограничено видами арифметики, которые вы можете выполнять при построении констант указателей, как описано выше. Вы не можете выполнять произвольную арифметику, с которой вам бы удавалось избежать (возможно, путаницы) в целочисленном выражении во время выполнения.

0 голосов
/ 22 октября 2019

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

Однако есть пункт C11 6.6 / 10:

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

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

...