什么是严格的混淆现象?

当问到C中常见的未定义行为时,人们有时会提到严格的混淆现象。他们在说什么?

270044 次浏览

严格的混淆现象是不允许不同的指针类型指向相同的数据。

这篇文章应该帮助您详细了解问题。

类型双关语通过指针转换(而不是使用联合)是打破严格混淆现象的一个主要例子。

我找到的最好的解释是Mike Acton,理解严格混叠。它有点专注于PS3开发,但基本上只是GCC。

来自文章:

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

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

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

你遇到严格混淆现象的典型情况是将结构(如设备/网络msg)覆盖到系统字长大小的缓冲区上(如指向uint32_tuint16_t的指针)。当你将结构覆盖到这样的缓冲区上,或者通过指针转换将缓冲区覆盖到这样的结构上时,你很容易违反严格的混淆现象。

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

typedef struct Msg{unsigned int a;unsigned int b;} Msg;
void SendWord(uint32_t);
int main(void){// Get a 32-bit buffer from the systemuint32_t* buff = malloc(sizeof(Msg));    
// Alias that buffer through messageMsg* msg = (Msg*)(buff);    
// Send a bunch of messagesfor (int i = 0; i < 10; ++i){msg->a = i;msg->b = i+1;SendWord(buff[0]);SendWord(buff[1]);}}

严格的混淆现象使得这种设置是非法的:取消引用一个指针,该指针别名的对象不是兼容类型或C 2011 6.5第71段允许的其他类型之一是未定义的行为。不幸的是,你仍然可以这样编码,也许得到一些警告,编译良好,只是在运行代码时出现奇怪的意外行为。

(海湾合作委员会在发出混淆现象警告的能力方面似乎有些不一致,有时会给我们一个友好的警告,有时不会。

要理解为什么这种行为是未定义的,我们必须考虑严格混淆现象规则给编译器带来了什么。基本上,有了这个规则,编译器就不必考虑每次循环运行都插入指令来刷新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)];};
  • 您可以在编译器中禁用严格混淆现象(gcc中的f[no-]严格混淆现象

  • 您可以使用char*代替系统的单词来表示混淆现象。规则允许char*(包括signed charunsigned char)例外。它总是假设char*别名其他类型。然而,这不会以其他方式工作:没有假设您的结构别名缓冲区的字符。

初学者当心

当两种类型相互叠加时,这只是一个潜在的雷区。你还应该学习endianness字对齐,以及如何正确处理通过填料结构的对齐问题。

脚注

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

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 与对象的有效类型对应的有符号或无符号类型的类型,
  • 对应于对象有效类型的限定版本的有符号或无符号类型的类型,
  • 在其成员中包含上述类型之一的聚合或联合类型(包括递归地,子聚合或包含联合的成员),或
  • 字符类型。

严格混淆现象不仅指的是指针,它也会影响引用,我写了一篇关于它的论文,用于提升开发人员wiki,它很受欢迎,我把它变成了我的咨询网站上的一个页面。它完全解释了它是什么,为什么它让人如此困惑,以及该怎么办。严格混叠白皮书。特别是它解释了为什么工会对C++来说是危险的行为,为什么使用memcpy是唯一可以跨C和C++移植的修复程序。希望这对你有帮助。

这是严格的混淆现象,见于C++03标准的第3.10节(其他答案提供了很好的解释,但没有提供规则本身):

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

  • 对象的动态类型,
  • 对象的动态类型的cv限定版本,
  • 对应于对象动态类型的有符号或无符号类型的类型,
  • 对应于对象动态类型的cv限定版本的有符号或无符号类型的类型,
  • 在其成员中包含上述类型之一的聚合或联合类型(包括递归地,子聚合或包含联合的成员),
  • 是对象的动态类型的(可能是cv限定的)基类类型的类型,
  • charunsigned char类型。

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

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

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

两个变化很小:glvalue而不是左值,以及对聚合/联合案例的澄清。

第三个变化提供了更强的保证(放松了强混淆现象):类似类型的新概念现在可以安全地使用别名。


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

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

  • 与对象的有效类型兼容的类型,
  • 与有效类型兼容的类型的限定版本对象,
  • 对应的有符号或无符号类型的类型对象的有效类型,
  • 对应于有符号或无符号类型的类型对象的有效类型的限定版本,
  • 包含上述之一的聚合或联合类型其成员之间的类型(递归地包括子聚合或包含联合),或
  • 字符类型。

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

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

check. c

#include <stdio.h>
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。

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

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

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

根据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指出,程序调用未定义的行为是因为编写类型为“双”的联合成员和读取类型为“int”的联合成员的操作调用了实现定义的行为。这种推理是荒谬的,但它构成了有效类型规则的基础,这些规则不必要地使语言复杂化,同时对解决原始问题什么也不做。

解决原始问题的最佳方法可能是处理关于该规则的目的的脚注,就好像它是规范的一样该规则不可执行,除非实际涉及使用别名的冲突访问。给定如下:

 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,这将使事情变得更加清晰,而无需添加“有效类型”或其他类似的复杂性。

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

严格的混淆现象(我稍后会描述)很重要,因为

  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. Using the restrict keyword. i.e.:

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

Now, by satisfying the Strict Aliasing rule, step 3 can be avoided and the code will run significantly faster.

In fact, by adding the restrict keyword, the whole function could be optimized to:

  1. load a and b from memory.

  2. add a to b.

  3. save result both to a and to b.

This optimization couldn't have been done before, because of the possible collision (where a and b would be tripled instead of doubled).

说明

这是摘自我的“什么是严格的混叠规则,我们为什么要关心?”文章。

什么是严格混淆现象?

在C和C++混淆现象与我们被允许访问存储值的表达式类型有关。在C和C++标准都规定了允许哪些表达式类型别名哪些类型。编译器和优化器被允许假设我们严格遵守混淆现象规则,因此术语严格混淆现象。如果我们试图使用不允许的类型访问一个值,它被归类为未定义行为UB)。一旦我们有未定义的行为,所有赌注都取消了,我们程序的结果不再可靠。

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

为了更多地了解我们为什么关心,我们将讨论违反严格混淆现象时出现的问题,类型双关语,因为类型双关语中使用的常见技术经常违反严格的混淆现象规则以及如何正确键入双关语。

初步例子

让我们看一些例子,然后我们可以确切地谈论标准的内容,检查一些进一步的例子,然后看看如何避免严格的混淆现象并发现我们错过的违规行为。这是一个不应该令人惊讶的例子(实例):

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

我们有一个int*指向int占用的内存,这是一个有效的混淆现象。优化器必须假设通过ip的赋值可以更新x占用的值。

下一个示例显示导致未定义行为的混淆现象(实例):

int foo( float *f, int *i ) {*i = 1;*f = 0.f;    
return *i;}
int main() {int x = 0;    
std::cout << x << "\n";   // Expect 0x = foo(reinterpret_cast<float*>(&x), &x);std::cout << x << "\n";   // Expect 0?}

在函数foo中,我们取一个int*和一个浮动*,在这个例子中,我们调用foo并将两个参数设置为指向同一个内存位置,在这个例子中,这个内存位置包含一个int。注意,reinterpret_cast告诉编译器对待表达式就像它具有模板参数指定的类型一样。在这种情况下,我们告诉它对待表达式&x就像它具有类型浮动*一样。我们可能天真地期望第二个cout的结果是,但是使用int*0启用了优化,gcc和clang都会产生以下结果:

01

这可能出乎意料,但由于我们调用了未定义的行为,因此完全有效。浮动不能有效地别名int对象。因此,优化器可以假设在取消引用时存储的常数1将是返回值,因为通过f的存储不能有效地影响int对象。在编译器资源管理器中插入代码表明这正是正在发生的事情(实例):

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

使用基于类型的别名分析(TBAA)的优化器假设将返回1并直接将常量值移动到携带返回值的寄存器eax中。TBAA使用关于允许哪些类型别名的语言规则来优化加载和存储。在这种情况下,TBAA知道浮动不能别名int并优化了的负载。

现在,到规则书

标准究竟说我们被允许和不被允许做什么?标准语言并不简单,所以对于每个项目,我将尝试提供演示含义的代码示例。

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有一个扩展允许分配无符号整型*int*,即使它们不是兼容的类型。

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

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 version 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 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标准草案怎么说

C++17标准草案第0节说:

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

(11.1)-对象的动态类型,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an objectint *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to intstd::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 assumptionint 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,无符号char或std::byte类型。

int foo( std::byte &b, uint32_t &ui ) {b = static_cast<std::byte>('a');ui = 0xFFFFFFFF;  
return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias// an object of type uint32_t}

值得注意的是有符号字符不包括在上面的列表中,这与C一种字符类型有显着区别。

什么是双关语

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

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

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

int x = 1;
// In Cfloat *fp = (float*)&x;  // Not a valid aliasing
// In C++float *fp = reinterpret_cast<float*>(&x);  // Not a valid aliasing
printf( "%f\n", *fp );

正如我们之前看到的,这不是一个有效的混淆现象,所以我们调用了未定义的行为。但传统上编译器没有利用严格的混淆现象规则,这种类型的代码通常只是工作,不幸的是,开发人员已经习惯了这样做。类型双关语的常见替代方法是通过联合,这在C中有效,但在C++中未定义行为查看实时示例):

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双倍的大小相同:

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方法或双关语联盟方法相同的代码。检查生成的代码,我们看到它只使用寄存器mov(实时编译器资源管理器示例)。

C++20和bit_cast

在C++20中,我们可能会获得bit_cast在提案的链接中提供实施),它提供了一种简单而安全的键入双关语的方法,并且可以在Constexr上下文中使用。

以下是如何使用bit_cast键入双关语a无符号整型浮动看到它活着)的示例:

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

类型大小不同的情况下,我们需要使用一个中间结构15。我们将使用一个包含sizeof(无符号整型)字符数组(假设4字节无符号int)的结构作为类型,无符号整型作为类型。:

struct uint_chars {unsigned char arr[sizeof( unsigned int )] = {};  // Assume sizeof( unsigned int ) == 4};
// Assume len is a multiple of 4int 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<unsigned int>(f);
result += foo( result );}
return result;}

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

捕捉严格的混叠违规行为

我们没有很多好的工具来捕捉C++中的严格混淆现象,我们拥有的工具会捕捉一些严格混淆现象和一些不对齐的加载和存储情况。

使用标志-混淆现象-混淆现象的gcc可以捕获一些情况,尽管并非没有误报/阴性。例如,以下情况将在gcc(看到它活着)中生成警告:

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<short*>(&a)));printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

虽然它不会捕获这个额外的案例(看到它活着):

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

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

我们可以使用的另一个工具是ASan,它可以捕获未对齐的加载和存储。虽然这些不是直接的严格混淆现象,但它们是严格混淆现象的常见结果。例如,使用-fsanitize=地址使用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风格的演员对C风格的转换进行诊断。这将强制任何未定义的类型双关语使用reinterpret_cast,一般来说reinterpret_cast应该是一个标志,用于更仔细的代码审查。在代码库中搜索reinterpret_cast以执行审计也更容易。

对于C,我们已经涵盖了所有工具,并且我们还有tis解释器,这是一种静态分析器,可以详尽地分析C语言的大部分子集的程序。给定前面示例的C版本,其中使用-混淆现象错过了一个案例(看到它活着

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-内核作为tis-解释器(为简洁起见编辑了输出):

./bin/tis-kernel -sa example1.c...example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasingrules 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 byaccessing 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 byaccessing a cell with effective type int.

最后是目前正在开发中的TySan。此清理程序在影子内存段中添加类型检查信息并检查访问以查看它们是否违反混淆现象。该工具可能能够捕获所有混淆现象,但可能有很大的运行时开销。

从技术上C++,严格的混淆现象可能永远不会适用。

注意间接的定义(*操作员):

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

也来自价值的定义

glvalue是一个表达式,它的计算决定了一个对象,(…剪切)

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