【问题标题】:Multiple structures in a single malloc invoking undefined behaviour?单个 malloc 中的多个结构调用未定义的行为?
【发布时间】:2019-03-06 02:37:42
【问题描述】:

Use the correct syntax when declaring a flexible array member 开始,当data[1] 被黑进struct 时,malloc 用于标题和灵活数据时,

此示例在访问其他任何元素时具有未定义的行为 比数据数组的第一个元素。 (参见 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;
}

打印,

<Foo> <To be or >

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

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

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

【问题讨论】:

  • 注意:“访问数据的第二个元素。” --> 这里没有 array 的第二个元素(struct Array 类型)。 “除了数据数组的第一个元素”不适用,因为没有 array。我在这里看不到 UB,
  • 没错,我可以在Array 的末尾加上一个char [1];这很重要吗?
  • 断言 UB 是由于无效的数组使用。没有数组,因此很难支持 UB 声明,
  • Array 的末尾加上char [1] 会改变struct Array 的类型,但这里仍然没有struct Array arraystruct Array foo[1];是一个数组struct Array *a; 是一个指针。
  • 在链接的示例中,是的,UB 由于索引越界

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


【解决方案1】:

这应该很好定义:

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

因为您创建了一个指向超过大小为 1 的数组末尾的一个元素的指针,但不要取消引用它。而且因为a-&gt;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

您的程序会由于未对齐的写入而崩溃。所以一般来说你不应该依赖这个。最好坚持使用标准保证有效的灵活数组成员。

【讨论】:

  • 有什么方法可以对齐数组,这样就可以了?只需将更多数据放入Array
  • @NeilEdelman 您需要确定结构的对齐要求和指向类型的对齐要求。如果前者较小,则需要四舍五入到后者的下一个倍数并指向那里。
  • C90 中不存在有效类型。
【解决方案2】:

作为@dbush 好的答案的补充,解决对齐问题的一种方法是使用union。这确保&amp;p[1](uint64_t*)1 正确对齐。 sizeof *psizeof *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

【讨论】:

  • 非常聪明!
【解决方案3】:

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

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

如果 i 不在 0 到 3 的范围内,则此类实现会在努力访问 p-&gt;a[i] 时发出尖叫声。对于不需要索引数组类型左值 p-&gt;a 的地址的程序来访问该数组之外的任何内容,能够捕获此类越界访问都会很有用。

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

因此,标准的作者将此类访问视为实现可以支持或不支持的东西,在他们闲暇时,大概基于对他们的客户最有用的东西。虽然对于通常对数组中结构的访问进行边界检查的实现通常会有所帮助,但在间接访问的结构的最后一项是具有一个元素的数组(或者,如果他们扩展语言以放弃编译时约束,零元素),编写此类实现的人可能能够识别此类事物,而无需标准的作者告诉他们。直到 C89 的后续标准发布之后,“未定义的行为”旨在作为某种形式的禁止的概念似乎才真正站稳脚跟。

关于您的示例,在 struct 中有一个指向稍后存储在同一分配中的指针应该可以工作,但有几个警告:

  1. 如果分配传递给realloc,则其中的指针将失效。

  2. 与灵活数组成员相比,使用指针的唯一真正优势在于它允许将指针指向其他位置。如果唯一的“其他东西”永远是一个永远不需要释放的静态持续时间的常量对象,或者如果它是某种不需要释放的其他类型的对象,那么这可能会很好,但可能如果它可以保存对存储在单独分配中的内容的唯一引用,那将是有问题的。

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

【讨论】:

    【解决方案4】:

    你可以定义 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 );
    

    【讨论】:

    • 这就是 CM Wiki 所说的未定义行为。 @chux 在 cmets 中指出,这是因为索引超出范围。 CM Wiki 还说,“尽管不正确的语法将适用于大多数实现”,并将其评为“不太可能”,并说,“对于尚未实现标准 C [99] 语法的编译器,这种方法可能是唯一的选择。”
    • 好的,但是这种实现的想法很糟糕。我的建议是使用更少内存的类似东西,没有指针和 UB。
    猜你喜欢
    • 2011-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-02
    • 1970-01-01
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    相关资源
    最近更新 更多