Непоследовательные строгие правила псевдонимов

У меня есть следующая программа, в которой я инициализирую два буфера, казалось бы, быстрым способом, приводя 8-битный буфер к 32- и 64-битным значениям.

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

typedef struct {
    uint32_t a[2];
    uint16_t b[4];
} ctx_t;

void inita(ctx_t *ctx, uint8_t *aptr)
{
    *(uint64_t *) (ctx->a) = *(uint64_t *) (aptr);
}

void initb(ctx_t *ctx, uint8_t *bptr)
{
    for (int i = 0; i < 2; i++) {
        *((uint32_t *) (ctx->b) + i) = *(uint32_t *) (bptr);
    }
}

int main()
{
    uint8_t a[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef};
    uint8_t b[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef};

    ctx_t ctx;
    inita(&ctx, a);
    initb(&ctx, b);

    printf("a: ");
    for (int i = 0; i < 8; i++) {
        printf("%02x", a[i]);
    }

    printf("\nb: ");
    for (int i = 0; i < 8; i++) {
        printf("%02x", b[i]);
    }
    printf("\n");
}

При компиляции с использованием GCC версии 8.2.1 я получаю следующее предупреждающее сообщение:

> gcc -std=c99 -Wall -Wextra -Wshadow -fsanitize=address,undefined -O2 main.c
main.c: In function ‘inita’:
main.c:11:3: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
  *(uint64_t *) (ctx->a) = *(uint64_t *) (aptr);
   ^~~~~~~~~~~~~~~~~~~~~

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

Программа работает и дает ожидаемый результат:

a: 0123456789abcdef
b: 0123456789abcdef

Если я исправлю предупреждение, выполнив:

void inita(ctx_t *ctx, uint8_t *aptr)
{
    *(uint32_t *) (ctx->a) = *(uint32_t *) (aptr);
    *(uint32_t *) (ctx->a + 1) = *(uint32_t *) (aptr + 4);
}

Затем я получаю тот же результат, что и раньше, но без предупреждений.

У меня все еще есть проблемы с псевдонимами в моем коде (из-за initb) или это безопасно?


person Noxet    schedule 19.03.2019    source источник
comment
Я получаю то же самое на gcc 5.4.0.   -  person dbush    schedule 19.03.2019
comment
Попробуйте развернуть цикл в initb(), вы увидите те же предупреждения, что и для inita(). Так что это просто gcc не распознает проблему, когда задействована переменная цикла   -  person Ctx    schedule 19.03.2019


Ответы (2)


У меня все еще есть проблемы с псевдонимами в моем коде (из-за initb) или это безопасно?

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

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

Вместо этого используйте memcpy:

void inita(ctx_t *ctx, uint8_t *aptr)
{
    memcpy(ctx->a, aptr, sizeof uint64_t);
}
person user694733    schedule 19.03.2019
comment
Если я изменю функцию initb так же, как я изменил inita, где я привел свой буфер к правильному размеру, это все еще проблема? - person Noxet; 19.03.2019
comment
@Noxet Ваша измененная функция исправляет доступ к ctx->a, но не к aptr. Вы разыменовываете uint8_t как uint32_t. Это все еще недействительно. - person user694733; 19.03.2019
comment
Конечно.. Тогда немного странно, что компилятор жалуется только на половину проблемы. О, теперь я знаю, как это исправить - person Noxet; 19.03.2019

В языках, изобретенных Деннисом Ритчи, названных C и описанных в обоих изданиях Язык программирования C (но расширенных для включения 64-битных типов), единственная проблема с приведенным выше кодом заключается в том, что нет никакой гарантии. что автоматические объекты char[] будут выровнены способом, подходящим для доступа через 64-битный указатель. Это не будет проблемой на платформах x86 или x64, но будет проблемой на платформах, основанных на других архитектурах, таких как ARM Cortex-M0.

Диалекты, обрабатываемые такими реализациями, как MSVC или icc, которые поддерживают дух C, описанный Комитетом по стандартам, включая принцип «Не мешайте программисту делать то, что нужно», и прилагают усилия для поддержки низкоуровневого программирования. , будет распознавать, что конструкция формы *(T*)pointerOfTypeU может обращаться к объекту типа U. Хотя стандарт не требует такой поддержки, это, вероятно, потому, что авторы ожидали, что реализации предпримут хотя бы некоторые усилия для распознавания ситуаций, когда указатель один тип был сформирован из lvalue другого типа, и думал, что такое распознавание можно оставить как проблему качества реализации. Обратите внимание, что в Стандарте нет ничего, что требовало бы от компилятора:

struct foo {int x[10]; };

void test(struct foo *p, int i)
{
  struct foo temp;
  temp = *p;
  temp.x[i] = 1;
  *p = temp;
}

распознать, что на объект типа struct foo могут повлиять действия по формированию int* с адресом foo.x+i, разыменованию его и последующей записи в результирующее lvalue типа int. Вместо этого авторы Стандарта полагаются на качественные реализации, чтобы попытаться распознать очевидные случаи, когда указатель или lvalue одного типа выводятся из указателя или lvalue другого.

Компилятор icc, учитывая:

int test1(int *p1, float *q1)
{
    *p1 = 1;
    *q1 = 2.0f;
    return *p1;
}
int test2(int *p2, int *q2)
{
    *p2 = 1;
    *(float*)q2 = 2.0f;
    return *p2;
}
int test3(int *p3, float volatile *q3)
{
    *p3 = 1;
    *q3 = 2.0f;
    return *p3;
}

будет предполагать, что, поскольку p1 и p2 являются разными типами, и не имеют различимой взаимосвязи, они не будут псевдонимами, но распознают, что, поскольку p2 и q2 имеют один и тот же тип, они могут идентифицировать один и тот же объект. Кроме того, он распознает, что lvalue *(float*)q2 совершенно очевидно основан на q2. Следовательно, он распознает, что доступ к *(float*)q2 может быть доступом к *p2. Кроме того, icc будет рассматривать volatile как индикацию «не предполагайте, что вы понимаете все, что здесь происходит», и, таким образом, допускает возможность того, что доступ через q3 может повлиять на другие объекты странным образом.

Некоторые компиляторы, такие как clang и gcc, если они не вынуждены через -O0 или -fno-strict-aliasing вести себя так, как это подходит для низкоуровневого программирования, интерпретируют стандарт как предлог для игнорирования очевидных взаимосвязей между lvalue разных типов, за исключением случаев, когда это может нарушить языковые конструкции. так плохо, что они практически бесполезны. Хотя они понимают, что доступ к someUnion.array[i] является доступом к someUnion, они точно так же распознают *(someUnion.array+i), даже несмотря на то, что определение someUnion.array[i] — это *(someUnion.array+i). Учитывая, что стандарт рассматривает поддержку почти всего, что связано с хранилищем смешанного типа, как проблему «качества реализации», все, что можно сказать, это то, что компиляторы, подходящие для разных целей, поддерживают разные комбинации конструкций.

person supercat    schedule 19.03.2019