如何在堆栈上分配具有灵活数组成员的结构

问题描述

假设我们有一个以灵活数组成员结尾的结构:

struct foo {
    size_t len;
    uint8_t data[];
};

如何在栈上分配这个结构体(即在作用域结束时自动释放内存)?此外,如果 len 可以包含字段 data 的大小,那就太好了。

目前,我做的事情如下:

uint8_t buf[256];
struct foo *foo = (struct foo *)buf;
foo->len = sizeof(buf) - sizeof(struct foo);

但是,它很容易出错。使用 alloca() 可能会稍微好一点:

struct foo *foo = alloca(256 + sizeof(struct foo));
foo->len = 256;

从那里,我可以定义一个这样的宏:

#define STACK_ALLOC_FOO(SIZE) ({                          \
    struct foo *_tmp = alloca(SIZE + sizeof(struct foo)); \
    _tmp->len = SIZE;                                     \
    _tmp;                                                 \
})

并声明它:

struct foo *foo = STACK_ALLOC_FOO(256);

但是,我不确定用 alloca() 分配的内存的生命周期。是内部作用域还是函数

此外,分配全局变量是行不通的(即使这不是我主要关心的问题)。

是否有人考虑过在堆栈上分配具有灵活数组成员的结构的良好做法?

解决方法

假设我们有一个以可变长度数组 (VLA) 结尾的结构:

好吧,你没有。您有一个以灵活数组成员结尾的结构。不同的东西,主要用于动态内存分配场景。

如何在栈上分配这个结构

如果没有一些非标准的扩展,很难做到这一点。例如,一个 alloca 扩展保证返回没有有效类型的内存。这意味着内存尚未被编译器在内部标记为具有某种类型。否则...

struct foo *foo = (struct foo *)buf;

您会遇到严格的别名违规未定义行为,就像上面的错误代码一样。 What is the strict aliasing rule?

此外,您还需要注意对齐和填充。

但是,我不确定使用 alloca() 分配的内存的生命周期。是内部作用域还是函数?

可能是的。它不是标准函数,我不确定任何 lib 对其行为提供了可移植的保证。它甚至不是 POSIX 函数。 Linux man 保证:

alloca() 函数在调用者的堆栈帧中分配 size 字节的空间。当调用 alloca() 的函数返回给它的调用者时,这个临时空间会自动释放。

我假设这适用于 *nix 下的 gcc/glibc,但不适用于其他工具链或系统。


为了获得可移植且坚固耐用的代码,您可以做的是:

struct foo {
    size_t len;
    uint8_t data[];
};

struct bar256 {
  size_t len;
  uint8_t data[256];
};

typedef union
{
  struct foo f;
  struct bar256 b;
} foobar256;

此处可以在本地定义 bar256foobar256。您可以通过 f.datab.datafoobar256 访问数据。这种类型的双关语在 C 中是允许的并且定义良好。

此时您可能会意识到结构体更麻烦的是它的价值,只需使用两个局部变量,一个是实际的 VLA:

size_t len = ... ;
uint8_t data[len];
,

可变长度数组(如 GNU C 中所理解的)通常不使用 alloca 分配。在 C90 中,它们不受支持。

典型的方式是这样的:

int main() {
    int n;
    struct foo {
        char a;
        int b[n]; // n needs to be in the same scope as the struct definition
    };

    n = 1;
    struct foo a;
    a.a = 'a';
    a.b[0] = 0;
    // writing a.b[1] = 1 will not cause the compiler to complain

    n = 2;
    struct foo b;
    b.a = 'b';
    b.b[0] = 0;
    b.b[1] = 1;
}

-fsanitize=undefined 与 GCC(更具体地说是 -fsanitize=bounds)一起使用将在访问越界 VLA 成员时触发运行时错误。

,

如果您打算像这样使用它:

#include <stdio.h>
#include <stdlib.h>
#include <alloca.h>
#include <string.h>
#include <stdint.h>
#include <sys/types.h>

struct foo {
    size_t len;
    uint8_t data[];
}; 


#define STACK_ALLOC_FOO(SIZE) ({                          \
    struct foo *_tmp = alloca(SIZE + sizeof(struct foo)); \
    _tmp->len = SIZE;                                     \
    _tmp;                                                 \
})


void print_foo() {
    struct foo *h = STACK_ALLOC_FOO(sizeof("Hello World"));
    memcpy(h->data,"Hello World",h->len);
    fprintf(stderr,"[%lu]%s\n",h->len,h->data);
}

int main(int argc,char *argv[])
{
    print_foo();
    return 0;
}

因此:

alloca() 分配的空间不会自动释放 如果指向它的指针超出范围。

它将产生完全有效的代码,因为唯一超出范围的是 *_tmp 并且不会解除分配 alloca,您仍然在同一个堆栈帧中。它确实会随着 print_foo 的返回而解除分配。

实际上,看看编译器如何处理优化标志和程序集输出非常有趣。 (如果您例如使用 -O3,则与 alloca 相关的代码在 main 中完全重复)

希望有帮助

,

作为替代,我建议:

#define DECLARE_FOO(NAME,SIZE) \
  struct {                      \
      struct foo __foo;         \
      char __data[SIZE];        \
  }  __ ## NAME = {             \
      .__foo.len = SIZE,\
  };                            \
  struct foo *NAME = (struct foo *)&__ ## NAME

所以你可以这样做:

DECLARE_FOO(var,100);

它不是很优雅。但是,它可以声明全局/静态变量。

,

如何在堆栈上分配具有可变长度数组 (VLA) 的结构

您必须确保您的缓冲区正确对齐。使用 unsinged char 或仅 char 表示“字节”,uint8_t 表示 8 位数字。

#include <stdalign.h>
alignas(struct foo) unsigned char buf[sizeof(struct foo) + 20 * sizeof(uint8_t));
struct foo *foo = (struct foo *)buf;
foo->len = sizeof(buf) - sizeof(struct foo);

我可以像这样定义一个宏:

({ 是一个 gcc 扩展。您还可以定义一个宏来定义变量,例如:

// technically UB I believe
#define FOO_DATA_SIZE  sizeof(((struct foo*)0)->data)

struct foo *foo_init(void *buf,size_t bufsize,size_t count) {
    struct foo *t = buf;
    memset(t,bufsize);
    t->size = count;
    return t;
}

#define DEF_struct_foo_pnt(NAME,COUNT) \
    _Alignas(struct foo) unsigned char _foo_##NAME##_buf[sizeof(struct foo) + COUNT * FOO_DATA_SIZE); \
    struct foo *NAME = foo_init(_foo_##NAME##_buf,sizeof(buf),COUNT);

void func() {
   DEF_struct_foo_pnt(foo,20);
}

使用 alloca() 可能稍微好一点:

除非您碰巧在循环中调用 alloca()...

我不确定用 alloca() 分配的内存的生命周期。是内部作用域还是函数?

Memory allocated with alloca gets freed at end of function or at end of scope?

分配全局变量不起作用(即使这不是我的主要关注点)。

这会很难 - C 没有构造函数。您可以使用外部工具或尝试使用预处理器魔术来生成代码,例如:

_Alignas(struct foo) unsigned char buf[sizeof(struct foo) + count * sizeof(uint8_t)) = {
     // Big endian with 64-bit size_t?
     20,0x00,};
struct foo *foo_at_file_scope = (struct foo*)buf;

即您必须初始化缓冲区,而不是结构。我想我会使用相同的编译器和相同的选项在 C 中编写一个工具来生成代码(对于 gcc-ish 环境中的交叉编译,我只会编译一些带有初始化的可执行文件到 ELF 文件,而不是运行它,使用objdump从ELF文件中获取初始化,并处理它以生成C源)。

或者,您可以(ab-)使用 GCC 扩展 __attrbute__((__constructor__)) - 在另一个宏中定义具有该属性的函数。一些东西:

#define DEF_FILE_SCOPE_struct_foo_pnt(NAME,COUNT) \
    _Alignas(struct foo) unsigned char _foo_##NAME##_buf[sizeof(struct foo) + COUNT * FOO_DATA_SIZE); \
    struct foo *NAME = NULL; \
    __attribute__((__constructor__)) \
    void _foo_##NAME##_init(void) { \
        NAME = foo_init(_foo_##NAME##_buf,COUNT); \
    }

DEF_FILE_SCOPE_struct_foo_pnt(foo_at_file_scope,20)

是否有人考虑过在堆栈上分配 [灵活数组成员] 的良好做法?

不要使用它们。相反,使用指针和 malloc。