Struct中的指针 - 未知大小的数组

时间:2015-01-12 11:53:45

标签: c struct

我正在尝试在C中实现TLV (Type-Length-Value),但是我遇到了使动态大小值正常工作的问题。

我的结构看起来像这样:

typedef struct __attribute__((packed)){
   unsigned char type;
   unsigned char length;
   unsigned char * value;
} TLV;

我正在尝试将数组转换为结构,以便我可以轻松访问类型和长度。例如一个数组:

unsigned char test[5] = {(unsigned char)'T', 0x03, 0x01, 0x02, 0x03};

' T'在数组中是类型,第一个0x03是长度。

我正在将数组转换为结构,如下所示:

TLV* tlv = (TLV*)test; 

然而,当我尝试访问值数组时,我得到了一个分段错误,即使我尝试访问值存储器地址的第一个元素(应该是长度之后的数组中的第一个元素)。

如何解决此分段错误?

3 个答案:

答案 0 :(得分:6)

不要将值声明为指针,而是将其声明为未知大小的数组unsigned char value[]

typedef struct __attribute__((packed))
{
   unsigned char type;
   unsigned char length;
   unsigned char value[];
} TLV;

执行此操作后,其余代码将按预期工作。

答案 1 :(得分:3)

value不是数组,它是一个指针(指向结构外部的某个位置)。如果您想要数组(大小未知),请改为编写unsigned char value[1]

typedef struct __attribute__((packed)) {
    unsigned char type;
    unsigned char length;
    unsigned char value[1];
} TLV;

拥有大小为1的数组允许您实际处理任意数量的字节。这实际上是UB,但它实际上已经使用并且在我看到的所有情况下都能正常工作。

GCC允许使用大小为0的数组。我已经习惯了这种惯例,我忘了在C中不允许使用大小为0的数组。

编辑:

答案很长

数组和指针之间存在差异。虽然您可以使用类似的代码来处理两者,但这些仍然是不同的野兽。

免责声明:以下代码适用于gcc,但可能并非严格有效。我并没有试着让它完全有效。

让我们定义两个结构:

typedef struct {
    char p[20];
} sa;

typedef struct {
    char *p;
} sp;

并创建那些实例:

sa x = { "Hello, world" };
sp y = { "Howdy, world" };

这两者之间有什么区别?

printf("%s\n", x.p); // prints "Hello, world"
printf("%s\n", y.p); // prints "Howdy, world"

这些地址怎么样?

printf("address of x = %p\n", &x); // On my machine it prints 0x7fffacce9b20
printf("address of y = %p\n", &y); // 0x7fffacce9b10

嗯..不是很有趣,除了这些数字是非常相似的 - 两个结构都位于大致相同的位置 - 在我的情况下它是堆栈,地址空间的末尾,但可能在其他地方。

printf("address of x.p = %p\n", &x.p); // 0x7fffacce9b20
printf("address of y.p = %p\n", &y.p); // 0x7fffacce9b10

相同的数字。正如预期的那样。

printf("address of x.p[0] = %p\n", &x.p[0]); // 0x7fffacce9b20 - same as before
printf("address of y.p[0] = %p\n", &y.p[0]); // 0x400764 - could be anything

现在这些是不同的。字符串“Hello,world”与结构x位于同一位置,而字符串“Howdy,world”位于其他位置 - 数据段,位于地址空间的开头,但也可能位于其他位置。

所以这就是区别:数组是“在这里”存储的一些数据,而指针只是“某处”存储的数据的地址。

在您的情况下,您希望能够将数据保存在“此处” - 紧跟在类型和长度之后。这就是你需要数组而不是指针的原因。

我找不到任何证据证明上面的TLV实现不是UB,但我看到很多情况下,通过将其转换为指向某个结构的指针来“解析”字符数组。我自己甚至写了这样的代码。

0大小的数组

正如我之前所说,C标准不允许使用大小为0的数组。但它们是GCC允许的,这很方便,因为它允许您执行以下操作:

typedef struct {
    unsigned char type;
    unsigned char length;
    unsigned char value[0];
} TLV;

int required_length = 10;
TLV *tlv = (TLV *) malloc(sizeof(TLV) + required_length);

如果没有0大小的数组,你必须在上面的代码中的某处添加(或减去?减去我猜)1。

答案 2 :(得分:-1)

以下几乎完全是可移植的,当然不是由于别名造成的UB,因为在任何时候test都不会被解除引用,所以你可以忘掉它。

(技术上)可移植的是假设struct TLV中没有内部填充。

为了获得可移植性,我删除了__attribute__((packed))

如果你的编译器支持它,那么你是100%明确没有UB。

除非您将value更改为对齐类型,否则您可能会被破坏。 这一切都有效,因为sizeof(unsigned char)必须为1,并且类型对齐必须划分它们的大小。请记住,如果某个类型T没有malloc(n*sizeof(T)),则会将n T类型的unsigned char元素数组破坏。 C标准被绘制成char无法对齐的角落,因为将内存视为assert(.)(任何一种)的数组始终是合法的。

因此,以下程序将在#include <stddef.h> #include <stdlib.h> #include <stdio.h> #include <assert.h> typedef struct { unsigned char type; unsigned char length; unsigned char value; } TLV; static TLV dummy; int main(void) { //There's no standard way to verify this at compile time. //NB: If you stick with packing or leave all the members of TLV the same type //Then this is almost certainly NOT an issue. //However the cast of test implicitly assumes the following is the case. //Here's a run-time check of a static constraint. assert(offsetof(TLV,value)==(sizeof(dummy.type)+sizeof(dummy.length))); unsigned char test[5] = {(unsigned char)'T', 0x03, 0x01, 0x02, 0x03}; TLV* tlv=(TLV*)test; for(unsigned char i=0;i<tlv->length;++i){ printf("%u\n",(&tlv->value)[i]); } (&tlv->value)[0]=253; (&tlv->value)[1]=254; (&tlv->value)[2]=255; for(unsigned char i=0;i<tlv->length;++i){ printf("%u\n",(&tlv->value)[i]); } return EXIT_SUCCESS; } 失败或成功执行。 在所有已知的平台上,它将成功执行,因为没有已知的平台可以选择内部填充给定的数据结构 - 无论您是否指定打包。

但为什么这样做:

#include <stdlib.h>
#include <stdio.h>

typedef struct {
   unsigned char type;
   unsigned char length;
   unsigned char value[];//Variable length member.
} TLV;

int main(void) {

    TLV* tlv=malloc(sizeof(TLV)+3*sizeof(unsigned char));

    tlv->type='T';
    tlv->length=3;
    tlv->value[0]=1;
    tlv->value[1]=2;
    tlv->value[2]=3;

    for(unsigned char i=0;i<tlv->length;++i){
        printf("%u\n",tlv->value[i]);
    }

    tlv->value[0]=253;
    tlv->value[1]=254;
    tlv->value[2]=255;

    for(unsigned char i=0;i<tlv->length;++i){
        printf("%u\n",tlv->value[i]);
    }

    free(tlv);

    return EXIT_SUCCESS;
}

当你能做到这一点(C99以后我被告知)并且没有糟糕的对齐问题:

value

请注意,没有合规的保证方式静态分配这些东西,因为没有合规的保证方式来指示结构的布局(特别是大小),因此您无法知道在char数组中分配多少空间。

你可以(当然)混合解决方案,但是如果打包结构,你可能会破坏TLV的对齐(如果需要对齐的东西),如果你没有冒险编译器内部填充{{1 }}。如果你将length升级到size_t的类型 - 这是自然的“完整”答案,那么内部填充不太可能是当前的伪装,但实际上非常可能。

目前的长度限制为255(几乎在所有平台上)坦率地说是吝啬。 1993年在Turbo Pascal写作时感觉很卑鄙。在2015年它是piffling。至少将length实现为`unsigned int',除非你知道这么紧的上限就足够了。