Можно ли получить доступ за размер структуры через адрес члена с выделенным достаточным пространством?

В частности, следующий код, строка под маркером, хорошо?

struct S{
    int a;
};

#include <stdlib.h>

int main(){
    struct S *p;
    p = malloc(sizeof(struct S) + 1000);
    // This line:
    *(&(p->a) + 1) = 0;
}

Люди спорили здесь, но никто не дал убедительного объяснения или ссылки.

Их аргументы основаны на несколько иной основе, но, по сути, те же

typedef struct _pack{
    int64_t c;
} pack;

int main(){
    pack *p;
    char str[9] = "aaaaaaaa"; // Input
    size_t len = offsetof(pack, c) + (strlen(str) + 1);
    p = malloc(len);
    // This line, with similar intention:
    strcpy((char*)&(p->c), str);
//                ^^^^^^^

3 ответа

Решение

По крайней мере, с момента стандартизации C в 1989 году было задумано, что реализациям разрешается проверять границы массивов для доступа к массивам.

Член p->a является объектом типа int, C11 6.5.6p7 говорит, что

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

таким образом

&(p->a)

это указатель на int; но это также, как если бы это был указатель на первый элемент массива длины 1, с int как тип объекта.

Теперь 6.5.6p8 позволяет рассчитать &(p->a) + 1 который является указателем на конец массива, поэтому не существует неопределенного поведения. Однако разыменование такого указателя недопустимо. Из Приложения J.2, где это прописано, поведение не определено, когда:

Сложение или вычитание указателя на объект массива или сразу за ним и целочисленный тип приводит к результату, который указывает непосредственно за объектом массива и используется как операнд унарного * оператор, который оценивается (6.5.6).

В приведенном выше выражении есть только один массив, один (как будто) с ровно одним элементом. Если &(p->a) + 1 разыменовывается, доступ к массиву длиной 1 выходит за границы и происходит неопределенное поведение, т.е.

поведение [...], для которого Стандарт [C11] не предъявляет никаких требований

С запиской о том, что:

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

То, что наиболее распространенным поведением является полное игнорирование ситуации, т. Е. Ведение себя так, как будто указатель ссылается на область памяти сразу после, не означает, что другой вид поведения не будет приемлемым с точки зрения стандарта - стандарт допускает все мыслимые и невообразимый результат.


Были претензии, что стандартный текст C11 был написан расплывчато, и намерение комитета должно состоять в том, чтобы это действительно было позволено, и ранее это было бы хорошо. Это не правда. Прочитайте часть ответа комитета на [Дефект № 017 от 10 декабря 1992 года до C89].

Вопрос 16

[...]

отклик

Для массива массивов допустимая арифметика указателя в подпункте 6.3.6, стр. 47, строки 12-40 следует понимать, интерпретируя использование объекта слова как обозначение конкретного объекта, определяемого непосредственно типом и значением указателя, а не другие объекты, связанные с этим по смежности. Поэтому, если выражение превышает эти разрешения, поведение не определено. Например, следующий код имеет неопределенное поведение:

 int a[4][5];

 a[1][7] = 0; /* undefined */ 

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

(жирный акцент мой)

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

Если вы хотите обратиться к structs, вы всегда можете взять указатель на первый член и привести его в качестве указателя на struct и продвиньте это вместо этого:

*(int *)((S *)&(p->a) + 1) = 0;

Это неопределенное поведение, так как вы обращаетесь к чему-то, что не является массивом (int a в struct S) как массив, и за пределами этого.

Правильный способ достичь того, что вы хотите, это использовать массив без размера в качестве последнего struct член:

#include <stdlib.h>

typedef struct S {
    int foo;    //avoid flexible array being the only member
    int a[];
} S;

int main(){
    S *p = malloc(sizeof(*p) + 2*sizeof(int));
    p->a[0] = 0;
    p->a[1] = 42;    //Perfectly legal.
}

Стандарт C гарантирует, что
§6.7.2.1 / 15:

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

&(p->a) эквивалентно (int *)p, &(p->a) + 1 будет адрес элемента второй структуры. В этом случае присутствует только один элемент, в структуре не будет никакого дополнения, так что это будет работать, но там, где будет заполнение, этот код сломается и приведет к неопределенному поведению.

Другие вопросы по тегам