问题描述
假设我们有一个以灵活数组成员结尾的结构:
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;
此处可以在本地定义 bar256
和 foobar256
。您可以通过 f.data
的 b.data
或 foobar256
访问数据。这种类型的双关语在 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。