当前位置: 首页 > 知识库问答 >
问题:

严格遵守container_of的使用

澹台承
2023-03-14

container_of及其等效的WinApiCONTAING_RECORD是流行且有用的宏。原则上,它们使用char*上的指针算法来恢复指向成员的给定指针所属聚合的指针。

极简主义的实现通常是:

#define container_of(ptr, type, member) \
   (type*)((char*)(ptr) - offsetof(type, member))

然而,这个宏的使用模式的严格符合性是有争议的。例如:

struct S {
    int a;
    int b;
};

int foo(void) {
    struct S s = { .a = 42 };
    int *p = &s.b;
    struct S *q = container_of(p, struct S, b);
    return q->a;
}

据我了解,该程序不严格合规,因为:

    < li >表达式< code>s.b是< code>int类型的左值 < li > <代码>

我注意到问题不在于< code>container_of宏本身。而是构造< code>ptr参数的方式。如果指针是从< code>struct S类型的l值计算的,则不会有越界运算。就不会有UB了。该程序的一个潜在兼容版本是:

int foo(void) {
    struct S s = { .a = 42 };
    int *p = (int*)((char*)&s + offsetof(struct S, b));
    struct S *q = container_of(p, struct S, b);
    return q->a;
}

发生的实际算术是:

container_of(ptr, struct S, b)

展开container_of

(struct S*)((char*)(ptr) - offsetof(struct S, b))

放置ptr的表达式

(struct S*)((char*)((int*)((char*)&s + offsetof(struct S, b))) - offsetof(struct S, b))

drop casts < code >(char *)(int *)

(struct S*)((char*)&s + offsetof(struct S, b) - offsetof(struct S, b)))

添加offsetof(struct S, b)不会溢出struct S。做算术时没有UB。正负项减少。

(struct S*)((char*)&s)

现在去掉多余的造型。

&s

以上推导正确吗?

< code>container_of的这种用法是否严格合规?

如果是这样,则指向成员的指针的计算可以委托给名为member_of的新宏。指针的构造方式与container_of类似。这个新宏将是container_of的补充,用于严格兼容的程序。

#define member_of(ptr, type, member) \
   (void*)((char*)(ptr) + offsetof(type, member))

或者更方便,类型安全,但不太便携(尽管在C23中很好)版本:

#define member_of(ptr, member) \
   (typeof(&(ptr)->member))((char*)(ptr) + offsetof(typeof(*(ptr)), member))

该计划将是:

int foo(void) {
    struct S s = { .a = 42 };
    int *p = member_of(&s, struct S, b);
    struct S *q = container_of(p, struct S, b);
    return q->a;
}

共有2个答案

章晗日
2023-03-14

如果你正在寻找的正式术语是严格一致的,那么这意味着不存在任何形式的定义不明确的行为。如果您的示例依赖于对齐/填充注意事项,则它们具有实现定义的行为,并且由于该原因而未严格符合。

否则,在C17 6.3.2.3/7:下,C允许所有形式的对象指针转换

指向对象类型的指针可以转换为指向不同对象类型的指针。如果生成的指针没有正确对准引用的类型,则行为未定义。否则,当再次转换回来时,结果将与原始指针相等。当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。结果的连续递增,直到对象的大小,会产生指向对象剩余字节的指针。

“再次转换”规则保证“元数据”不会丢失。

这条规则还意味着我们可以通过使用字符指针来检查结构,但如果不是从开始就开始检查,那么在任何地方开始检查确实是有问题的。指向结构中间的int*指针必须被视为一个int*指针,而不是一个指向“聚合”类型的指针,因此我们可能不会越界访问它(在指向的地址下方迭代)。

相反,为了理解上述规则,我们必须可以自由地将结构视为
char [sizeof(the_struct)],否则我们会遇到指针算术规则的麻烦(在 C17 6.5.6 下面说明)。但要做到这一点,我们需要从指向结构的指针开始。

至于严格混淆现象规则(6.5/7),它只适用于进行左值访问时,因此在这里大多不相关。此外,它在将某些内容作为字符类型访问时也有特殊例外。

所以你的假设看起来都是正确的,根据上面引用的规则6.3.2.3/7,它们不违反指针算术也不违反严格的混淆现象。

关于类型安全,也许您可以在标准C中实现宏,如下所示:

#define member_of(ptr, type, member) \
  _Generic((ptr), type*: (type*)((char*)(ptr) + offsetof(type, member)) )
吴高畅
2023-03-14

指针元数据有两种情况

类型#1 -指针指向分配的缓冲区,其中隐藏的前同步码保存分配的块的元数据。

从本幻灯片中(幻灯片#9之后):

这绝对不会影响指针算术,也不是OP所指的情况。

类型#2-源或嵌入指针的其他元数据

这是“C的来源感知内存对象模型”的草稿。它描述了在C中实现指针解析来源背后的想法。

有一句话讨论了成员补偿:

指针成员偏移量给定C类型τ处的非空指针p,该指针指向结构或联合类型对象的开始(ISO C建议必须存在,写“该值是第一个表达式指向的对象的命名成员的值”),如果p ,偏移指向成员 m的指针的结果具有相同的源π和适当的偏移量a。

结合后面关于指针算术的两个语句:

指针加法和减法指针算术(整数的加法或减法)保留出处。如果结果不在存储实例内(或过去一次),则生成的指针值不确定。

指针差值 仅针对具有相同出处且位于同一数组中的指针定义指针差值...

以及没有提议改变ISO标准中讨论指针算法的第6.2.5节的事实。

导致唯一可能的结论,这是可以的。

另一个问题是< code>(char*)(ptr)操作是否违反了严格的别名规则。

严格的混淆现象定义(以防万一),来自不同的堆栈溢出帖子:

严格别名是C(或C)编译器的一种假设,即对不同类型对象的取消引用指针永远不会引用同一内存位置(即彼此别名)

但是,由于该操作位于同一结构中,并且我们仅将其用于编译时计算,因此这是可以的。

 类似资料:
  • 问题内容: 默认情况下,Logback编码器使用类似于ISO 8601 标准的日期格式。但是它在日期和时间部分之间的中间缺少“ T”。该标记使解析更加容易,并且是标准所必需的(除非私有方另行同意)。 是否有一些技巧可以 使Logback包括在内? 这个… 代替这个 我想我可以在添加“ T”的同时重新创建整个格式,但是我想知道是否有一些更简单的方法。 问题答案: Logback的JIRA页面上有关于

  • 问题内容: 我有一个DateTime对象,目前正在通过该对象格式化 这给了我确切的格式: 星期二5.3.2012 唯一缺少的一点是正确的语言。我需要(),即()的德语翻译。 这给了我正确的语言环境设置 但是我不知道该如何使用它。 没有办法做类似的事情: 问题答案: 那是因为不注意语言环境。您应该改用。 例如:

  • 本文向大家介绍使用Hooks要遵守哪些原则?相关面试题,主要包含被问及使用Hooks要遵守哪些原则?时的应答技巧和注意事项,需要的朋友参考一下 只在最顶层使用 Hook 不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。 只在 React 函数中调用 Hook 不要在普通的 JavaScript 函数中调用 Hook。你可以: ✅ 在 React 的函

  • 本文向大家介绍使用HTML5需要遵守哪些规则?相关面试题,主要包含被问及使用HTML5需要遵守哪些规则?时的应答技巧和注意事项,需要的朋友参考一下 * 新的特性应该基于HTML、CSS、DOM和JavaScript * 减少对外部插件的需求(如Flash) * 更好的错误处理 * 更多的替换脚本的标记 * HTML5应与设备无关 * 开发过程必须可视化

  • 我正在尝试以一种方式管理日志记录,即最早的存档日志文件一旦达到总累积大小限制或达到最大历史记录限制,就会被删除。在Logback 1.1.7中使用时,滚动文件附加器将继续创建新的存档,尽管超过了设置。 这是日志中的一个bug还是我没有正确配置滚动文件附加器?

  • 我们应该严格遵守一个组件只有一个根元素吗? 相比于一个组件有多个根元素它有什么好的地方和不不好的地方?