3

在声明灵活数组成员时使用正确的语法它说,当被黑客入侵时,何时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;
}

印刷,

<Foo> <To be or >

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

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

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

4

4 回答 4

2

这应该很好定义:

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

因为您创建了一个指向超过大小为 1 的数组末尾的一个元素的指针,但不要取消引用它。而且因为a->arraynow 指向的字节还没有有效的类型,所以你可以安全地使用它们。

但是,这仅适用,因为您将后面的字节用作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

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

于 2019-03-06T03:17:25.670 回答
2

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

于 2019-03-06T03:29:46.443 回答
1

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

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

p->a[i]如果不在 0 到 3 的范围内,这样的实现会努力访问i。对于不需要索引数组类型左值的地址p->a来访问该数组之外的任何内容的程序,能够捕获这样的输出-of-bounds 访问会很有用。

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

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

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

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

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

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

于 2019-03-07T17:58:02.700 回答
0

您可以将结构定义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 );
于 2019-03-07T18:04:10.293 回答