LINUX.ORG.RU

Я познаю strict aliasing

 


4

5

Всем привет, недавно у меня стало возникать слишком много мыслей о том, насколько легитимно приводить типы указателей в некоторых случаях и не нарушает ли это правил strict aliasing.

Компилируется код такой командой (gcc 4.9.2):

gcc test.c -o /dev/null -O3 -Wall -Wextra

Случай №1. Есть какой-то буфер в виде массива чаров, полученный откуда-то (по сети, например). Хочется его распарсить, для этого привести char * к какому-нибудь struct payload * и работать со структурой; выравнивание и порядок байтов к вопросу отношения не имеют, считаем, что там всё правильно. Для примера можно для упрощения вместо struct payload взять обычный int — с ним происходит то же самое:

int main()
{
        char buf[5] = "TEST";
        int *p = (int *)&buf; // По стандарту char может алиасить любой тип, но не наоборот
        *p = 0x48414559; // Но здесь предупреждения о нарушении strict aliasing почему-то нет
        *(int *)buf = 0x48414559; // А вот здесь есть
//        *(int *)(buf+1) = 0x48414559; // Вот так уже не будет, кстати
        return 0;
}
test.c: In function 'main':
test.c:6:2: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
  *(int *)buf = 0x48414559;
  ^

Вопрос: чем отличается доступ через указатель p и через buf, приведенный к int *? Почему в одном случае нет варнинга, в другом есть? Действительно ли в одном случае нарушается правило strict aliasing, а в другом нет? Или это потерянный варнинг? Или особенность реализации gcc?

Случай №2. Приведение struct sockaddr_in * к struct sockaddr *, использующееся повсеместно. Для чистоты эксперимента структуры объявнены вручную, а не взяты из хедеров. Да, их наполнение отличается от того, что там должно быть. Итак, я решил продолжить эксперимент с приведением типа указателя без промежуточной переменной.

#include <stdint.h>

struct sockaddr {
        uint16_t sa_family;
        char sa_data[14];
};

struct sockaddr_in
{
        uint16_t sin_family;
        uint16_t sin_port;
        uint32_t sin_addr;
        char sin_zero[8];
};

int main()
{
        {
                struct sockaddr_in addr;
                ((struct sockaddr *)&addr)->sa_family = 2; // Тут варнинга почему-то нет
        }
        {
                char addr[16];
                ((struct sockaddr *)&addr)->sa_family = 2; // А тут есть, как и в предыдущем примере
        }
        {
                uint32_t addr[4];
                ((struct sockaddr *)&addr)->sa_family = 2; // А здесь почему-то снова нет
        }
        return 0;
}

Господа, я в замешательстве. Вот моё мнение по этому поводу:

В первом примере нарушения правила strict aliasing есть в обоих случаях (char может алиасить любой тип, но не наоборот), однако варнинг есть почему-то в одном из случаев, в связи с этим вопрос: это недостающий варнинг или особенность поведения gcc?

Во втором примере нарушений правила strict aliasing нет, поскольку я обращаюсь только к объекту struct sockaddr. Однако в случае, когда addr — это массив чаров (как в первом примере), варнинг возникает. Здесь аналогичный вопрос: это лишний варнинг, или же смысл различен?

Ну и один глобальный вопрос: если я где-то в своих рассуждениях ошибаюсь (или чего-то не понимаю), то где?

int *p = (int *)&buf

& здесь не нужен. Иначе получается фигня. buf уже char *

mkevac ()

И тут тоже:

char addr[16];
((struct sockaddr *)&addr)->sa_family = 2;
Имя массива это и есть его адресс. В каждой книге же об этом пишут.

FIL ★★★★ ()
Ответ на: комментарий от FIL

mkevac

Если foo — это имя массива, то foo и &foo — эквивалентные записи.

intelfx ★★★★★ ()
Ответ на: комментарий от mkevac

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

int *p = (int *)buf;

Это не вызывает warning почему-то. Ещё попробовал с -Wstrict-aliasing=2, тогда это начинает вызывать warning, это меня запутало ещё больше.

gentoo_root ★★★★★ ()
Ответ на: комментарий от intelfx

Если foo — это имя массива, то foo и &foo — эквивалентные записи.

Не совсем правда, вот пример:

void arr(int (*a)[10])
{
        *a[0] = *a[9];
}

int main()
{
        int a[10];
        int b[9];
        arr(a); // warning
        arr(&a); // ok
        arr(&b); // warning
        return 0;
}
arr.c: In function 'main':
arr.c:10:6: warning: passing argument 1 of 'arr' from incompatible pointer type
  arr(a);
      ^
arr.c:1:6: note: expected 'int (*)[10]' but argument is of type 'int *'
 void arr(int (*a)[10])
      ^
arr.c:12:6: warning: passing argument 1 of 'arr' from incompatible pointer type
  arr(&b);
      ^
arr.c:1:6: note: expected 'int (*)[10]' but argument is of type 'int (*)[9]'
 void arr(int (*a)[10])
      ^

mkevac, FIL

gentoo_root ★★★★★ ()

int *p = (int *)&buf; // непонятно куда указывает, поэтому нет

*(int *)buf = 0x48414559; //указывает на char как на int, поэтому есть

struct sockaddr_in addr; ((struct sockaddr *)&addr)->sa_family = 2; // sin_family uint16_t и sa_family uint16_t, поэтому нет

char addr[16]; ((struct sockaddr *)&addr)->sa_family = 2; // addr[0] char, a sa_family uint16_t, поэтому есть

uint32_t addr[4]; ((struct sockaddr *)&addr)->sa_family = 2; // addr[0] uint32_t, a sa_family uint16_t, будет warning или нет зависит от эффективного размера поля sa_family в структуре.

zudwa ()
Ответ на: комментарий от gentoo_root

Можно даже такого навернуть:

int (*arr(int (*a)[10]))[10]
{
        return a;
}

Но всё же хотелось бы разобраться со strict aliasing :D

gentoo_root ★★★★★ ()
Ответ на: комментарий от intelfx

Это что за тип вообще? o_0

Указатель на массив. То есть именно на массив интов размером N, а не на инт.

gentoo_root ★★★★★ ()
Ответ на: комментарий от gentoo_root

Тогда чем void arr(int (*a)[10]) отличается от void arr(int a[10])?

intelfx ★★★★★ ()

А вообще собрал я твой случай №1 шлангом, c -Wall, Werror, -std=c++11. Все работает.

FIL ★★★★ ()
Ответ на: комментарий от intelfx

char buf[16];

char* p1=buf;

char* p2=&buf[0];

вот так указатели будут равны

Если char* p=&buf, то buf[0] == **p

zudwa ()
Ответ на: комментарий от zudwa

Имя buf не является lvalue просто по смыслу (на стеке нет такого места, где хранился бы адрес начала массива buf), поэтому там неоткуда взяться двойному разыменованию.

intelfx ★★★★★ ()
Ответ на: комментарий от zudwa

int *p = (int *)&buf; // непонятно куда указывает, поэтому нет

Как это непонятно? На buf указывает.

struct sockaddr_in addr; ((struct sockaddr *)&addr)->sa_family = 2; // sin_family uint16_t и sa_family uint16_t, поэтому нет

struct sockaddr addr;
((struct sockaddr_in *)&addr)->sin_port = 2;

Не годится объяснение, такое тоже не даёт варнинга.

char addr[16]; ((struct sockaddr *)&addr)->sa_family = 2; // addr[0] char, a sa_family uint16_t, поэтому есть

uint32_t addr[4]; ((struct sockaddr *)&addr)->sa_family = 2; // addr[0] uint32_t, a sa_family uint16_t, будет warning или нет зависит от эффективного размера поля sa_family в структуре.

Ну и в чём же разница? Ни char, ни uint16_t — это не uint32_t.

gentoo_root ★★★★★ ()
Ответ на: комментарий от intelfx

Тогда чем void arr(int (*a)[10]) отличается от void arr(int a[10])?

В параметрах функции int a[10] — это то же самое, что int *a.

gentoo_root ★★★★★ ()
Последнее исправление: gentoo_root (всего исправлений: 1)
Ответ на: комментарий от intelfx

[] это несколько однотипных элементов в памяти (неважно где: в стеке, куче или статических)

buf это указатель на начало этой памяти, элвивалентен &buf[0]

&buf - это указатель на указатель buf

zudwa ()
Ответ на: комментарий от FIL

А вообще собрал я твой случай №1 шлангом, c -Wall, Werror, -std=c++11. Все работает.

Шланг я пока решил вообще не трогать (хотя он у меня есть, я даже пытался собрать llvm шлангом, и он не собрался, а gcc хотя бы может собрать себя :D). Всё работает — это не тот результат, который я ожидаю, по моей логике там два нарушения, так что я хотел бы узнать объяснение, почему так ведёт себя gcc, как он себя ведёт, либо почему там нет нарушений.

gentoo_root ★★★★★ ()
Ответ на: комментарий от gentoo_root

эффективный размер поля uint16_t в структуре может быть больше (uint32_t) из-за выравнивания, а меньше (char) быть не может.

zudwa ()
Ответ на: комментарий от zudwa

Это всё я понимаю. «Указателя buf» нет нигде в памяти, поэтому его адрес взять невозможно (== это не lvalue). Как правильно замечает gentoo_root, с точки зрения сишной системы типов выражения buf и &buf всё же отличаются (хоть я и не понимаю, чем именно), но по факту у них одно и то же значение: адрес начала массива buf.

intelfx ★★★★★ ()
Ответ на: комментарий от zudwa

эффективный размер поля uint16_t в структуре может быть больше (uint32_t) из-за выравнивания

Но на практике на x86_64 и на x86 он не будет больше. Для того чтобы удостовериться в этом, добавил __attribute__((packed)) к объявлениям структур, выхлоп не изменился.

gentoo_root ★★★★★ ()
Ответ на: комментарий от intelfx

с точки зрения сишной системы типов выражения buf и &buf всё же отличаются

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

FIL ★★★★ ()
Ответ на: комментарий от intelfx
char buf[100];
char** p=&buf;

if(buf[0] == **p){

}

не код же пишу, а пояснения

zudwa ()
Ответ на: комментарий от zudwa

Запускать-то хоть пробовал этот свой код?

int main()
{
        char a[] = "abc";
        char **p = &a;
        if (a[0] == **p) {
                printf("%c\n", **p);
        }
        return 0;
}

До принтэфа даже не доходит, раньше сегфолтит (в первом **p).

gentoo_root ★★★★★ ()
Последнее исправление: gentoo_root (всего исправлений: 1)
Ответ на: комментарий от gentoo_root

А как он по твоему работать должен? Если оно даже собираться не должно, мы же выяснили что &a == a, а тут двойной указатель, которого нету даже.
Объяви как const char *a = «abc» и будет оно работать.

FIL ★★★★ ()
Последнее исправление: FIL (всего исправлений: 3)
Ответ на: комментарий от gentoo_root

Конечно варнинг, ты просишь двойной указатель в функцию а передаешь одинарный.

FIL ★★★★ ()
Последнее исправление: FIL (всего исправлений: 1)
Ответ на: комментарий от FIL

А как он по твоему работать должен?

По-моему он и не должен работать. Он и не работает. А вот zudwa считает, что должен. Я осознанно скомпилировал написанный им код.

Если оно даже собираться не должно

Там варнинги, не ерроры.

gentoo_root ★★★★★ ()
Ответ на: комментарий от FIL

Конечно варнинг, ты просишь двойной указатель в функцию а передаешь одинарный.

Это была демонстрация различий &a и a.

gentoo_root ★★★★★ ()
Ответ на: комментарий от gentoo_root
char buf[]="123";
printf("buf: %p\np: %p\npp: %p",buf, &buf, &buf[0]);

buf: 0xbfaa5cf4
p: 0xbfaa5cf4
pp: 0xbfaa5cf4

да, видимо перепраздновал... извиняюсь.

zudwa ()

Кто мне объяснит почему &buf = buf дам печеньку.
А я пожалуй спать. Кстати второй случай скомпиленный в шланге тоже работает

FIL ★★★★ ()
Ответ на: комментарий от FIL

Кто мне объяснит почему &buf = buf дам печеньку.

Если рассматривать только значения, а не типы, то:

&buf — это адрес массива. По этому адресу находятся элементы массива, начиная с нулевого. Поэтому адрес нулевого элемента &buf[0] будет равен адресу массива &buf. Когда же мы пишем просто buf, эта запись в подходящих случаях (т.е. в большинстве случаев) неявно преобразовывается в указатель на тип элемента массива, который указывает на начало массива (т.е. туда же, куда и &buf, и &buf[0]), таким образом вся арифметика с указателями работает (операторы +, -, *).

Что касается типов, то типы у &buf[0] и преобразованного в указатель buf одинаковые — это указатель на элемент массива (т.е. buf == &buf[0] и по значению, и по типу). Тип у &buf — указатель на массив (что-то вроде char (*)[10], например), но он может неявно преобразовываться в указатель на элемент массива (char *), пример, где это разные вещи, я приводил выше.

Как-то так, вроде, надеюсь, на ночь глядя не напутал чего.

gentoo_root ★★★★★ ()

сишники до сих пор героически борются с указателями.

10 различий между *&*&*&*&*&&&&&*****&&*&*&*&ptr и &****&*&*&&&&*****ptr.

anonymous ()
Ответ на: комментарий от mkevac

& здесь не нужен. Иначе получается фигня. buf уже char *

Нет никакой фигни, это равноценные записи.

andreyu ★★★★★ ()
Ответ на: комментарий от andreyu

ох уж эти умолчания в приведениях.

равноценность &buf и buf где buf это массива - артефакт развития языка Си

если требовать от языка «ортогональности» и забить на удобство то &buf и buf дОлжно быть различно ибо объект и имя_объекта могут совпадать только у объектов которые в остальном пусты , но не у контейнеров.

qulinxao ★★☆ ()
Ответ на: комментарий от qulinxao

Это не имеет значения. Артефакт это или как оно «должно было быть». Есть факт - записи в данном случае равноценны.

andreyu ★★★★★ ()
Ответ на: комментарий от FIL

buf это сам массив, если не веришь, посчитай sizof. А &buf это указатель. При передаче массива в функцию передаётся только указатель, и sizeof внутри функции покажет размер указателя, 4/8 единиц для x86/amd64 соответственно.

emulek ()

из выше приведённой переписки:

From: Dennis Ritchie <dmr@bell-labs.com>
Newsgroups: comp.std.c
Subject: Re: Initializing Automatic Arrays
Date: Tue, 29 Sep 1998 18:27:02 +0100

Initializing automatic arrays was explicitly not in K&R 1;
it was added by the current standard.  It was such an obvious
extension that it was probably added as an extension to
some compilers before the standard.

Another change was a bit stranger, namely the one discussed in
another thread; if fp is a function pointer then all
of

  fp(), (*fp)(), (**fp)(), (***fp)() ...

are allowed and all the same.  In K&R 1 function names
did not decay to pointers in function call position, and
the thing in function call position had to be a function,
not a pointer; the only correct form was

  (*fp)()

This was enforced in the PDP-11 compiler.  For reasons known
only to himself, Johnson in PCC decided to accept both fp()
and (*fp)().  The rest were an accidental effect.  The committee
(presumably because it had become practice) decided to change
the rules to regularize it.

Something similar (but with different result) happened with
arrays.  In K&R 1, given an array A, &A was simply not allowed.
Johnson allowed &A, with the same meaning as A.  Here, however,
the committee decided to extend K&R 1 consistently, with &A
being a pointer to A.  This was the right thing to do because
there are (rare) occasions when it's needed.

The problem with the PCC interpretations of these is that
as a «favor» to the user, it complicated further an already somewhat
difficult type notation.  (I plead guilty to somewhat the same
thing in allowing [] in function arguments.)

	Dennis
qulinxao ★★☆ ()
Ответ на: комментарий от emulek

Юзай union, бро

union не хочу юзать, потому что не хочу лишнего копирования. Мне это представляется как-то так:

char buf[1024];
/* recv'ом зафигачили в buf какие-то данные, опустим детали */
union {
    char buf[1024];
    struct payload payload;
} u;
memcpy(u.buf, buf, sizeof(u.buf));
struct payload *payload = &u.payload;

Во-первых, выглядит громоздко. Во-вторых, buf будет копироваться (бегло посмотрел ассемблерный код с -O3, там есть rep stos). В основном мне здесь не нравится копирование.

Почитал здесь цитаты из стандарта и сделал следующие выводы. Если выделять buf каким-нибудь malloc'ом, то можно смело сначала в него зафигачить кучу чаров, а потом скастовать указатель к struct payload * и работать с ним. Если же buf — это объявленная переменная, то у неё уже есть эффективный тип char, приведение к struct payload * будет нарушением правил алиасинга.

Поэтому нарушения алиасинга есть во всех примерах из ОП. Варнинги от gcc бесполезны, поскольку я нафигачил у себя локально кучу примеров, где на разных уровнях -Wstrict-aliasing= варнинги то есть, то нет, при этом нарушения есть. Есть даже пример, не выводящий никогда варнинг, но дающий неправильный ответ; clang ведёт себя так же:

#include <stdio.h>

// gcc 5.c -o 5 -O3 -Wall -Wextra -Wstrict-aliasing=3
// no warning

// gcc 5.c -o 5 -O3 -Wall -Wextra -Wstrict-aliasing=2
// no warning

// gcc 5.c -o 5 -O3 -Wall -Wextra -Wstrict-aliasing=1
// no warning

// strict aliasing is broken

struct global {
        int a;
} global;

int modify(char *test)
{
        global.a = 'a';
        *(short *)test = 'b';
        return global.a;
}

int main()
{
        printf("%c\n", modify((char *)&global));
        return 0;
}

Пример печатает a, запись символа b, скорее всего, переставляется.

gentoo_root ★★★★★ ()
Последнее исправление: gentoo_root (всего исправлений: 1)
Ответ на: комментарий от gentoo_root

Дык ты сделай массив из union'ов, и прямо его и юзай.

А то, что ты пытаешься сделать, имхо непереносимый и неподдерживаемый говнокод. Лично меня расстраивает, когда указатель ВНЕЗАПНО меняет свой тип. Да, для malloc сделано исключение, там иначе никак не реализовать.

emulek ()
Ответ на: комментарий от gentoo_root

Зафигачивай не в buf, а прямо из u.buf. в этом случае можно юзать безымянные обьединения, если лень много букв писать.

И используй обьединение, без преобразования.

emulek ()
Ответ на: комментарий от emulek

Дык ты сделай массив из union'ов

И как я массивом из юнионов буду массив чаров преобразовывать в одну структуру?

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

Именно это — код, который по C99 будет работать. В C++ может не работать, там юнион не обязан делать type punning, и даже есть компилятор от Sun, с которым этот фокус не пройдёт.

Лично меня расстраивает, когда указатель ВНЕЗАПНО меняет свой тип.

А по-другому никак, если хочется из указателя на массива чаров сделать указатель на структуру.

Зафигачивай не в buf, а прямо из u.buf.

Один момент мешает этому — зафигачивание будет происходить в main(), а преобразование в struct payload в другой функции, которой будет передан указатель char *.

gentoo_root ★★★★★ ()
Ответ на: комментарий от gentoo_root

Я не понимаю проблемы, передай указатель на union, в котором массив чартов и структура. Зачем тут копирование и преобразование?

Зыж пишу с планшета, потому не смогу дать пример кода, извиняюсь.

emulek ()
Ответ на: комментарий от emulek

Ну вот есть у меня основная программа, там я объявляю массив чаров под буфер. Принимаю его по сети recv'ом. После этого передаю его функции, которая занимается обработкой, передаю как есть. Основную программу не должно заботить, что означают эти чары, она не знает, что там на самом деле лежит struct payload, который тоже заканчивается массивом чаров, в котором может лежать struct data1 или struct data2 и т.п. Поэтому я не могу объявить union в основной программе, принять данные туда и передать union в функцию-обработчик. Я уже должен в функции-обработчике думать, как мне так извернуться, чтобы полученный char * превратить в struct payload *.

gentoo_root ★★★★★ ()
Ответ на: комментарий от gentoo_root

А.

void handler (char *param)
{
    union {
        char *raw;
        struct payload *payload;
    } u;

    u.raw = param;

    /* access u.payload */
}
intelfx ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.