最简单的经验法则是避免破坏严格别名规则?

时间:2022-07-17 02:14:20

While read another question about aliasing ( What is the strict aliasing rule? ) and its top answer, I realised I still wasn't entirely satisfied even though I think I understood it all there.

在阅读另一个关于别名的问题(什么是严格的混叠规则?)及其最佳答案时,我意识到我仍然不完全满意,即使我认为我在那里理解了它。

(This question is now tagged as C and C++. If your answer refers to just one of these, please clarify which.)

(这个问题现在被标记为C和C ++。如果您的答案仅涉及其中一个,请澄清哪个。)

So I want to understand how to do some development in this area, casting pointers in aggressive ways, but with a simple conservative rule that ensures I don't introduce UB. I have a proposal here for such a rule.

所以我想了解如何在这个领域做一些开发,以积极的方式投射指针,但有一个简单的保守规则,确保我不引入UB。我在这里提出了这样的规则。

(Update: of course, we could just avoid all type punning. But that's not very educational. Unless of course, there are literally zero well-defined exceptions, beyond the union exception.)

(更新:当然,我们可以避免所有类型的惩罚。但这不是很有教育意义。除非当然,除了联合例外之外,确实存在零明确定义的异常。)

Update 2: I understand now why the method proposed in this question is not correct. However, it is still interesting to know whether a simple, safe, alternative exists. As of now, there is at least one answer that proposes such a solution.

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

This is the original example:

这是最初的例子:

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] );   
   }
}

The important line is:

重要的是:

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

which means there are now two pointers (of different types) pointing to the same data. My understanding is that any attempt to write through one of these will render the other pointer essentially invalid. (By 'invalid' I mean that we can ignore it safely, but that reading/writing through an invalid pointer is UB.)

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

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

Therefore, my proposed rule is that, once you create the second pointer (i.e. create msg), you should immediately and permanently 'retire' the other pointer.

因此,我提出的规则是,一旦你创建了第二个指针(即创建消息),你应该立即永久地“退出”另一个指针。

What better way to retire a pointer than to set it to NULL:

退出指针比将其设置为NULL更好的方法是:

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

Now, the last line assigning to msg->a can't invalidate any other pointers because, of course, there are none.

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

Next, of course, we have to find a way to call SendWord(buff[1] );. This can't be done immediately because buff has been retired and is NULL. My proposal now is to cast back again.

接下来,当然,我们必须找到一种方法来调用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] );

In summary, every time you cast a pointer between two 'incompatible' types (I'm not sure how to define 'incompatible'?) then you should immediately 'retire' the old pointer. Set it to NULL explicitly if that helps you to enforce the rule.

总之,每次在两个'不兼容'类型之间转换指针时(我不确定如何定义'不兼容'?),那么你应该立即'退出'旧指针。如果这有助于您强制执行规则,则将其显式设置为NULL。

Is this conservative enough?

这样保守吗?

Perhaps this is too conservative and has other problems, but I first want to know if this is conservative enough to avoid introducing UB via offending strict aliasing.

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

Finally, recap the original code, modified to use this rule:

最后,回顾原始代码,修改为使用此规则:

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

4 个解决方案

#1


6  

C++ answer: that won't work. The C++ strict aliasing rule explicitly enumerates which types can be used to access an object. If you use a different type, you get UB, even if you've "retired" all access methods of a different type. As per C++14 (n4140) 3.10/10, the allowed types are:

C ++回答:那不行。 C ++严格别名规则显式枚举了可用于访问对象的类型。如果您使用其他类型,即使您已“退出”所有不同类型的访问方法,也会获得UB。根据C ++ 14(n4140)3.10 / 10,允许的类型是:

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:

  • the dynamic type of the object,
  • 对象的动态类型,

  • a cv-qualified version of the dynamic type of the object,
  • 一个cv限定版本的动态类型的对象,

  • a type similar (as defined in 4.4) to the dynamic type of the object,
  • 与对象的动态类型类似(如4.4中所定义)的类型,

  • a type that is the signed or unsigned type corresponding to the dynamic type of the object,
  • 与对象的动态类型对应的有符号或无符号类型的类型,

  • a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
  • 一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,

  • an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
  • 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),

  • a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
  • 一种类型,是对象的动态类型的(可能是cv限定的)基类类型,

  • a char or unsigned char type.
  • char或unsigned char类型。

"Similar types," as per 4.4, pertain to modifying cv-qualification of multi-level pointers.

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

So, if you've ever written into an area through a pointer (or other accessor) to one type, you cannot access it through a pointer to a different type (unless sanctioned by 3.10/10), even if you forget the old pointer.

所以,如果你曾经通过指针(或其他访问器)写入一个区域,你不能通过指向不同类型的指针访问它(除非3.10 / 10批准),即使你忘记了旧的指针。

If you've never written to an area through a particular type, casting pointers back and forth is not an issue.

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

#2


1  

The rule is:

规则是:

"Unless the pointers are of compatible types. You cannot have two pointers pointing to the same memory."

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

Here is a simpler example of an endless cycle:

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

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

This is essentially how the compiler will/can approach this code:

这基本上是编译器将如何处理此代码:

The test for some_buff[0] == 0 can be optimized out, because there is no valid way how the some_buff[0] could be changed. It is accessed through manipulator, but manipulator isn't of a compatible type, therefore according to the strict aliasing rule, the value of some_buff[0] cannot change.

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

If you want an even more simpler example:

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

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]);

It is perfectly OK for this code to always print zero and it doesn't matter what manipulate does.

这段代码始终打印为零是完全可以的,并且无论操作是什么都无关紧要。

#3


1  

My understanding is that any attempt to write through one of these will render the other pointer essentially invalid.

我的理解是,任何通过其中一个写入的尝试都会使另一个指针基本上无效。

As long as you don't access the type-punned pointer, the other, "official" one is ok. However, if you do that, it will cause undefined behavior, which may just work, do what you said or something out of this galaxy, including making the other pointer invalid. Compilers can treat UB at their pleasure.

只要你不访问类型惩罚指针,另一个,“官方”指针就可以了。但是,如果你这样做,它将导致未定义的行为,这可能只是工作,做你说的或这个星系的东西,包括使另一个指针无效。编译器可以随意对待UB。

The only way to make buff a valid pointer to Msg is memcpy/memmove, according to the standard:

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

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

Also, what triggers UB, is not only writing but also reading or whatever other way that accesses the object:

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

If a program attempts to access the stored value of an object through an lvalue of other than one of the following types the behavior is undefined:

如果程序试图通过不同于以下类型之一的左值访问对象的存储值,则行为未定义:

Some compilers also allow "suspending" that rule such as GCC, clang and ICC (probably also MSVC) but that cannot be considered portable or standard behavior. Further techniques, and their code generation analysis, are thoroughly analyzed here.

一些编译器还允许“暂停”该规则,例如GCC,clang和ICC(可能还有MSVC),但这不能被视为可移植或标准行为。此处详细分析了其他技术及其代码生成分析。

Do you really need to break the strict-aliasing rule?

Most of the times, no, you do not need that. There are ways and ways to overcome that problem that involve perfectly legal solutions. In the above case, simply store a plain pointer within the struct and send each member in a determined format.

大多数时候,不,你不需要那样做。有一些方法和方法可以克服涉及完全合法解决方案的问题。在上面的例子中,只需在结构中存储一个普通指针,并以确定的格式发送每个成员。

#4


0  

Your suggestion doesn't help at all, because it doesn't matter what value you assign your pointer variable after using it. You do access the same memory location through pointers of incompatible types.

您的建议完全没有帮助,因为使用它后指定变量的值是无关紧要的。您通过不兼容类型的指针访问相同的内存位置。

For C (not for C++), there is at least one safe thing to do other than avoiding type punning: You can safely cast pointers to structs, given that the one struct type just adds fields to the end of the other. This even works when the longer struct just contains the shorter as its first member: A pointer to a struct points to its first member. So e.g. these are safe in C:

对于C(不适用于C ++),除了避免类型惩罚之外,至少还有一个安全的做法:您可以安全地将指针转换为结构,假设一个结构类型只是将字段添加到另一个结尾。当更长的struct只包含short作为其第一个成员时,这甚至有效:指向struct的指针指向其第一个成员。所以例如这些在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;

#1


6  

C++ answer: that won't work. The C++ strict aliasing rule explicitly enumerates which types can be used to access an object. If you use a different type, you get UB, even if you've "retired" all access methods of a different type. As per C++14 (n4140) 3.10/10, the allowed types are:

C ++回答:那不行。 C ++严格别名规则显式枚举了可用于访问对象的类型。如果您使用其他类型,即使您已“退出”所有不同类型的访问方法,也会获得UB。根据C ++ 14(n4140)3.10 / 10,允许的类型是:

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:

  • the dynamic type of the object,
  • 对象的动态类型,

  • a cv-qualified version of the dynamic type of the object,
  • 一个cv限定版本的动态类型的对象,

  • a type similar (as defined in 4.4) to the dynamic type of the object,
  • 与对象的动态类型类似(如4.4中所定义)的类型,

  • a type that is the signed or unsigned type corresponding to the dynamic type of the object,
  • 与对象的动态类型对应的有符号或无符号类型的类型,

  • a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
  • 一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,

  • an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
  • 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),

  • a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
  • 一种类型,是对象的动态类型的(可能是cv限定的)基类类型,

  • a char or unsigned char type.
  • char或unsigned char类型。

"Similar types," as per 4.4, pertain to modifying cv-qualification of multi-level pointers.

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

So, if you've ever written into an area through a pointer (or other accessor) to one type, you cannot access it through a pointer to a different type (unless sanctioned by 3.10/10), even if you forget the old pointer.

所以,如果你曾经通过指针(或其他访问器)写入一个区域,你不能通过指向不同类型的指针访问它(除非3.10 / 10批准),即使你忘记了旧的指针。

If you've never written to an area through a particular type, casting pointers back and forth is not an issue.

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

#2


1  

The rule is:

规则是:

"Unless the pointers are of compatible types. You cannot have two pointers pointing to the same memory."

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

Here is a simpler example of an endless cycle:

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

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

This is essentially how the compiler will/can approach this code:

这基本上是编译器将如何处理此代码:

The test for some_buff[0] == 0 can be optimized out, because there is no valid way how the some_buff[0] could be changed. It is accessed through manipulator, but manipulator isn't of a compatible type, therefore according to the strict aliasing rule, the value of some_buff[0] cannot change.

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

If you want an even more simpler example:

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

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]);

It is perfectly OK for this code to always print zero and it doesn't matter what manipulate does.

这段代码始终打印为零是完全可以的,并且无论操作是什么都无关紧要。

#3


1  

My understanding is that any attempt to write through one of these will render the other pointer essentially invalid.

我的理解是,任何通过其中一个写入的尝试都会使另一个指针基本上无效。

As long as you don't access the type-punned pointer, the other, "official" one is ok. However, if you do that, it will cause undefined behavior, which may just work, do what you said or something out of this galaxy, including making the other pointer invalid. Compilers can treat UB at their pleasure.

只要你不访问类型惩罚指针,另一个,“官方”指针就可以了。但是,如果你这样做,它将导致未定义的行为,这可能只是工作,做你说的或这个星系的东西,包括使另一个指针无效。编译器可以随意对待UB。

The only way to make buff a valid pointer to Msg is memcpy/memmove, according to the standard:

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

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

Also, what triggers UB, is not only writing but also reading or whatever other way that accesses the object:

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

If a program attempts to access the stored value of an object through an lvalue of other than one of the following types the behavior is undefined:

如果程序试图通过不同于以下类型之一的左值访问对象的存储值,则行为未定义:

Some compilers also allow "suspending" that rule such as GCC, clang and ICC (probably also MSVC) but that cannot be considered portable or standard behavior. Further techniques, and their code generation analysis, are thoroughly analyzed here.

一些编译器还允许“暂停”该规则,例如GCC,clang和ICC(可能还有MSVC),但这不能被视为可移植或标准行为。此处详细分析了其他技术及其代码生成分析。

Do you really need to break the strict-aliasing rule?

Most of the times, no, you do not need that. There are ways and ways to overcome that problem that involve perfectly legal solutions. In the above case, simply store a plain pointer within the struct and send each member in a determined format.

大多数时候,不,你不需要那样做。有一些方法和方法可以克服涉及完全合法解决方案的问题。在上面的例子中,只需在结构中存储一个普通指针,并以确定的格式发送每个成员。

#4


0  

Your suggestion doesn't help at all, because it doesn't matter what value you assign your pointer variable after using it. You do access the same memory location through pointers of incompatible types.

您的建议完全没有帮助,因为使用它后指定变量的值是无关紧要的。您通过不兼容类型的指针访问相同的内存位置。

For C (not for C++), there is at least one safe thing to do other than avoiding type punning: You can safely cast pointers to structs, given that the one struct type just adds fields to the end of the other. This even works when the longer struct just contains the shorter as its first member: A pointer to a struct points to its first member. So e.g. these are safe in C:

对于C(不适用于C ++),除了避免类型惩罚之外,至少还有一个安全的做法:您可以安全地将指针转换为结构,假设一个结构类型只是将字段添加到另一个结尾。当更长的struct只包含short作为其第一个成员时,这甚至有效:指向struct的指针指向其第一个成员。所以例如这些在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;