【问题标题】:Pointer in Struct -- unknown sized array结构中的指针——未知大小的数组
【发布时间】:2015-01-12 11:53:45
【问题描述】:

我正在尝试在 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; 

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

如何解决这个分段错误?

【问题讨论】:

  • 那是因为你的value 是一个指针,你指定它指向地址0x01。你不能那样做。
  • 指针不是数组。

标签: c struct


【解决方案1】:

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】:

    以下内容几乎是完全可移植的,而且由于别名肯定不是 UB,因为在任何时候都不会取消引用 test,所以你可以忘记它。

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

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

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

    除非您将value 更改为对齐类型,否则您很可能会崩溃。 这一切都有效,因为sizeof(unsigned char) 必须为 1 并且类型对齐必须除以它们的大小。请记住,如果它不是 malloc(n*sizeof(T)) 对于某些类型 T 被破坏为 n 类型为 T 的元素的数组。 C 标准被画在了 unsigned char 无法对齐的角落,因为将内存视为 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;
    }
    

    当你可以做到这一点(我被告知从 C99 开始)并且没有糟糕的对齐问题时:

    #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;
    }
    

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

    您(当然)可以混合解决方案,但如果您打包结构,您可能会破坏value 的对齐(如果需要对齐)并且如果您不冒编译器内部填充TLV 的风险。在当前的情况下,这种内部填充不太可能,但如果您将 length 升级为 size_t 类型,则实际上很有可能 - 这是自然的“完整”答案。

    坦率地说,当前 255 的长度限制(几乎在所有平台上)都是小气。 1993 年用 Turbo Pascal 写作感觉很刻薄。在 2015 年,它令人毛骨悚然。至少将length 实现为“unsigned int”,除非您知道如此严格的上限就足够了。

    【讨论】:

    • OP 声明他正在解除对结构指针的引用 然而,当我尝试访问值数组时,我遇到了分段错误,即使我尝试访问值内存地址的第一个元素(其中应该是数组中长度之后的第一个元素)。 其中 value 是结构中的元素,通过指针访问。因此导致你知道什么(未定义的 b.)。
    • @2501 他没有提供他的代码,但我相信我的第一个示例答案可以解决这个问题。你是说我漏掉了问题?
    • 你的第二个例子是正确的,但第一个不是,特别是这些问题:Breaking strict aliasing rules here: printf("%u\n",(&amp;tlv-&gt;value)[i]): ISO/IEC 9899:201x 6.5,p76.2.7,p2。并且在 i 的值大于零时超出对象边界的同一行:6.5.6, p8。在这两种情况下,标准都明确表示,引用:行为未定义
    • @2501 我会检查一下,但我相信你过度解释了别名。首先,您没有超出数组范围。 test 被分配为一个数组。在那里执行指针运算绝对可以。其次,没有取消引用别名。根本上,您认为有问题的别名的两个取消引用是什么?您不能选择访问权限并将其声明为别名。你需要两个。 test 永远不会被取消引用。没关系。
    • test 永远不会被取消引用。 test 在这里被取消引用printf("%u\n",(&amp;tlv-&gt;value)[i]); 取消引用结构指针本身会导致未定义的行为,因为:ISO/IEC 9899:201x 6.2。 5、p28 声明:指向其他类型的指针不必具有相同的表示或对齐要求。 这意味着struct 指针不能表示char 指针。即使你认为&amp;tlv-&gt;value 没有取消引用指针,指针本身也是无效的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-15
    • 1970-01-01
    • 2017-06-30
    相关资源
    最近更新 更多