单个malloc中的多个结构调用未定义的行为?

时间:2019-03-06 02:37:42

标签: c language-lawyer c89 flexible-array-member

Use the correct syntax when declaring a flexible array member中说,当malloc被入侵data[1]时,struct被用作标题和灵活数据时,

  

当访问其他任何元素时,此示例具有未定义的行为   比数据数组的第一个元素。 (请参见C标准6.5.6。)   因此,编译器可以生成不返回代码的代码。   访问数据的第二个元素时的期望值。

我查看了C Standard 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;
}

打印

<Foo> <To be or >

兼容的解决方案使用C99个灵活的数组成员。该页面还显示,

  

在声明灵活数组时未能使用正确的语法   成员可能导致未定义的行为,尽管语法不正确   将可用于大多数实现。

从技术上讲,此C90代码也会产生未定义的行为吗?如果没有,有什么区别? (或者Carnegie Mellon Wiki是不正确的?)对它不起作用的实现因素是什么?

4 个答案:

答案 0 :(得分:2)

这应该定义清楚:

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

因为您创建了一个指向大小为1的数组末尾的元素的指针,但没有取消引用它。而且由于a->array现在指向的字节尚未有效,因此可以安全地使用它们。

这仅能工作,因为您将后面的字节用作char数组。如果改为尝试创建大小大于1的其他类型的数组,则可能会出现对齐问题。

例如,如果您使用32位指针为ARM编译了一个程序,并且具有以下内容:

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 :(得分:2)

作为@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 :(得分:1)

在发布C89之前,有一些实现将尝试识别和捕获越界数组访问。给出类似的东西:

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

如果p->a[i]不在0到3的范围内,则此类实现会努力尝试访问i。对于不需要索引数组类型左值{ {1}}可以访问该数组之外的任何内容,因此能够捕获此类越界访问将很有用。

C89的作者几乎还可以肯定,程序在结构的末尾使用伪大小的数组的地址作为访问结构外存储的一种手段是很常见的。使用这种技术可以完成其他方面几乎无法完成的工作,根据标准作者的说法,C精神的一部分是“不要阻止程序员去做需要做的事情”。完成”。

因此,该标准的作者将这些访问视为实现可以在其闲暇时支持或不支持的东西,大概是基于对客户最有用的东西。尽管通常对检查数组中结构的访问进行边界检查的实现通常会有所帮助,但在间接访问结构的最后一项是具有一个元素的数组(或,如果他们将语言扩展为放弃编译时约束(零元素),则编写此类实现的人们大概能够识别此类事情,而本标准的作者不必告诉他们。直到C89的后继标准发布后,“未定义行为”的意图才被视为某种形式的禁止。

对于您的示例,在结构点内有一个指针,可以在以后以相同的分配存储该指针,但应该注意以下几点:

  1. 如果将分配传递给p->a,则其中的指针将变为无效。

  2. 使用指针与灵活数组成员的唯一真正优势在于,它允许将指针指向其他位置。如果唯一的“别的东西”始终是永远不需要释放的静态持续时间的恒定对象,或者如果不是必须释放的是其他某种对象,则可能会很好。如果它只能保存对存储在单独分配中的内容的引用,将是有问题的。

在编写C89之前,灵活的数组成员已在某些编译器中作为扩展提供,并在C99中正式添加。任何体面的编译器都应支持它们。

答案 3 :(得分:0)

您可以将结构struct Array { size_t length; char array[1]; }; /* +(length + 1) char */ 定义为:

malloc( sizeof *a + length )

然后array[1]。 “ +1”元素位于a->length = length; strcpy( a->array, str ); 成员中。用以下内容填充结构:

List<HttpStatusCode> list = EnumUtils<HttpStatusCode>.ToList();

Dictionary<int, HttpStatusCode> map = EnumUtils<HttpStatusCode>.ToDictionary();