【问题标题】:Simplest rule of thumb to avoid breaking strict-aliasing rules?避免违反严格混叠规则的最简单的经验法则?
【发布时间】:2015-10-04 10:09:59
【问题描述】:

在阅读有关别名的另一个问题 (What is the strict aliasing rule?) 及其最佳答案时,我意识到我仍然不完全满意,即使我认为我在那里理解了所有内容。

这个问题现在被标记为 C 和 C++。如果您的答案只涉及其中之一,请说明是哪一个。

所以我想了解如何在这方面进行一些开发,以激进的方式投射指针,但有一个简单的保守规则,以确保我不会引入 UB。我有一个关于这样一条规则的建议。

(更新:当然,我们可以避免所有类型的双关语。但这不是很有教育意义。当然,除了@987654323 之外,确实存在个明确定义的例外@异常。)

更新 2:我现在明白为什么这个问题中提出的方法不正确。但是,了解是否存在简单、安全的替代方案仍然很有趣。截至目前,至少有一个答案提出了这样的解决方案。

这是原始示例:

int main()
{
   // Get a 32-bit buffer from the system
   uint32_t* buff = malloc(sizeof(Msg));

   // Alias that buffer through message
   Msg* msg = (Msg*)(buff);

   // Send a bunch of messages    
   for (int i =0; i < 10; ++i)
   {
      msg->a = i;
      msg->b = i+1;
      SendWord(buff[0] );
      SendWord(buff[1] );   
   }
}

重要的一行是:

Msg* msg = (Msg*)(buff);

这意味着现在有两个(不同类型的)指针指向相同的数据。我的理解是,任何通过其中一个进行写入的尝试都会使另一个指针基本上无效。 (“无效”是指我们可以安全地忽略它,但通过无效指针读取/写入是 UB。)

Msg* msg = (Msg*)(buff);
msg->a = 5;           // writing to one of the two pointers
SendWord(buff[0] );   // renders the other, buffer, invalid

因此,我建议的规则是,一旦您创建了第二个指针(即创建 msg),您应该立即并永久地“淘汰”另一个指针。

有什么比将指针设置为 NULL 更好的方法来引退指针:

Msg* msg = (Msg*)(buff);
buff = NULL; // 'retire' buff. now just one pointer
msg->a = 5;

现在,分配给msg-&gt;a 的最后一行不能使任何其他指针无效,因为当然没有。

接下来,当然,我们得想办法打电话给SendWord(buff[1] );。这不能立即完成,因为buff 已停用并且为 NULL。我现在的建议是再次投退。

Msg* msg = (Msg*)(buff);
buff = NULL; // 'retire' buff. now just one pointer
msg->a = 5;

buff = (uint32_t*)(msg);   // cast back again
msg = NULL;                // ... and now retire msg

SendWord(buff[1] );

总而言之,每次你在两个“不兼容”类型之间转换一个指针(我不确定如何定义“不兼容”?),那么你应该立即“淘汰”旧指针。如果这有助于您执行规则,请将其显式设置为 NULL。

这样够保守吗?

也许这太保守了,还有其他问题,但我首先想知道这是否足够保守,以避免通过违反严格的别名来引入 UB。

最后,回顾一下原始代码,修改为使用这条规则:

int main()
{
   // Get a 32-bit buffer from the system
   uint32_t* buff = malloc(sizeof(Msg));

   // Send a bunch of messages    
   for (int i =0; i < 10; ++i)
   {  // here, buff is 'valid'

      Msg* msg = (Msg*)(buff);
      buff = NULL;
      // here, only msg is 'valid', as buff has been retired
      msg->a = i;
      msg->b = i+1;
      buff = (uint32_t*) msg;  // switch back to buff being 'valid'
      msg = NULL;              // ... by retiring msg
      SendWord(buff[0] );
      SendWord(buff[1] );
      // now, buff is valid again and we can loop around again
   }
}

【问题讨论】:

  • 经验法则:不要使用双关语。它的一些实例是明确定义的,但大多数不是,除非有一个很好的理由,否则您通常可以编写一个更漂亮的解决方案,而不涉及类型双关。
  • @FUZxxl,在某种意义上我同意。我从来没有这样做过,我现在也不需要这样做。但我很好奇。在我生命中的某个时候,我可能别无选择,只能稍微突破界限。如果每个人都说“不要这样做”,或者“它总是 UB”,那么我别无选择,只能编写代码并向我的老板说“好吧,它在我的测试中有效,所以我要必须使用它,因为我无法获得任何其他有用的建议” :-) .
  • @AaronMcDaid 好吧,你问的是“最简单的经验法则”,我给你的是最简单的经验法则吗?
  • 此外,对这个问题进行双重标记会使问题加倍复杂,因为 C 中的严格别名规则与 C++ 中的不同。事实上,在 C++ 中,对于在 malloc 空间中进行别名处理时会发生什么,并没有明确说明。

标签: c++ c strict-aliasing


【解决方案1】:

C++ 答案:那行不通。 C++ 严格的别名规则明确列举了哪些类型可以用来访问一个对象。如果你使用不同的类型,你会得到 UB,即使你已经“淘汰”了不同类型的所有访问方法。根据 C++14 (n4140) 3.10/10,允许的类型是:

如果一个程序试图通过 Glvalue 访问一个对象的存储值,而不是其中一个 以下类型的行为未定义:

  • 对象的动态类型,
  • 对象动态类型的 cv 限定版本,
  • 与对象的动态类型类似(如 4.4 中定义)的类型,
  • 对应于对象动态类型的有符号或无符号类型,
  • 一种有符号或无符号类型,对应于动态类型的 cv 限定版本 对象,
  • 在其元素中包含上述类型之一或非静态的聚合或联合类型 数据成员(递归地包括子聚合的元素或非静态数据成员) 或包含联合),
  • 一种类型,它是对象动态类型的(可能是 cv 限定的)基类类型,
  • charunsigned char 类型。

“类似类型”,根据 4.4,与修改多级指针的 cv 限定有关。

因此,如果您曾经通过指向一种类型的指针(或其他访问器)写入区域,即使您忘记了,也无法通过指向不同类型的指针访问它(除非 3.10/10 批准)旧指针。

如果您从未通过特定类型写入区域,则来回转换指针不是问题。

【讨论】:

  • "如果你使用不同的类型,你会得到 UB"。这似乎如此广泛,以至于将 C++ 中的所有类型双关语都写成 UB?肯定是允许的吗?我想我在这里反对“使用”这个词,因为它很模糊。初始演员表是否算作“使用”。或者您是否必须通过强制转换的指针来读/写。
  • 一个非常愚蠢的简短示例来澄清我之前的评论:int main() { long x = 5; long *xp = &amp;x; int *ip = (int*) xp; *ip = 4; } 这是 UB 吗?
  • 严格别名规则的理由通常是在假设某些指针/左值指向不同数据的优化方面给出的。通过引退一个指针,我消除了这个问题并且不允许任何不适当的优化,因为编译器不能再进行不正确的优化。因此,即使我已经解决了该规则的常见原因,严格的别名仍然是一个问题?!
  • @AaronMcDaid 是的,您的 int+long 示例符合 C++ 标准的 UB。我不能为 C 说话。
  • 在 C++ 中,不清楚 malloc 空间的类型是什么。 C 有一条规则,写入 malloc 的空间“印记”执行写入的表达式的类型,但 C++ 没有这样的规则。此外,在 C++ 中还不清楚将对象复制到 malloc 空间中是否会导致 malloc 空间中的对象开始生命周期。 (标准没有说是这样)
【解决方案2】:

规则是:

"除非指针是兼容的类型。你不能有两个 指向同一个内存的指针。”

这是一个更简单的无限循环示例:

1: int *some_buff = malloc(sizeof(whatever));
2: memset(some_buff,0,sizeof(whatever));
3: while (some_buff[0] == 0)
4: {
5:     whatever *manipulator = (whatever*)some_buff; 
6:     manipulate(manipulator);
7: }

这本质上是编译器将/可以处理此代码的方式:

some_buff[0] == 0 的测试可以优化出来,因为有 无法更改 some_buff[0] 的有效方法。它是 通过manipulator 访问,但manipulator 不是 兼容类型,因此根据严格的别名规则, some_buff[0] 的值不能改变。

如果你想要一个更简单的例子:

int *some_buff = malloc(sizeof(whatever));
memset(some_buff,0,sizeof(whatever));
whatever *manipulator = (whatever*)some_buff;
manipulate(manipulator);
printf("%d\n",some_buff[0]);

这段代码总是打印零是完全可以的,不管操作是什么。

【讨论】:

  • 明确一点:两个不兼容的指针指向同一个内存是可以的。这取决于你做什么这些指针是否会导致UB。但是您可以将其用作经验法则​​,以确保您避开任何可能的麻烦。
  • "不能有两个指针指向同一个内存。"是的,你可以。
  • @black 这与之前的评论有何不同?
  • @Let_Me_Be 不多,但您尚未在答案中纠正这一点,您可能想做的事情。
  • @black 这个问题是关于经验法则的。不是定义或规范。这是避免任何问题的好规则,即使它会禁止有效案例。
【解决方案3】:

我的理解是,任何试图通过其中之一进行写作的尝试 将使另一个指针基本上无效。

只要您不访问类型双关指针,另一个“官方”指针就可以了。然而,如果你这样做,它会导致未定义的行为,这可能只是工作,做你所说的或这个星系之外的事情,包括使另一个指针无效。编译器可以随意处理 UB。

根据标准,使buff 成为指向Msg 的有效指针的唯一方法是memcpy/memmove

memcpy( (void*)msg, (const void*) buff, sizeof (*msg));

此外,触发 UB 的不仅是写入,还有 读取 或任何其他访问对象的方式:

如果程序尝试访问对象的存储值 不是以下类型之一的左值行为是 未定义

一些编译器还允许“暂停”该规则,例如 GCC、clang 和 ICC(可能还有 MSVC),但这不能被视为可移植或标准行为。 进一步的技术,以及它们的代码生成分析,被彻底分析here

你真的需要打破严格的别名规则吗?

大多数时候,不,你不需要那个。有ways and ways 来克服涉及完全合法解决方案的问题。 在上述情况下,只需在struct 中存储一个普通指针,并以确定的格式发送每个成员。

【讨论】:

  • @Angew 是的(我确实在谈论访问该对象)。至少我们明白了。
  • 我没有得到这个问题的第一行。你是说如果我通过一个类型双关的指针写入,那么我会得到 UB,即使我再也没有通过任何一个指针读取或写入
  • @Angew 有效我的意思是“安全”访问,即指向的地址是有效的。要初始化该区域,您必须复制MsgBuf 对象的内容,以使别名不成问题; char*,例如,memcpy 可能会这样做。这有帮助吗?
  • @AaronMcDaid 是的,正在修复。它通过导致 UB 的类型双关指针访问对象。正如 Angew 所说,简单地指向它不会造成任何问题。
  • 抱歉学究了,但你不能用“它绝对是 UB”打开,然后引用一个异常 (memcpy) 让它再次正常。如果memcpy 没问题,那么我们可以提炼为什么没问题吗?如果我重新实现了我自己的memcpy,那就没问题了,是吗?并且memcpy 在其签名中具有void*,而不是char*,因此char* 的特殊性与memcpy 的理由无关。 (对不起,如果我的语气有误,但这很重要。)
【解决方案4】:

您的建议根本没有帮助,因为在使用指针变量后分配什么值并不重要。您确实通过不兼容类型的指针访问相同的内存位置。

对于 C(不是对于 C++),除了避免类型双关语之外,至少有一件安全的事情可以做:您可以安全地将指针转换为结构,因为一种结构类型只是添加字段到对方的尽头。这甚至在较长的结构只包含较短的结构作为其第一个成员时也有效:指向结构的指针指向其第一个成员。所以例如这些在 C 中是安全的

typedef struct
{
    int id;
    const char *name;
} base_t;

typedef struct
{
    base_t base;
    long foo;
} derived_t;

derived_t *d = malloc(sizeof derived_t);
base_t *b = (base_t *)d;
int *i = (int *)d;

【讨论】:

  • 我并不是说将指针设置为 NULL 实际上对编译器有任何意义。这样做的目的是提醒我不要再使用那个指针了。而且因为它迫使我停止使用该指针,所以我不是“通过不兼容类型的指针访问[ing]相同的内存位置”。或者,更准确地说,我不会同时访问具有两个不同位置的同一位置。可以说,在代码(演员表)中的某一点有一个明确的“移交”
  • ...在程序的整个生命周期中,由于mallocfree,您有两个不同类型的两个不同指针指向同一位置,这是完全可能的。这显然不是问题。因此,唯一的问题是它们两个指针同时存在。我想。
猜你喜欢
  • 2011-04-15
  • 1970-01-01
  • 2019-04-10
  • 1970-01-01
  • 1970-01-01
  • 2017-02-25
  • 2018-05-23
  • 1970-01-01
  • 2021-02-01
相关资源
最近更新 更多