Несколько структур в одном malloc, вызывающем неопределенное поведение - PullRequest
3 голосов
/ 06 марта 2019

С Используйте правильный синтаксис при объявлении элемента гибкого массива , это говорит о том, что когда malloc используется для заголовка и гибких данных, когда data[1] взломан в struct,

Этот пример имеет неопределенное поведение при доступе к любому элементу, кроме первого элемента массива данных.(См. Стандарт C, 6.5.6.) Следовательно, компилятор может генерировать код, который не возвращает ожидаемое значение при доступе ко второму элементу данных.

Я посмотрел Стандарт C в 6.5.6, и не мог понять, как это приведет к неопределенному поведению.Я использовал шаблон, который мне удобен, где за заголовком неявно следуют данные, используя тот же тип malloc,

#include <stdlib.h> /* EXIT malloc free */
#include <stdio.h>  /* printf */
#include <string.h> /* strlen memcpy */

struct Array {
    size_t length;
    char *array;
}; /* +(length + 1) char */

static struct Array *Array(const char *const str) {
    struct Array *a;
    size_t length;
    length = strlen(str);
    if(!(a = malloc(sizeof *a + length + 1))) return 0;
    a->length = length;
    a->array = (char *)(a + 1); /* UB? */
    memcpy(a->array, str, length + 1);
    return a;
}

/* Take a char off the end just so that it's useful. */
static void Array_to_string(const struct Array *const a, char (*const s)[12]) {
    const int n = a->length ? a->length > 9 ? 9 : (int)a->length - 1 : 0;
    sprintf(*s, "<%.*s>", n, a->array);
}

int main(void) {
    struct Array *a = 0, *b = 0;
    int is_done = 0;
    do { /* Try. */
        char s[12], t[12];
        if(!(a = Array("Foo!")) || !(b = Array("To be or not to be."))) break;
        Array_to_string(a, &s);
        Array_to_string(b, &t);
        printf("%s %s\n", s, t);
        is_done = 1;
    } while(0); if(!is_done) {
        perror(":(");
    } {
        free(a);
        free(b);
    }
    return is_done ? EXIT_SUCCESS : EXIT_FAILURE;
}

Prints,

<Foo> <To be or >

Совместимое решение использует C99 гибких элементов массива.На странице также написано:

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

Технически, этот код C90 также вызывает неопределенное поведение?А если нет, то в чем разница?(Или Carnegie Mellon Wiki неверен?) Какой фактор в реализациях не будет работать?

Ответы [ 4 ]

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

В качестве дополнения к @ dbush хороший ответ, способ обойти проблемы выравнивания - это использовать union.Это гарантирует, что &p[1] правильно выровнено для (uint64_t*) 1 .sizeof *p включает все необходимые дополнения по сравнению с sizeof *a.

  union {
    struct Array header;
    uint64_t dummy;
  } *p;
  p = malloc(sizeof *p + length*sizeof p->header->array);

  struct Array *a = (struct Array *)&p[0]; // or = &(p->header);
  a->length = length;
  a->array = (uint64_t*) &p[1]; // or &p[1].dummy;

Или используйте C99 и гибкий элемент массива.


1 А такжеstruct Array

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

Это должно быть четко определено:

a->array = (char *)(a + 1);

Потому что вы создаете указатель на один элемент после конца массива размера 1, но не разыменовываете его. И поскольку a->array теперь указывает на байты, которые еще не имеют эффективного типа, вы можете безопасно их использовать.

Однако это работает только потому, что вы используете байты, следующие за массивом char. Если вы попытаетесь создать массив другого типа, размер которого больше 1, у вас могут возникнуть проблемы с выравниванием.

Например, если вы скомпилировали программу для ARM с 32-битными указателями и у вас было следующее:

struct Array {
    int size;
    uint64_t *a;
};
...
Array a = malloc(sizeof *a + (length * sizeof(uint64_t)));
a->length = length;
a->a= (uint64_t *)(a + 1);       // misaligned pointer
a->a[0] = 0x1111222233334444ULL;  // misaligned write

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

1 голос
/ 07 марта 2019

До публикации C89 существовало несколько реализаций, которые пытались идентифицировать и перехватить доступ к массиву за пределами границ. Учитывая что-то вроде:

struct foo {int a[4],b[4];} *p;

такие реализации будут кричать при попытке доступа к p->a[i], если i не находится в диапазоне от 0 до 3. Для программ, которым не нужно индексировать адрес типа массива lvalue p->a для доступа что-нибудь за пределами этого массива, было бы полезно захватывать такие выходы за пределы допустимого.

Авторы C89 также почти наверняка знали, что программы часто использовали адрес массива фиктивного размера в конце структуры в качестве средства доступа к хранилищу вне структуры. Использование таких методов позволило делать вещи, которые иначе не могли бы быть выполнены так же хорошо, и, как утверждают авторы Стандарта, часть Духа С гласит: «Не мешайте программисту делать то, что нужно». сделано».

Следовательно, авторы Стандарта рассматривали такие обращения как нечто, что реализации могли бы поддерживать или нет на досуге, предположительно, исходя из того, что было бы наиболее полезным для их клиентов. Хотя часто для реализаций, которые обычно ограничивают доступ к структурам в массиве, было бы полезно предоставить возможность пропустить такие проверки в случаях, когда последним элементом структуры с косвенным доступом является массив с одним элементом (или, если они расширят язык, чтобы отменить ограничение времени компиляции (нулевые элементы), люди, пишущие такие реализации, предположительно будут способны распознавать такие вещи без необходимости объяснения их авторами Стандарта. Представление о том, что «неопределенное поведение» было задумано как какая-то форма запрета, похоже, на самом деле не утвердилось до публикации преемника C89.

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

  1. Если выделение передано realloc, указатель внутри него станет недействительным.

  2. Единственное реальное преимущество использования указателя по сравнению с гибким элементом массива состоит в том, что он позволяет указывать его где-то еще. Это может быть хорошо, если единственным видом «чего-то другого» всегда будет постоянный объект статической длительности, который никогда не должен быть освобожден, или, возможно, если это какой-то другой тип объекта, который не нужно освобождать, но может быть проблематичным, если он может содержать единственную ссылку на что-то, хранящееся в отдельном выделении.

Гибкие члены массива были доступны как расширение в некоторых компиляторах до написания C89 и были официально добавлены в C99. Любой достойный компилятор должен их поддерживать.

0 голосов
/ 07 марта 2019

Вы можете определить struct Array как:

struct Array
{
    size_t length;
    char array[1];
}; /* +(length + 1) char */

затем malloc( sizeof *a + length ).Элемент "+1" входит в array[1] член.Заполните структуру:

a->length = length;
strcpy( a->array, str );
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...