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



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

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扩展保证返回没有有效类型的内存。这意味着编译器尚未将内存内部标记为具有特定类型。否则

structfoo*foo=(structfoo*)buf;

您会得到严格的别名冲突未定义行为,就像上面的错误代码一样。什么是严格的混叠规则?

此外,您还需要注意对齐&衬料

然而,我不确定用alloca()分配的内存的生存期。是内部范围还是功能?

可能吧。它不是一个标准函数,我不确定任何lib是否能为其行为提供可移植的保证。它甚至不是POSIX函数。Linuxman保证:

alloca()函数在调用方的堆栈帧中分配大小字节的空间。当调用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;

这里CCD_ 7和CCD_。您可以通过foobar256f.datab.data访问数据。这种类型的双关语在C.中是允许的,并且定义明确

在这一点上,你可能会意识到这个结构比它更麻烦,只需要使用两个局部变量,其中一个是实际的VLA:

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

作为替代方案,我建议:

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

所以你可以做:

DECLARE_FOO(var, 100);

它不是很优雅。但是,它可以声明全局/静态变量,并且不依赖于任何强制转换运算符。

可变长度数组(如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]%sn", h->len, h->data);
}
int main(int argc, char *argv[])
{
print_foo();
return 0;
}

正因为如此:

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

它将生成完全有效的代码,因为唯一超出作用域的是*_tmp,并且它不会解除分配,您仍然在同一堆栈帧内。它确实会随着print_foo的返回而被取消分配。

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

希望这能帮助

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

您必须确保缓冲区正确对齐。使用CCD_ 17或仅CCD_;字节";,CCD_ 19表示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, 0, 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()分配的内存的生存期。是内部范围还是功能?

用alloca分配的内存在函数结束时还是在作用域结束时被释放?

分配全局变量是不起作用的(即使它不是我主要关心的问题)。

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

_Alignas(struct foo) unsigned char buf[sizeof(struct foo) + count * sizeof(uint8_t)) = {
// Big endian with 64-bit size_t?
20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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, sizeof(buf), COUNT); 
}
DEF_FILE_SCOPE_struct_foo_pnt(foo_at_file_scope, 20)

是否有人在堆栈上分配[灵活数组成员]的良好实践?

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

最新更新