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 — это массив чаров (как в первом примере), варнинг возникает. Здесь аналогичный вопрос: это лишний варнинг, или же смысл различен?

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

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

Ох ты.

*посыпаю голову пеплом и ухожу читать маны*

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

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

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

На самом-то деле в gcc можно и без копирования, если прифигачить __attribute__((may_alias)) к struct payload *, обидно, что нет стандартного способа это сделать (хотя и можно буфер выделить через malloc или alloca, тогда нарушений не будет по стандарту, но нет никаких гарантий, что в функцию-обработчик подсунут именно такой буфер). Вроде, на си пишут довольно низкоуровневый код, работу с железом, памятью, сетью, контроллеры программируют, а тут такое ограничение серьёзное возникает, ещё и вносит огромную долю непредсказуемости.

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

Тут проблема даже не в сишке, а в сильно разных платформах, потому и неопределенность.

Самое лучшее, передавать plain text, оно конечно дольше, но оверхед ничтожен, переносимость важнее.

Твой подход к серелизации был оправдан в прошлом веке, с модемами на 14400 бод, и 640k, которых хватит всем.

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

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

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

AptGet ★★★
()

Да, иной раз хрен поймешь, с какого перепоя ошибка возникает. Приходится разные варианты пробовать — читать-то мануал неохота!

Зачастую хватает просто расставить дополнительные скобки.

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

Элементарно на макросах можно преобразования сделать: если, скажем, у тебя формат бинарных данных big-endian, а ты на little-endian данные передаешь, то стандартными макросами байтики тудым-сюдым перекидываешь.

Таки все эти хытымыэли уже давным-давно существуют.

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

Да я в курсе, что можно накостылять. А зачем? Текст-то он и в Африке текст.

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

Возможно я ошибаюсь, но любой тип можно приводить к char* и char* можно приводить к любому типу не нарушая strict aliasing. Но стоит помнить нюансы с масивами в C (о которых уже говорили) а именно

char buf[1024];
int *p1 = (int*)(buf); // WARNING buff is not a char*, buf is array
int *p2 = (int*)(&buf); // WARNING (same as above)
int *p3 = (int*)(&buf[0]); // OK, but some optimizations (-O3) cab broke this
int *p4 = (int*)((char*)(buf)); // OK

В вашем примере с struct global ошибка возникает не из-за strict aliasing, проблема возникает из-за агресивной оптимизации gcc (-O3) и неявному повторному изменению структуры global. volatile - решает данную проблему

struct global {
       int a;
} volatile  global;

Также стоит иметь ввиду причины возникновения strict aliasing, в принципе виной тому мобильные плотформы. На некоторых платформах (в частности старые версии ARMов) попытка чтения скжем 32битного инта по адресу не кратному 4 приводит к крашу приложения (SIGBUS) (или 16бит по не четному адрессу). В принципе это основная причина (плюс замутки с оптимизацией) по которой начал активно продвигатся strict aliasing. Но GCC позиционирует себя как лояльный к алиасингу компилятор, и он не генерирует варнинги если strict aliasing явно нарушен но данное нарушение безопасно (по мнению GCC).

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

немного больше поковырял данный пример - в принципе modify возвращает 'a' только если влючена оптимизация по strict-aliasing (тоесть при сборке с -O3 -fno-strict-aliasing возвращаться будет 'b'). Но при этом строчка *(short *)test = 'b'; - обсолютно законна и корректа (мы можем приводить char* к short*). Однако компилятор видет что short* не может быть алиасом ни для global (struct global* не может быть приведен к short*), ни для global.a (int* не совместим с short*) а значит строчка

*(short *)test = 'b';
не может модифецировать переменную global (поскольку разрешена оптимизация по strict aliase), а занчит можно производить оптимизацию возвращаемого значения (вернуть константу 'a' вместо чтения из памяти значения global.a).

Например если заменить short* на int* или global*

((struct global*)(test))->a = 'b';
// OR
*((int*)test) = 'b';
то такое присвоение с точки зрения GCC может изменить состояние переменной global и при возврате значения он сгенерирует чтение из памяти вместо возврата константы 'a'.

Если ничего не напутал то както-так.

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

Самое лучшее, передавать plain text

Пришло время рассказать это тем, кто делал netlink. Да, это я с ним играюсь. Ну и там, собственно, происходит всё локально, поэтому париться о порядке байт, паддингах и т.п. не нужно, переносимость не нужна здесь.

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

и char* можно приводить к любому типу не нарушая strict aliasing

Я читал стандарт и из того, что я понял, это не всегда правда. Если у объекта (куска памяти) есть декларированный тип, и это, например, char, то к нему нельзя обратиться, как к struct payload, потому что его эффективный тип тоже будет char, несовместимый со struct payload, тут нарушение. Если же мы буфер замаллочили и получили на него указатель char *, то декларированного типа у объекта не будет, а эффективный тип будет определяться типом lvalue, вызвавшим последнее изменение, если оно было, иначе типом текущего обращения, т.е. в данном случае можно смело алиасить выделенный динамически массив чаров и struct payload.

На некоторых платформах (в частности старые версии ARMов) попытка чтения скжем 32битного инта по адресу не кратному 4 приводит к крашу приложения (SIGBUS) (или 16бит по не четному адрессу).

Да, я в курсе, что такие архитектуры есть, и я слежу за тем, чтобы обращения были выравненными. Тем более даже на x86, где есть unaligned access, он более медленный.

Однако компилятор видет что short* не может быть алиасом ни для global (struct global* не может быть приведен к short*), ни для global.a (int* не совместим с short*) а значит строчка *(short *)test = 'b'; не может модифецировать переменную global (поскольку разрешена оптимизация по strict aliase), а занчит можно производить оптимизацию возвращаемого значения (вернуть константу 'a' вместо чтения из памяти значения global.a).

Да, именно так этот пример и работает, во втором сообщении всё верно.

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

Ещё раз: в прошлом веке это было оправданно и целесообразно.

А переносимость она разная бывает. Например между разными платформами.

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

Смотря что и для чего мы передаём. В случае netlink там на фиг не сдался текстовый протокол. И в других случаях бинарщину не выкинуть. Аудио- и видеоконтейнеры, форматы архивов, исполняемые файлы — это те места, где сериализовать текстом было бы глупо и непрактично. А ещё все эти файлы тоже можно передавать по сети. А ещё есть сетевые протоколы и более низких уровней, чем прикладной.

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

Понятное дело, что везде текст пихать нерационально. Но ведь ты искал переносимое решение?

Кстати, в самих иксах картинки хранятся в текстовом формате xpm.

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

Я читал стандарт и из того, что я понял, это не всегда правда. Если у объекта (куска памяти) есть декларированный тип, и это, например, char, то к нему нельзя обратиться, как к struct payload, потому что его эффективный тип тоже будет char ...

Это из за путаници char*. Всеже char* можно алиасить во что угодно, НО!

char buf[1024];
В данном примере buf - это не char*, это массив и его алиасить нельзя. При этом GCC ведет себя несколько странно, для него buf+0 - опять массив а не char*, зато buf+1 - это уже char*. Как по мне то у GCC есть явные не доработки со стрикт алиасингом (он ведет себя не всегда адекватно с ним).

Второй момент если у нас есть функция которая принимает char* в качестве параметра то в принципе неважно как он получился на входе (малоком или массивом) - он должен алиасится без проблем.

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

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

Второй момент если у нас есть функция которая принимает char* в качестве параметра то в принципе неважно как он получился на входе (малоком или массивом) - он должен алиасится без проблем.

Проблемы возможны. Если функция находится и вызывается в том же файле, что и массив, то компилятор будет видеть, что char * указывает именно на массив. У массива есть эффективный тип (равный декларированному типу) — массив чаров, их нельзя алиасить структурой struct payload. Если функция заинлайнится, то могут произойти оптимизации, которые нарушат намерения, если мы будем обращаться к массиву по имени и к struct payload по указателю.

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

А вот это, мне кажется, объясняет наличие и отсутствие варнингов во втором примере из ОП. Массив чаров не обязан быть варавнен, поэтому делать из него указатель на структуру чревато.

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

Проблемы возможны. Если функция находится и вызывается в том же файле ... Если функция заинлайнится ...

Да в этом случае возможны проблемы, согласен.

zaz ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.