Непоследовательные строгие правила наложения имен - PullRequest
1 голос
/ 19 марта 2019

У меня есть следующая программа, в которой я на первый взгляд быстро инициализирую два буфера, приводя 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) или это безопасно делать?

Ответы [ 2 ]

2 голосов
/ 19 марта 2019

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

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

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

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

void inita(ctx_t *ctx, uint8_t *aptr)
{
    memcpy(ctx->a, aptr, sizeof uint64_t);
}
1 голос
/ 19 марта 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 не нужно вести себя так, как нужно для низкоуровневого программирования, интерпретировать стандарт как оправдание, чтобы игнорировать очевидные связи между l-значениями разных типов, за исключением случаев, когдаэто настолько сильно сломало бы языковые конструкции, что сделало бы их практически бесполезными.Хотя они случайно признают, что доступ к someUnion.array[i] является доступом к someUnion, они распознают *(someUnion.array+i) аналогично, даже если определение из someUnion.array[i] равно *(someUnion.array+i).Учитывая, что стандарт рассматривает поддержку почти всего, что связано с хранилищем смешанного типа, как проблему «качества реализации», все, что можно сказать в действительности, это то, что компиляторы, которые подходят для разных целей, поддерживают разные комбинации конструкций.

...