Законно ли получить доступ к элементам структуры через смещенные указатели от других элементов структуры?

В этих двух примерах доступ к элементам структуры путем смещения указателей от других элементов приводит к неопределенному / неуказанному / определяемому реализацией поведению?

struct {
  int a;
  int b;
} foo1 = {0, 0};

(&foo1.a)[1] = 1;
printf("%d", foo1.b);


struct {
  int arr[1];
  int b;
} foo2 = {{0}, 0};

foo2.arr[1] = 1;
printf("%d", foo2.b);

Пункт 14 § 6.7.2.1, по-видимому, указывает, что это должно быть определено реализацией:

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

и позже продолжает:

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

Однако код, подобный следующему, выглядит довольно распространенным:

union {
  int arr[2];
  struct {
    int a;
    int b;
  };
} foo3 = {{0, 0}};

foo3.arr[1] = 1;
printf("%d", foo3.b);

(&foo3.a)[1] = 2; // appears to be illegal despite foo3.arr == &foo3.a
printf("%d", foo3.b);

Стандарт, кажется, гарантирует, что foo3.arr такой же как &foo3.a и не имеет смысла, что ссылка на него одним способом является законной, а другой нет, но в равной степени не имеет смысла, что добавление внешнего объединения с массивом должно внезапно привести к (&foo3.a)[1] законны.

Мои рассуждения для размышления о первых примерах также должны быть законными:

  1. foo3.arr гарантированно будет таким же, как &foo.a
  2. foo3.arr + 1 а также &foo3.b указать на ту же область памяти
  3. &foo3.a + 1 а также &foo3.b поэтому должен указывать на одну и ту же ячейку памяти (от 1 и 2)
  4. макеты структуры должны быть согласованными, поэтому &foo1.a а также &foo1.b должен быть выложен точно так же, как &foo3.a а также &foo3.b
  5. &foo1.a + 1 а также &foo1.b поэтому должен указывать на одну и ту же ячейку памяти (из 3 и 4)

Я сталкивался с некоторыми внешними источниками, которые предполагают, что оба foo3.arr[1] а также (&foo3.a)[1] примеры являются незаконными, однако я не смог найти в стандарте конкретного утверждения, которое бы сделало это так. Даже если они оба были недопустимыми, можно также создать один и тот же сценарий с указателями гибких массивов, которые, насколько я могу судить, имеют стандартное поведение.

union {
  struct {
    int x;
    int arr[];
  };
  struct {
    int y;
    int a;
    int b;
  };
} foo4;

Исходное приложение рассматривает вопрос о том, является ли переполнение буфера из одного поля структуры в другое строго определенным стандартом:

struct {
  char buffer[8];
  char overflow[8];
} buf;
strcpy(buf.buffer, "Hello world!");
println(buf.overflow);

Я ожидал бы, что это выведет "rld!" почти на любом реальном компиляторе, но гарантируется ли такое поведение стандартом, или это неопределенное или определяемое реализацией поведение?

1 ответ

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

Этот ответ отражает мою точку зрения, а не навязывание стандарта.


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

Правило C11 6.5.6/8 . Это говорит о том, что индексирование по указателю должно оставаться в пределах "объекта массива" (или одного после конца). Это не говорит, какой объект массива, но в целом согласны, что в случае int *p = &foo.a; тогда "объект массива" foo.a, а не какой-либо более крупный объект которого foo.a это подобъект

Соответствующие ссылки: один, два.


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


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

Хотя мы знаем, что &foo.a + 1 а также &foo.b один и тот же адрес памяти, это действительно для доступа к int через второй и не действительный доступ к int через первый.

Общепринято, что вы можете получить доступ к int, вычислив его адрес другими способами, которые не нарушают правило 6.5.6 / 8, например:

((int *)((char *)&foo + offsetof(foo, b))[0]

или же

((int *)((uintptr_t)&foo.a + sizeof(int)))[0]

Соответствующие ссылки: один, два


Вообще не согласовано ((int *)&foo)[1] является действительным. Некоторые говорят, что он в основном такой же, как ваш первый код, поскольку стандарт гласит "указатель на объект, соответствующим образом преобразованный, указывает на первый объект элемента". Другие говорят, что это в основном так же, как мой (char *) пример выше, потому что это следует из спецификации приведения указателя. Некоторые даже утверждают, что это строгое нарушение псевдонимов, потому что это псевдоним структуры как массива.

Возможно, актуальным является N2090 - Предложение провайдера указателя. Это напрямую не решает проблему и не предлагает отмену 6.5.6 / 8.

Согласно проекту C11 N1570 6.5p7, попытка получить доступ к сохраненному значению объекта структуры или объединения, используя что-либо, кроме lvalue типа символа, типа структуры или объединения или содержащего типа структуры или объединения, вызывает UB, даже если поведение в противном случае был бы полностью описан другими частями стандарта. Этот раздел не содержит положения, позволяющего использовать lvalue не символьного типа члена (или любого не символьного числового типа, в этом отношении) для доступа к хранимому значению структуры или объединения.

Однако, согласно опубликованному документу Rationale, авторы Стандарта признали, что разные реализации предлагали разные поведенческие гарантии в тех случаях, когда Стандарт не предъявлял никаких требований, и рассматривали такие "популярные расширения" как полезную и полезную вещь. Они посчитали, что на вопросы о том, когда и как следует поддерживать такие расширения, лучше ответит рынок, чем Комитет. Хотя может показаться странным, что стандарт позволит тупому компилятору игнорировать возможность someStruct.array[i] может повлиять на сохраненное значение someStructавторы Стандарта признали, что любой компилятор, авторы которого не являются намеренно тупыми, будет поддерживать такую ​​конструкцию независимо от того, обязывает Стандарт или нет, и что любая попытка предписать какое-либо полезное поведение компиляторов, созданных тупо, будет бесполезной.

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

PS - компиляторы, настроенные на совместимость с MSVC-стилем volatile семантика будет интерпретировать этот классификатор как указание на то, что доступ к указателю может иметь побочные эффекты, которые взаимодействуют с объектами, адрес которых был взят и которые не охраняются restrictесть ли какая-либо другая причина ожидать такой возможности. Использование такого классификатора при доступе к хранилищу "необычными" способами может сделать читателей более очевидными, что код делает что-то "странное" в то же время, поскольку это обеспечит совместимость с любым компилятором, который использует такую ​​семантику, даже если такой компилятор иначе не распознал бы этот шаблон доступа. К сожалению, некоторые авторы компиляторов отказываются поддерживать такую ​​семантику на уровне, отличном от уровня оптимизации 0, за исключением программ, которые требуют использования нестандартного синтаксиса.

Другие вопросы по тегам