Подтвердить что ты не робот

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

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

код:

main.c

#include <stdint.h>
#include <stdio.h>

int a __attribute__((section("test")));
extern int b;

void check(int cond) { puts(cond ? "TRUE" : "FALSE"); }

int main() {
    int * p = &a + 1;
    check(
        (p == &b)
        ==
        ((uintptr_t)p == (uintptr_t)&b)
    );
    check(p == &b);
    check((uintptr_t)p == (uintptr_t)&b);
    return 0;
}

b.c

int b __attribute__((section("test")));

Если я скомпилирую его с -O0, он печатает

TRUE
TRUE
TRUE

Но с -O1

FALSE
FALSE
TRUE

Итак, p и &b на самом деле одно и то же значение, но компилятор оптимизировал их сравнение, полагая, что они никогда не могут быть равными.

Я не могу понять, какая оптимизация сделала это.

Это не похоже на строгий псевдоним, потому что указатели имеют один тип, а параметр -fstrict-aliasing не делает этого.

Является ли это документированным поведением? Или это ошибка?

4b9b3361

Ответ 1

В коде есть три аспекта, которые приводят к общим проблемам:

  • Преобразование указателя в целое число определяется реализацией. Существует не гарантия преобразования двух указателей, чтобы все биты были идентичными.

  • uintptr_t гарантированно конвертировать из указателя в тот же тип, а затем обратно без изменений (то есть сравнить его с исходным указателем). Но ничего больше. Целочисленные значения не гарантируют сопоставление равных. Например. могут быть неиспользуемые биты с произвольным значением. См. Стандарт 7.20.1.4.

  • И (вкратце) два указателя могут сравнивать только равные, если они указывают на тот же массив или прямо за ним (последняя запись плюс одна) или по крайней мере один - это нулевой указатель. Для любого другого созвездия они сравниваются неравномерно. Подробные сведения см. В стандарте 6.5.9p6.

Наконец, нет никакой гарантии, что переменные помещаются в память с помощью инструментальной цепочки (обычно это компоновщик для статических переменных, компилятор для автоматических переменных). Только массив или struct (т.е. Составные типы) гарантируют упорядочение его элементов.

В вашем примере применяется 6.5.9p7. Он в основном обрабатывает указатель на объект без массива для сравнения, например, с первой записью массива размера 1. Это означает, что не покрывает инкрементированный указатель прошлый объект, например &a + 1. Релевантным является объект, на котором основан указатель. Это объект a для указателя p и b для указателя &b. Остальное можно найти в параграфе 6.

Ни одна из ваших переменных не является массивом (последняя часть параграфа 6), поэтому указатели не должны сравнивать равные, даже для &a + 1 == &b. Последний "TRUE" может возникнуть из gcc, предполагая, что сравнение uintptr_t возвращает true.

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

Ответ 2

p == &b представляет собой сравнение указателей и подчиняется следующим правилам из стандарта C (6.5.9. Операторы равенства, пункт 4):

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

(uintptr_t)p == (uintptr_t)&b является арифметическим сравнением и подчиняется следующим правилам (6.5.9 Операторы равенства, пункт 6):

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

Эти две отрывки требуют от реализации очень разных вещей. И понятно, что спецификация C не предъявляет требования к реализации, чтобы имитировать поведение прежнего вида сравнения в случаях, когда вызывается последний вид, и наоборот. Реализация требуется только для соблюдения этого правила (7.18.1.4. Целочисленные типы, способные удерживать указатели объектов в C99 или 7.20.1.4 на C11):

Тип [uintptr_t] обозначает целочисленный тип без знака с тем свойством, что любой действительный указатель на void может быть преобразован в этот тип, а затем преобразован обратно в указатель на void, и результат будет сравниваться с исходным указателем.

(Приложение: приведенная выше цитата неприменима в этом случае, поскольку преобразование с int* в uintptr_t не включает void* в качестве промежуточного шага. См. Хади отвечает за объяснение и цитату из этого. Тем не менее, рассматриваемое преобразование является реализацией, и для двух сравнений, которые вы пытаетесь, не требуется проявлять такое же поведение, что и основной вынос здесь.)

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

&a + 1 - целое число, добавленное к указателю, которое подчиняется следующим правилам (6.5.6 Аддитивные операторы, пункт 8):

Когда выражение, которое имеет целочисленный тип, добавляется или вычитается из указателя, result имеет тип операнда указателя. Если операнд указателя указывает на элемент объект массива и массив достаточно велик, результат указывает на смещение элемента от исходный элемент такой, что разность индексов результирующего и оригинального элементы массива равны целочисленному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывает соответственно на я + n-й и i-n-й элементы объект массива, если они существуют. Более того, если выражение P указывает на последнее элемент объекта массива, выражение (P) +1 указывает один за последним элементом массив, и если выражение Q указывает один за последним элементом объекта массива, выражение (Q) -1 указывает на последний элемент объекта массива. Если оба указателя операнд и результат указывают на элементы одного и того же объекта массива или один за последним элемент объекта массива, оценка не должна приводить к переполнению; в противном случае поведение undefined. Если результат указывает один за последний элемент объекта массива, он не должен использоваться в качестве операнда унарного * оператора, который оценивается.

Я считаю, что эта выдержка показывает, что сложение (и вычитание) указателя определяется только для указателей внутри одного и того же объекта массива или за последним элементом. И поскольку (1) a не является массивом и (2) a и b не являются членами одного и того же объекта массива, мне кажется, что ваша операция с указателем math вызывает поведение undefined и ваш компилятор использует его, чтобы предположить, что сравнение указателей возвращает false. Опять же, как указано в Hadi answer (и в отличие от того, что мой оригинальный ответ предполагается, что в данный момент), указатели на объекты без массива можно рассматривать как указатели на объекты массива длиной один, и, таким образом, добавление одного к вашему указателю на скаляр имеет право указывать на один конец конца массива.

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

Ответ 3

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

Начнем с &a + 1. В отличие от того, что заявили @Theodoros и @Peter, это выражение определило поведение. Чтобы увидеть это, рассмотрим раздел 6.5.6, пункт 7 "Аддитивные операторы", который гласит:

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

и пункт 8 (в частности, подчеркнутая часть):

Когда выражение, которое имеет целочисленный тип, добавляется или вычитается из указателя результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, а массив достаточно велика, результат указывает на смещение элемента от оригинальный элемент такой, что разность индексов результирующие и исходные элементы массива равны целочисленному выражению. Другими словами, если выражение P указывает на i-й элемент массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывают соответственно на я + n-й и i-n-ых элементов массива, если они существуют. Более того, if выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, и если выражение Q указывает один за последним элементом массива объект, выражение (Q) -1 указывает на последний элемент массива объект. Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива, или один за последним элементом массива объект, оценка не должна приводить к переполнению; в противном случае поведение undefined. Если результат указывает один за последним элементом объекта массива, он не должен использоваться как операнд унарного * оператор, который оценивается.

Выражение (uintptr_t)p == (uintptr_t)&b имеет две части. Преобразование из указателя в uintptr_t НЕ, определенное в разделе 7.20.1.4 (в отличие от того, что сказали @Olaf и @Theodoros):

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

uintptr_t

Важно признать, что это правило применяется только к действительным указателям на void. Однако в этом случае мы имеем действительный указатель на int. Соответствующий пункт можно найти в разделе 6.3.2.3, пункт 1:

Указатель на void может быть преобразован в или из указателя на любой объект тип. Указатель на любой тип объекта может быть преобразован в указатель на пустота и обратно; результат сравнивается с оригиналом указатель.

Это означает, что (uintptr_t)(void*)p разрешено в соответствии с этим пунктом и 7.20.1.4. Но (uintptr_t)p и (uintptr_t)&b управляются разделом 6.3.2.3, пункт 6:

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

Обратите внимание, что uintptr_t является целым типом, как указано в разделе 7.20.1.4, упомянутом выше, и поэтому это правило применяется.

Вторая часть (uintptr_t)p == (uintptr_t)&b сравнивается для равенства. Как обсуждалось ранее, поскольку результат преобразования определяется реализацией, результат равенства также определяется реализацией. Это применяется независимо от того, равны ли сами указатели или нет.

Теперь я обсужу p == &b. Третий пункт в ответе @Olaf неверен, и ответ @Theodoros является неполным относительно этого выражения. Раздел 6.5.9 "Операторы равенства", пункт 7:

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

и пункт 6:

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

В отличие от того, что сказал @Olaf, сравнение указателей с использованием оператора == никогда не приводит к поведению undefined (что может произойти только при использовании реляционных операторов, таких как <= в соответствии с разделом 6.5.8, параграф 5, который я здесь пропущу для краткости). Теперь, поскольку p указывает на следующий int относительно a, он будет равен &b, только если компоновщик разместил b в этом месте в двоичном формате. В противном случае, неравноправны. Таким образом, это зависит от реализации (относительный порядок a и b не указывается стандартом). Поскольку объявления a и b используют расширение языка, а именно __attribute__((section("test"))), относительные местоположения действительно зависят от реализации в J.5 и 3.4.2 (опущены для краткости).

Мы заключаем, что результаты check(p == &b) и check((uintptr_t)p == (uintptr_t)&b) зависят от реализации. Поэтому ответ зависит от того, какую версию компилятора вы используете. Я использую gcc 4.8 и компилируя параметры по умолчанию, кроме уровня оптимизации, вывод, который я получаю в обоих случаях -O0 и -O1, - это все TRUE.

Ответ 4

Согласно C11 6.5.9/6 и C11 6.5.9/7, тест p == &b должен давать 1, если a и b смежны в адресном пространстве.

В вашем примере показано, что GCC, похоже, не выполняет это требование Стандарта.


Обновление 26/Апр/2016: В моем первоначальном ответе содержались предложения по изменению кода для удаления других потенциальных источников UB и изоляции этого одного условия.

Однако с тех пор выяснилось, что проблемы, поднятые этим потоком, находятся в стадии рассмотрения - N2012.

Одна из их рекомендаций заключается в том, что p == &b должен быть неуказан, и они признают, что GCC фактически не выполняет требования ISO C11.

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

Ответ 5

Перечитав вашу программу, я вижу, что вы (понятно) озадачены тем, что в оптимизированной версии

p == &b

false, а

(uintptr_t)p == (uintptr_t)&b;

истинно. Последняя строка указывает, что числовые значения действительно идентичны; как может p == &b быть ложным?

Я должен признать, что понятия не имею. Я убежден, что это ошибка gcc.

После обсуждения с M.M я думаю, что могу сделать следующий случай, если преобразование в uintptr_t проходит через промежуточный указатель void (вы должны включить это в свою программу и посмотреть, не изменит ли он что-либо):

Поскольку оба шага в цепочке преобразования int*void*uintptr_t гарантированно обратимы, неравные int указатели могут логически не приводить к равным значениям uintptr_t. 1 (Те, которые равны значениям uintptr_t, должны были бы вернуться обратно к равным int указателям, изменив хотя бы один из них и тем самым нарушив правило преобразования, сохраняющее значение.) В коде (я не нацеливаю для равенства здесь, просто демонстрируя конверсии и сравнения):

int a,b, *ap=&a, *bp = &b;

assert(ap != bp);

void *avp = ap, *bvp bp;

uintptr_t ua = (uintptr_t)avp, ub = (uintptr_t)bvp;

// Now the following holds:
// if ap != bp then *necessarily* ua != ub. 
// This is violated by the OP case (sans the void* step).

assert((int *)(void *)ua == (int*)(void*)ub);

1 Это предполагает, что uintptr_t не несет скрытую информацию в виде битов заполнения, которые не оцениваются в арифметическом сравнении, но, возможно, в преобразовании типа. Можно проверить это через CHAR_BIT, UINTPTR_MAX, sizeof (uintptr_t) и немного бит возиться. &mdash?
По той же причине можно предположить, что два значения uintptr_t сравниваются друг с другом, но преобразуются обратно к одному и тому же указателю (а именно, если в uintptr_t есть биты, которые не используются для хранения значения указателя, а конверсия не равна нулю). Но это противоположно проблеме ОП.