Можно ли исправить небезопасное типирование, отметив переменную volatile? - PullRequest
0 голосов
/ 10 декабря 2018

В ответе zwol на Законно ли реализовать наследование в C путем приведения указателей между одной структурой, которая является подмножеством другой, а не первого члена? он приводит пример того, почему простая типизация между аналогичными структурамине является безопасным, и в комментариях есть пример среды, в которой он ведет себя неожиданно: компиляция следующего с gcc на -O2 заставляет его напечатать "x = 1.000000 some = 2.000000"

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

struct base
{
    double some;
    char space_for_subclasses[];
};
struct derived
{
    double some;
    int value;
};

double test(struct base *a, struct derived *b)
{
    a->some = 1.0;
    b->some = 2.0;
    return a->some;
}

int main(void)
{
    size_t bufsz = sizeof(struct base);
    if (bufsz < sizeof(struct derived)) bufsz = sizeof(struct derived);
    void *block = malloc(bufsz);

    double x = test(block, block);
    printf("x=%f some=%f\n", x, *(double *)block);
    return 0;
}

Iдурачился с кодом, чтобы лучше понять, как он себя ведет, потому что мне нужно сделать что-то подобное, и заметил, что маркировки a как volatile было достаточно, чтобы он не печатал разные значения.Это согласуется с моими ожиданиями относительно того, что происходит неправильно - gcc предполагает, что запись b->some не влияет на a->some.Тем не менее, я бы подумал, что gcc может принять это, только если a или b помечены restrict

Я неправильно понимаю, что здесь происходит и / или значение ограничивающего квалификатора?Если нет, то может ли gcc сделать это предположение , потому что a и b бывают разных типов?Наконец, помечает ли a и b как volatile соответствие этого кода стандарту или, по крайней мере, не позволяет неопределенному поведению разрешить gcc сделать вышеупомянутое предположение?

Ответы [ 2 ]

0 голосов
/ 10 декабря 2018

Если доступ к области памяти осуществляется исключительно с использованием volatile -квалифицированных l-значений, компилятору придется приложить все усилия, чтобы не обрабатывать каждую запись как преобразование значений, записанных в массив битов, и их сохранение,и каждый читается как чтение битового паттерна из памяти и перевод его в значение.Стандарт на самом деле не предписывает такое поведение, и теоретически данный компилятор:

long long volatile foo;
...
int test(void)
{
  return *((short volatile*)(&foo));
}

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

С другой стороны, учитывая функцию, подобную следующей:

void zero_aligned_pair_of_shorts(uint16_t *p)
{
  *((uint32_t void volatile*)&p) = 0;
}

компиляторы, такие как gcc и clang, не будут надежно распознавать, чтоэто может оказать некоторое влияние на сохраненное значение объекта, доступ к которому осуществляется с использованием неквалифицированного lvalue типа uint16_t.Некоторые компиляторы, такие как icc, рассматривают доступ volatile как индикатор для синхронизации любых объектов, кэшированных в регистре, чей адрес был взят, потому что это делает дешевый и простой способ для компиляторов придерживаться принципа духа C, описанного в уставе стандартов иобосновать документы как «не мешайте программисту делать то, что нужно», не требуя специального синтаксиса.Однако другие компиляторы, такие как gcc и clang, требуют, чтобы программисты либо использовали специфичные для gcc / clang встроенные функции, либо использовали параметры командной строки для глобального блокирования большинства форм кэширования регистров.

0 голосов
/ 10 декабря 2018

Проблема с этим конкретным вопросом и ответом Звола заключается в том, что они объединяют типизацию и строгое наложение.Ответ Зволя правильный для этого конкретного варианта использования из-за типа, используемого для инициализации структуры;но не в общем случае и не в отношенииstruct sockaddr POSIX-типы, как можно прочитать из ответа, подразумеваемого.

Для определения типов между структурами с общими начальными членами все, что вам нужно сделать, это объявить (не использовать!) Объединение этих структур,и вы можете безопасно получить доступ к общим членам через указатель любого из типов структур.Это явно разрешенное поведение начиная с ANSI C 3.3.2.3, включая C11 6.5.2.3p6 (ссылка на черновик n1570).

Если реализация содержит объединение всех struct sockaddr_ структурвидимый для приложений пользовательского пространства, ответ zwol OP ссылки на OP, на мой взгляд, вводит в заблуждение, если считать, что подразумевается, что поддержка структуры struct sockaddr требует чего-то нестандартного от компиляторов.(Если вы определите _GNU_SOURCE, glibc определит такое объединение как struct __SOCKADDR_ARG, содержащее анонимное объединение всех таких типов. Однако glibc предназначен для компиляции с использованием GCC, поэтому у него могут быть другие проблемы.)

Строгое псевдонимы - это требование, чтобы параметры функции не ссылались на одно и то же хранилище (память).Например, если у вас

int   i = 0;
char *iptr = (char *)(&i);

int modify(int *iptr, char *cptr)
{
    *cptr = 1;
    return *iptr;
}

, то вызов modify(&i, iptr) является строгим нарушением псевдонимов.Тип punning в определении iptr является случайным и фактически разрешен (поскольку вы можете использовать тип char для проверки представления хранилища любого типа; C11 6.2.6.1p4 ).

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

struct item {
    struct item *next;
    int          type;
};

struct item_int {
    struct item *next;
    int          type; /* == ITEMTYPE_INT */
    int          value;
};

struct item_double {
    struct item *next;
    int          type; /* == ITEMTYPE_DOUBLE */
    double       value;
};

struct item_string {
    struct item *next;
    int          type;    /* == ITEMTYPE_STRING */
    size_t       length;  /* Excluding the '\0' */
    char         value[]; /* Always has a terminating '\0' */
};

enum {
    ITEMTYPE_UNKNOWN = 0,
    ITEMTYPE_INT,
    ITEMTYPE_DOUBLE,
    ITEMTYPE_STRING,
};

Теперь, если в той же области виден следующий союз, мы можем набрать пункт между указателямик указанным выше типам структур и совершенно безопасно получить доступ к элементам next и type:

union item_types {
    struct item         any;
    struct item_int     i;
    struct item_double  d;
    struct item_string  s;
};

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

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

void print_items(const struct item *list, FILE *out)
{
    const char *separator = NULL;

    fputs("{", out);        

    while (list) {

        if (separator)
            fputs(separator, out);
        else
            separator = ",";

        if (list->type == ITEMTYPE_INT)
            fprintf(out, " %d", ((const struct item_int *)list)->value);
        else
        if (list->type == ITEMTYPE_DOUBLE)
            fprintf(out, " %f", ((const struct item_double *)list)->value);
        else
        if (list->type == ITEMTYPE_STRING)
            fprintf(out, " \"%s\"", ((const struct item_string *)list)->value);
        else
            fprintf(out, " (invalid)");

        list = list->next;
    }

    fputs(" }\n", out);
}

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

Обтекание по типу происходит в операторах fprintf() и действует в том и только в том случае, если 1) структуры были инициализированы с использованием структур, соответствующих полю type, и2) union item_types виден в текущей области видимости.

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

...