提问



当询问C中常见的未定义行为时,灵魂比我提到的严格别名规则更加开明
他们在说什么?

最佳参考


遇到严格别名问题的典型情况是将结构(如设备/网络消息)覆盖到系统字大小的缓冲区(如指向uint32_t s或uint16_t s的指针) 。当您通过指针转换将结构体重叠到此类缓冲区或缓冲区到此类结构上时,您很容易违反严格的别名规则。


所以在这种设置中,如果我想发送消息,我必须有两个不兼容的指针指向同一块内存。我可能会天真地编写这样的代码:


typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // 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]);   
    }
}


严格别名规则使得此设置非法:取消引用指向不是兼容类型的对象或C 2011 6.5第7段 1 允许的其他类型之一的指针是未定义的行为。不幸的是,你仍然可以用这种方式编码,也许得到一些警告,让它编译得很好,只是在你运行代码时有奇怪的意外行为。 [111]


(海湾合作委员会在提出别名警告的能力方面似乎有些不一致,有时会给我们一个友好的警告,有时却没有。)


要了解为什么此行为未定义,我们必须考虑严格别名规则购买编译器的内容。基本上,根据这个规则,它不必考虑插入指令来刷新buff的每次循环运行的内容。相反,在优化时,对于一些恼人的非执行的别名假设,它可以省略那些指令,在循环运行之前将buff[0]buff[1]]加载到CPU寄存器中,并加速循环体。在引入严格别名之前,编译器必须处于偏执状态任何人都可以随时随地改变buff的内容。因此,为了获得额外的性能优势,并假设大多数人都没有打字 - 指针,引入了严格的别名规则。


请记住,如果您认为该示例是人为的,如果您将缓冲区传递给另一个为您执行发送的函数,则可能会发生这种情况。


void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}


并重写了我们之前的循环以利用这个方便的功能


for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}


编译器可能或可能不能够足够聪明地尝试内联SendMessage,它可能会或可能不会决定加载或不再加载buff。如果SendMessage是另一个单独编译的API的一部分,它可能有加载buff内容的指令。然后,也许你在C ++中,这是一些模板化的头只有编译器认为它可以内联的实现。或者它可能只是你为了自己的方便在.c文件中写的东西。无论如何,未定义的行为可能仍然存在。即使我们知道一些发生在幕后的事情,它仍然违反了规则,因此没有明确定义的行为得到保证。因此,只需通过包含一个函数来包含我们的单词分隔缓冲区并不一定有帮助。


那么如何解决这个问题?



  • 使用联盟。大多数编译器都支持这一点而不抱怨严格的别名。这在C99中是允许的,并且在C11中明确允许。


    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    

  • 您可以在编译器中禁用严格别名(g [[no-]]严格别名在gcc中))[112]

  • 您可以使用char*进行别名而不是系统的单词。规则允许char*的异常(包括signed charunsigned char)。它是总是假设char*别名其他类型。然而,这不会起到另一种作用:没有假设你的结构别名为chars的缓冲区。



初学者要小心


当两种类型相互叠加时,这只是一个潜在的雷区。您还应该了解字节顺序,单词对齐以及如何通过正确打包结构来处理对齐问题。[113] [114] [115]


脚注



1 C 2011 6.5 7允许左值访问的类型是:



  • 与对象的有效类型兼容的类型

  • 与对象的有效类型兼容的类型的限定版本,

  • 与对象的有效类型对应的有符号或无符号类型

  • 与有效类型的对象的限定版本对应的有符号或无符号类型的类型,

  • 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或

  • 字符类型。


其它参考1


我发现的最佳解释是Mike Acton,了解严格别名。它主要关注PS3开发,但那基本上只是GCC。[116]


来自文章:



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



所以基本上如果你int*指向某个包含int的内存,然后你将float*指向该内存并将其用作float则会违反规则。如果你的代码不尊重这个,那么编译器的优化器很可能会破坏你的代码。


规则的例外是char*,允许指向任何类型。

其它参考2


这是严格的别名规则,可以在 C ++ 03 标准的3.10节中找到(其他答案提供了很好的解释,但没有提供规则本身):



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

??
??

    ??
  • 对象的动态类型

  • ??
  • 对象的动态类型的cv限定版本,

  • ??
  • 与对象的动态类型对应的有符号或无符号类型的类型

  • ??
  • 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,

  • ??
  • 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),

  • ??
  • 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,

  • ??
  • a charunsigned char类型。

  • ??



C ++ 11 C ++ 14 措辞(强调更改):



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

??
??

    ??
  • 对象的动态类型

  • ??
  • 对象的动态类型的cv限定版本,

  • ??
  • 与对象的动态类型相似的类型(如4.4中所定义),

  • ??
  • 与对象的动态类型对应的有符号或无符号类型的类型

  • ??
  • 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,

  • ??
  • 聚合或联合类型,其元素或非静态数据成员中包含上述类型之一(包括递归地,元素或非静态数据成员子集合或包含的联合>,

  • ??
  • 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,

  • ??
  • a charunsigned char类型。

  • ??



两个更改很小: glvalue 而不是左值,以及聚合/联合案例的说明。


第三个更改提供了更强大的保证(放宽了强大的别名规则):类似类型的新概念现在可以安全地使用别名。





还有 C 措辞(C99; ISO/IEC 9899:1999 6.5/7;完全相同的措辞用于ISO/IEC 9899:2011§6.5?7):



??对象的存储值只能由左值访问
??表达式具有以下类型之一 73)或88):

??
??

    ??
  • 与对象的有效类型兼容的类型

  • ??
  • 与有效类型兼容的类型的合格版本
    ??对象,

  • ??
  • 一种类型,是与之对应的有符号或无符号类型
    ??有效的对象类型,

  • ??
  • 与a对应的有符号或无符号类型的类型
    ??对象的有效类型的合格版本,

  • ??
  • 包含上述之一的聚合或联合类型
    ??其成员之间的类型(包括,递归地,一个成员
    ??subaggregate或contains union),或

  • ??
  • 字符类型。

  • ??

??
?? 73)或88)此列表的目的是指定对象可能或可能不具有别名的情况。


其它参考3


严格的别名并不仅仅指向指针,它也会影响引用,我为boost开发人员维基写了一篇关于它的文章,它很受欢迎,我把它变成了我咨询网站上的一个页面。它完全解释了什么这就是为什么它让人们如此困惑以及如何处理它。严格的别名白皮书。特别是它解释了为什么工会是C ++的危险行为,以及为什么使用memcpy是C和C ++中唯一可移植的解决方案。希望这一点很有帮助。[117]

其它参考4


作为Doug T.已经写过的补遗,这里
是一个简单的测试用例,可能用gcc触发它:


check.c


#include 

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}


gcc -O2 -o check check.c编译。
通常(我尝试过的大多数gcc版本)都输出严格别名问题,因为编译器假定h不能与check函数中的k相同。因此,编译器优化if (*h == 5)并始终调用printf。


对于那些感兴趣的人是由gcc 4.6.3生成的x64汇编程序代码,在ubuntu 12.04.2 for x64上运行:


movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts


所以if条件完全从汇编代码中消失了。

其它参考5


通过指针强制转换输入惩罚(与使用联合相反)是打破严格别名的一个主要例子。[118]

其它参考6


根据C89的基本原理,标准的作者不希望要求编译器给出如下代码:


int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}


应该要求在赋值和返回语句之间重新加载x的值,以便p指向x的可能性和*p的赋值因此可能会改变x的价值。编译器应该有权假设在上述之类的情况下不会出现别名的概念是无争议的。


不幸的是,C89的作者以一种方式编写了他们的规则,如果按字面意思读取,即使以下函数调用未定义的行为:


void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}


因为它使用类型int的左值来访问struct S类型的对象,而int不属于可以用来访问struct S的类型。因为将结构和联合的非字符类型成员的所有使用视为未定义行为是荒谬的,几乎每个人都认识到至少在某些情况下可以使用一种类型的左值来访问另一种类型的对象。不幸的是,C标准委员会未能确定这些情况。


很多问题都是缺陷报告#028的结果,它报告了一个程序的行为,如:


int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}


缺陷报告#28指出程序调用未定义的行为,因为编写类型为double的union成员并读取类型为int的联合成员的操作会调用Implementation-Defined行为。这种推理是荒谬的,但却构成了有效类型规则的基础,这种规则不必要地使语言复杂化,同时无需解决原始问题。


解决原始问题的最佳方法可能是对待
关于规则目的的脚注,好像它是规范性的,并且已经制定
该规则不可执行,除非实际涉及使用别名的冲突访问。给出类似的东西:


 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }


inc_int中没有冲突,因为通过*p访问的存储的所有访问都是使用类型int的左值完成的,并且test中没有冲突,因为p明显地来自struct S,并且在下次使用s时,将通过p对该存储的所有访问都已经发生。


如果代码略有改变......


 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }


这里,p与标记行上s.x的访问之间存在别名冲突,因为在执行的那一点存在另一个将用于访问同一存储的引用 。


有缺陷报告028说原始示例调用UB是因为两个指针的创建和使用之间的重叠,这将使事情变得更加清晰,而不必添加有效类型或其他此类复杂性。

其它参考7


在阅读了许多答案之后,我觉得有必要添加一些东西:


严格别名(我将稍后描述)很重要,因为:



  1. 内存访问可能很昂贵(性能明智),这就是为什么数据在CPU寄存器中操作,然后再写回物理内存。

  2. 如果两个不同CPU寄存器中的数据被写入相同的存储空间,我们无法预测当我们用C编码时哪些数据将存活


    在汇编中,我们手动编写CPU寄存器的加载和卸载代码,我们将知道哪些数据保持不变。但是C(幸运的是)摘录了这个细节。



由于两个指针可以指向内存中的相同位置,因此可能会导致处理可能的冲突的复杂代码


这个额外的代码很慢并且会损害性能,因为它执行额外的内存读/写操作,这些操作既慢又可能(不太可能)。


严格别名规则允许我们在应安全的情况下避免冗余机器代码,以假设两个指针不指向同一个内存块(另请参阅restrict关键字)。


严格别名声明可以安全地假设指向不同类型的指针指向内存中的不同位置。


如果编译器注意到两个指针指向不同的类型(例如,int *float *),它将假设内存地址不同并且不会保护对内存地址冲突,导致更快的机器代码。


例如:


让我们假设以下功能:


void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}


为了处理a == b(两个指针指向同一个内存)的情况,我们需要命令和测试我们将数据从内存加载到CPU寄存器的方式,所以代码可能最终会像这样:



  1. 从内存加载ab

  2. a添加到b

  3. 保存 b重新加载 a


    (从CPU寄存器保存到存储器并从存储器加载到CPU寄存器)。

  4. b添加到a

  5. a(从CPU寄存器)保存到内存。



第3步非常慢,因为它需要访问物理内存。但是,需要防止ab指向相同内存地址的情况。


严格别名将允许我们通过告诉编译器这些存储器地址明显不同来防止这种情况(在这种情况下,这将允许甚至进一步优化,如果指针共享存储器地址则不能执行)。



  1. 这可以通过两种方式告诉编译器,使用不同的类型指向。即:


    void merge_two_numbers(int *a, long *b) {...}
    

  2. 使用restrict关键字。即:


    void merge_two_ints(int * restrict a, int * restrict b) {...}
    



现在,通过满足严格别名规则,可以避免步骤3,并且代码将以明显更快的速度运行。


事实上,通过添加restrict关键字,整个函数可以优化为:



  1. 从内存加载ab

  2. a添加到b

  3. 将结果保存到ab



由于可能的碰撞(其中ab将增加三倍而不是加倍,因此之前无法进行此优化。

其它参考8


严格别名不允许不同的指针类型指向相同的数据。


本文应该帮助您详细了解该问题。[119]

其它参考9


注意



这摘自我的什么是严格的别名规则以及我们为什么关心?写的。[120]


什么是严格别名?



在C和C ++中,别名与我们允许通过哪些表达式类型访问存储的值有关。在C和C ++中,标准指定允许哪些表达式类型为哪些类型设置别名。允许编译器和优化器假设我们严格遵循别名规则,因此术语严格别名规则。如果我们尝试使用不允许的类型访问值,则将其归类为未定义的行为( UB )。一旦我们有未定义的行为,所有赌注都将被取消,我们的计划结果将不再可靠。[121]


不幸的是,由于严格的别名违规,我们经常会得到我们期望的结果,而新的优化编译器的未来版本可能会破坏我们认为有效的代码。这是不可取的,理解严格的别名规则以及如何避免违反它们是一个值得的目标。


为了更多地了解我们关心的原因,我们将讨论在违反严格别名规则时出现的问题,因为类型惩罚中使用的常用技术经常违反严格的别名规则以及如何正确输入双关语,因此请输入惩罚。


初步示例



让我们看一些例子,然后我们可以准确地谈谈标准的含义,检查一些进一步的例子,然后看看如何避免严格的别名并捕捉我们错过的违规行为。这是一个不应该令人惊讶的例子(实例):[122]


int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";


我们有一个 int * 指向 int 占用的内存,这是一个有效的别名。优化器必须假设通过 ip 的分配可以更新 x 占用的值。


下一个示例显示了导致未定义行为的别名(实例):[123]


int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}


在函数 foo 中,我们采用 int * 和 float * ,在本例中我们调用 foo 并设置两个参数指向相同的内存位置,在本例中包含 int 。注意,reinterpret_cast告诉编译器将表达式视为具有由其template参数指定的类型。在这种情况下,我们告诉它将表达式& x 视为类型 float * 。我们可能会天真地期望第二个 cout 的结果为 0 但是使用 -O2 启用优化时,gcc和clang都会产生以下结果: [124]


0
1


这可能不是预期的,但完全有效,因为我们调用了未定义的行为。 float 无法有效地为 int 对象设置别名。因此,当解除引用 i 将是返回值时,优化程序可以假设存储常量1 ,因为通过 f 的商店无法有效地影响 int 对象。在Compiler Explorer中插入代码显示这正是发生的事情(实例):[125]


foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret


使用基于类型的别名分析(TBAA)的优化器假定将返回 1 并直接将常量值移动到带有返回值的寄存器 eax 。 TBAA使用语言规则来规定哪些类型允许别名来优化loads和商店。在这种情况下,TBAA知道 float 不能使用别名和 int 并优化 i 的负载。[126]


现在,到规则书



该标准究竟是什么意思我们被允许而且不允许这样做?标准语言并不简单,因此对于每个项目,我将尝试提供演示其含义的代码示例。


C11标准说什么?



C11 标准在 6.5表达式第7段部分中说明了以下内容:



??对象的存储值只能由具有以下类型之一的左值表达式访问: 88)
?? - 与对象的有效类型兼容的类型,



int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int



?? - 与对象的有效类型兼容的类型的限定版本,



int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int



?? - 对应于对象的有效类型的有符号或无符号类型,



int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object


gcc/clang有一个扩展名,并且允许将 unsigned int * 分配给 int * ,即使它们不是兼容的类型。[127] [128]



?? - 对应于对象有效类型的限定版本的有符号或无符号类型,



int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object



?? - 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或者



struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );



?? - 一个字符类型。



int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.


C ++ 17草案标准所说的



[[basic.lval]]第11段部分中的C ++ 17草案标准说:



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



void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object



??(11.2) - 对象的动态类型的cv限定版本,



int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x



??(11.3) - 与对象的动态类型类似(如7.5中所定义)的类型,

??
??(11.4) - 对应于对象的动态类型的有符号或无符号类型,



// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}



??(11.5) - 对应于对象动态类型的cv限定版本的有符号或无符号类型,



signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing



??(11.6) - 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),



struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 



??(11.7) - 一种类型,是对象的动态类型的(可能是cv限定的)基类类型,



struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}



??(11.8) - char,unsigned char或std :: byte类型。



int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}


值得注意 signed char 未包含在上面的列表中,这与 C 的明显区别是字符类型。


什么是Punning类型



我们已经达到了这一点,我们可能想知道,为什么我们要为别名?答案通常是类型双关语,通常使用的方法违反了严格的别名规则。


有时我们想绕开类型系统并将对象解释为不同的类型。这称为类型双关语,以将一段内存重新解释为另一种类型。 类型双关语对于希望访问要查看,传输或操作的对象的基础表示的任务非常有用。我们发现使用类型双关语的典型区域是编译器,序列化,网络代码等......


传统上,这是通过获取对象的地址,将其转换为我们想要将其重新解释为的类型的指针,然后访问该值,或者换句话说通过别名来实现的。例如:


int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast(&x) ;  // Not a valid aliasing

printf( “%f\n”, *fp ) ;


正如我们之前看到的,这不是一个有效的别名,所以我们正在调用未定义的行为。但是传统的编译器并没有利用严格的别名规则,而且这种类型的代码通常只是起作用,不幸的是开发人员习惯于这样做。类型惩罚的一种常见替代方法是通过联合,它在C中有效,但在C ++中未定义的行为(参见实例):[129]


union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member


这在C ++中无效,有些人认为联合的目的仅仅是为了实现变体类型,并且认为使用联合进行类型惩罚是一种滥用。


我们如何正确打字?



C和C ++中类型双关语的标准方法是 memcpy 。这可能看起来有点沉重,但优化器应该认识到使用 memcpy 进行类型双关语并优化它并生成一个寄存器来注册移动。例如,如果我们知道 int64_t 与 double 的大小相同:


static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message


我们可以使用 memcpy :


void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...


在足够的优化级别,任何体面的现代编译器都会为前面提到的 reinterpret_cast 方法或 union 方法生成与类型双关语相同的代码。检查生成的代码,我们看到它只使用寄存器mov(实时编译器资源管理器示例)。[130]


C ++ 20和bit_cast



在C ++ 20中,我们可以获得 bit_cast ,它提供了一种简单而安全的输入方式,也可以在constexpr上下文中使用。


以下是如何使用 bit_cast 将 unsigned int 输入 float 的示例,(请参见实时):[131]]]


std::cout << bit_cast(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)


在 To 和 From 类型不具有相同大小的情况下,它需要我们使用中间struct15。我们将使用包含 sizeof的结构(unsigned int)字符数组(假定4字节无符号整数)为 From 类型, unsigned int 为到类型:


struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast(f);

   result += foo( result );
 }

 return result ;
}


不幸的是,我们需要这种中间类型,但这是 bit_cast 的当前约束。


捕获严格的别名冲突



我们没有很多很好的工具来捕获C ++中的严格别名,我们的工具将会遇到一些严格的混叠违规情况以及一些未对齐的加载和存储的情况。


使用标记 -fstrict-aliasing -Wstrict-aliasing 的gcc可以捕获一些情况,但不是没有误报/否定。例如,以下情况将在gcc中生成警告(请参见实时):[132]


int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast(&a)));
printf("%i\n", j = *(reinterpret_cast(&f)));


虽然它不会抓住这个额外的案例(见现场):[133]


int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast(p)));


虽然clang允许这些标志,但它显然实际上并没有实现警告。


我们可以使用的另一个工具是ASan,它可以捕获未对齐的载荷和存储。虽然这些不是直接严格的别名冲突,但它们是严格别名冲突的常见结果。例如,使用 -fsanitize=address 使用clang构建时,以下情况将生成运行时错误


int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]


我推荐的最后一个工具是C ++特定的,并不是严格意义上的工具,而是编码练习,不允许使用C风格的演员表.Gcc和clang都会使用 -Wold-style-制作C风格演员表的诊断这将迫使任何未定义类型的双关语使用reinterpret_cast,一般来说,reinterpret_cast应该是进行更密切代码审查的标志。搜索代码库以进行reinterpret_cast以执行审计也更容易。


对于C,我们已经涵盖了所有工具,我们还有tis-interpreter,一个静态分析器,可以对C语言的大部分子集进行详尽的分析。鉴于前一个示例的C版本使用 -fstrict-aliasing 错过了一个案例(请参见实时)[134]


int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));


tis-interpeter能够捕获所有三个,下面的示例调用tis-kernal作为tis-interpreter(为简洁起见,编辑输出):


./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.


最后还有TySan,目前正在开发中。此清理程序在影子内存段中添加类型检查信息,并检查访问以查看它们是否违反了别名规则。该工具可能应该能够捕获所有别名违规,但可能会产生大量的运行时开销。[135]

其它参考10


从技术上讲,在C ++中,严格的别名规则可能永远不适用。


注意间接的定义(*运算符):[136]



??一元*运算符执行间接:它所表达的表达式
??应用应该是指向对象类型的指针,或指向a的指针
??函数类型和结果是引用对象的左值
??函数表达式指向



同样来自glvalue [137]的定义



??glvalue是一种表达式,其评估决定了它的身份
??一个对象,(...剪辑)



因此,在任何定义良好的程序跟踪中,glvalue指的是一个对象。 所以所谓的严格别名规则并不适用。这可能不是设计师想要的。