![]() |
1
514
遇到严格别名问题的典型情况是,将结构(如设备/网络消息)叠加到系统字大小的缓冲区(如指向
所以在这种设置中,如果我想向某个对象发送消息,就必须有两个不兼容的指针指向同一块内存。然后我可能天真地编写这样的代码:
严格的别名规则使此设置非法:取消引用对非 compatible type 或C 2011 6.5第7段允许的其他类型之一 一 是未定义的行为。不幸的是,您仍然可以这样编码, 也许吧 得到一些警告,让它编译得很好,只会在运行代码时出现奇怪的意外行为。 (GCC给出别名警告的能力似乎有些不一致,有时给出友好警告,有时则没有。)
为了了解这种行为为什么没有定义,我们必须考虑一下严格的别名规则购买编译器的原因。基本上,使用这个规则,不需要考虑插入指令来刷新
请记住,如果您认为该示例是人为的,那么即使您正在将缓冲区传递给另一个为您执行发送的函数,也可能会发生这种情况,如果您已经这样做了的话。
并重写我们之前的循环以利用这个方便的功能
编译器可能或可能无法或不够聪明地尝试内联sendmessage,并且可能或可能不会决定再次加载buff。如果
那我该怎么解决这个问题呢?
初学者谨防 当两种类型叠加在一起时,这只是一个潜在的雷区。你也应该了解 endianness , word alignment 以及如何通过 packing structs 正确地。 脚注一 C 2011 6.5 7允许左值访问的类型有:
|
![]() |
2
222
我发现最好的解释是迈克·阿克顿, Understanding Strict Aliasing . 它将重点放在PS3开发上,但这基本上只是GCC。 从文章中:
所以基本上如果你有
规则的例外是
|
![]() |
3
127
这是严格的别名规则,可在 C++ 03 标准(其他答案提供了很好的解释,但没有提供规则本身):
C++ 11 和 C++ 14 措辞(强调变更):
两个变化很小: GLUVE 而不是 左值 以及对合并案/工会案的澄清。 第三个变化提供了更强的保证(放宽强混叠规则):新概念 相似类型 现在可以安全地使用别名了。 也 C 措辞(C99;ISO/IEC 9899:1999 6.5/7;ISO/IEC 9899:2011_§6.5_¶7中使用的措辞完全相同):
|
![]() |
4
41
严格的别名不仅仅指指针,它也影响引用,我为BoostDeveloper wiki写了一篇关于它的文章,得到了很好的认可,我把它变成了我的咨询网站上的一个页面。它完全解释了它是什么,为什么它让人们如此困惑,以及如何处理它。 Strict Aliasing White Paper . 特别地,它解释了为什么工会是C++的冒险行为,以及为什么使用MeMCPY是C和C++之间唯一的可移植的解决方案。希望这是有帮助的。 |
![]() |
5
37
注释这是从我的 "What is the Strict Aliasing Rule and Why do we care?" 写起来。 什么是严格的别名?在C和C++中,混叠与我们允许通过哪些表达式类型访问存储的值有关。在C和C++中,标准指定允许哪些类型的别名属于哪种类型。编译器和优化器可以假定我们严格遵循别名规则,因此术语 严格别名规则 . 如果我们试图使用不允许的类型访问一个值,它被分类为 undefined behavior ( UB )一旦我们有了未定义的行为,所有的赌注都取消了,我们程序的结果就不再可靠了。 不幸的是,由于存在严格的别名冲突,我们通常会得到我们期望的结果,这就使得未来版本的编译器有了新的优化,可能会破坏我们认为有效的代码。这是不可取的,理解严格的别名规则以及如何避免违反它们是值得的目标。 为了了解更多我们关心的原因,我们将讨论违反严格混叠规则时出现的问题,因为在类型混叠中使用的常见技术经常违反严格混叠规则,以及如何正确键入混叠。 初步示例让我们看一些例子,然后我们可以确切地讨论标准所说的内容,检查一些进一步的例子,然后看看如何避免严格的别名和捕获我们遗漏的冲突。这是一个不应该令人惊讶的例子( live example ):
我们有一个 INT* 指向被 int 这是一个有效的别名。优化器必须假定通过 知识产权 无法更新占用的值 X . 下一个示例显示导致未定义行为的别名( live example ):
在函数中 福 我们采取了 INT* 和A 浮动* ,在这个例子中,我们称之为 福 并将这两个参数设置为指向相同的内存位置,在本例中,该位置包含 int . 注意, reinterpret_cast 指示编译器将表达式视为其模板参数指定的类型。在这种情况下,我们告诉它处理表达式 &X 好像它有类型 浮动* . 我们可能天真地期望第二个结果 咳嗽 成为 零 但如果启用了优化,则使用 -O2 GCC和Clang均产生以下结果:
这可能不是预期的,但完全有效,因为我们调用了未定义的行为。一 浮动 不能有效地别名an int 对象。因此,优化器可以假定 常数1 取消引用时存储 我 将是从存储到 f 不能有效地影响 int 对象。在编译器资源管理器中插入代码表明这正是发生的事情( live example ):
优化器使用 Type-Based Alias Analysis (TBAA) 假设 一 将返回并直接将常量值移入寄存器 埃克斯 带有返回值。tbaa使用有关允许别名的类型的语言规则来优化加载和存储。在这种情况下,TBAA知道 浮动 不能别名和 int 并优化了 我 . 现在,到规则手册标准究竟规定了我们被允许和不被允许做什么?标准语言并不简单,因此对于每一项,我都将尝试提供代码示例来演示其含义。 C11标准怎么说?这个 C11 标准在第节中说明了以下内容 6.5表达第7段 :
gcc/clang has an extension 和 also 允许分配 无符号INT* 到 INT* 即使它们是不兼容的类型。
C++ 17草案标准说什么C++ 17版标准草案 [基本等级]第11段 说:
值得注意的 符号字符 不包括在上面的列表中,这与 C 哪说 字符类型 . 什么是双关拳我们已经到了这一点,我们可能想知道,为什么我们要化名为?答案通常是 双关语 ,通常使用的方法违反了严格的别名规则。 有时我们想绕过类型系统,将对象解释为不同的类型。这叫做 类型双关 ,将内存段重新解释为另一种类型。 型双关 对于希望访问对象的基础表示形式以进行查看、传输或操作的任务很有用。我们发现使用的典型类型punning是编译器、序列化、网络代码等。 传统上,这是通过获取对象的地址,将其转换为我们想要重新解释的类型的指针,然后访问该值,或者换句话说,通过别名来实现的。例如:
正如我们前面看到的,这不是一个有效的别名,因此我们正在调用未定义的行为。但是传统的编译器并没有利用严格的别名规则,这种类型的代码通常只起作用,开发人员不幸地习惯了这种方式。对于类型punning,一种常见的替代方法是通过unions,它在c中有效,但是 未定义的行为 在C++中 see live example ):
这在C++中是无效的,有些人认为工会的目的仅仅是为了实现变体类型,并认为使用工会进行类型双关是一种滥用。 我们如何正确地输入双关语?标准方法 类型双关 C和C++都是 曼皮西 . 这看起来有点笨手笨脚,但优化器应该认识到 曼皮西 对于 类型双关 并对其进行优化,生成寄存器进行寄存器移动。例如,如果我们知道 英特64 和一样大小 双重的 :
我们可以使用 曼皮西 :
在充分的优化级别上,任何合适的现代编译器都会生成与前面提到的相同的代码 重新解释铸模 方法或 联盟 方法 类型双关 . 检查生成的代码,我们看到它只使用寄存器mov( live Compiler Explorer Example ) C++ 20与BITICAST CAST在C++ 20中我们可以获得 比特铸 ( implementation available in link from proposal )这为输入pun提供了一种简单而安全的方法,并且可以在constexpr上下文中使用。 以下是如何使用的示例 比特铸 键入双关语 无符号整型 到 浮动 , see it live ):
在这种情况下 到 和 从 类型没有相同的大小,它要求我们使用中间结构15。我们将使用包含 sizeof(无符号int) 字符数组( 假设4字节无符号整数 要成为 从 类型及 无符号整型 作为 到 类型:
不幸的是,我们需要这个中间类型,但这是 比特铸 . 捕获严格的别名冲突我们没有很多很好的工具来捕获C++中的严格混叠,我们所使用的工具将捕获一些严格的混叠违反和一些错误的负载和存储的情况。 GCC使用标志 -fstrict别名 和 -wstrict别名 可以捕捉到一些情况,尽管不是没有误报/否定。例如,以下情况将在GCC中生成警告( see it live ):
尽管它不能抓住这个额外的案子( see it live ):
尽管clang允许使用这些标志,但它显然不实现警告。 我们可以使用的另一个工具是asan,它可以捕获未对齐的加载和存储。虽然这些不是直接严格的别名冲突,但它们是严格的别名冲突的常见结果。例如,以下情况将在使用clang生成时生成运行时错误 -fsanitize=地址
我推荐的最后一个工具是C++专用的,不是严格的工具,而是编码实践,不允许C样式的转换。GCC和Clang都将使用 -沃尔德式铸造 . 这将强制任何未定义的类型pun使用reinterpret-cast,一般来说,reinterpret-cast应该是用于更近的代码检查的标志。在代码库中搜索重新解释代码以执行审计也更容易。 对于C语言,我们已经介绍了所有的工具,我们也有TIS解释器,它是一个静态分析器,可以详尽地分析C语言的一大个子集的程序。给出了前面示例的验证,其中使用 -fstrict别名 错过一个案例( see it live )
tis interpeter能够捕获这三个,下面的示例将tis kernal作为tis解释器调用(为简洁起见,编辑输出):
终于有了 TySan 目前正在开发中。这个消毒剂在阴影内存段中添加类型检查信息,并检查访问是否违反了别名规则。该工具可能能够捕获所有别名冲突,但可能会有很大的运行时开销。 |
![]() |
6
32
作为Doug T.已经写的附录,这里 是一个简单的测试用例,它可能会触发GCC: 检查C
编译用
对于那些在这里感兴趣的人来说,X64汇编程序代码,由GCC4.6.3生成,运行在Ubuntu 12.04.2上,用于X64:
所以if条件完全从汇编程序代码中消失了。 |
![]() |
7
15
Type punning via指针强制转换(而不是使用联合)是打破严格混叠的主要示例。 |
![]() |
8
12
根据C89的基本原理,该标准的作者不希望要求编译器提供如下代码:
需要重新加载
不幸的是,C89的作者以一种方式编写了他们的规则,如果从字面上看,甚至会使以下函数调用未定义的行为:
因为它使用类型的左值
大部分问题都是缺陷报告028的结果,该报告询问了程序的行为,例如:
缺陷报告28指出,程序调用未定义的行为,因为写入“double”类型的联合成员并读取“int”类型的联合成员的操作调用实现定义的行为。这种推理是无意义的,但形成了有效类型规则的基础,这些规则不必要地使语言复杂化,而不做任何事情来解决原始问题。 解决原始问题的最佳方法可能是 关于规则目的的脚注,好像它是规范性的,并且 规则不可执行,除非实际涉及使用别名的冲突访问。给出如下信息:
里面没有冲突
如果代码稍微改变了…
这里,在
如果缺陷报告028说,最初的例子调用UB是因为两个指针的创建和使用之间存在重叠,那么在不添加“有效类型”或其他此类复杂性的情况下,这会使事情变得更加清楚。 |
![]() |
9
9
在阅读了许多答案之后,我觉得有必要增加一些内容: 严格的别名(我将用一点描述) 很重要,因为 :
由于两个指针可以指向内存中的同一位置,这可能导致 处理可能的冲突的复杂代码 . 这个额外的代码很慢而且 影响性能 因为它执行额外的内存读/写操作,这既慢又(可能)不必要。
这个
严格的别名规则允许我们避免冗余的机器代码
在这种情况下
应该是
假设两个指针不指向同一内存块(另请参见
严格的别名声明可以安全地假定指向不同类型的指针指向内存中的不同位置。
如果编译器发现两个指针指向不同的类型(例如,
例如 : 让我们假设以下函数:
为了处理
步骤3非常慢,因为它需要访问物理内存。但是,它需要防止出现以下情况:
严格的别名允许我们通过告诉编译器这些内存地址明显不同来防止这种情况(在本例中,这将允许进一步优化,如果指针共享内存地址,则无法执行进一步优化)。
现在,通过满足严格的别名规则,可以避免步骤3,代码运行速度将显著加快。
实际上,通过添加
由于可能发生碰撞(在
|
![]() |
10
5
严格的别名不允许对同一数据使用不同的指针类型。 This article 应该有助于你全面了解这个问题。 |
![]() |
11
-2
在C++技术上,严格的混叠规则可能是不适用的。 注意间接性的定义( * operator ):
所以在任何定义良好的程序跟踪中,glvalue都引用一个对象。 所以所谓的严格别名规则永远都不适用。 这可能不是设计师想要的。 |
![]() |
MaPo · Linux,设置锁定ICMP_过滤器选项 5 月前 |
![]() |
Doohyeon Won · 内联函数上的奇怪现象?[关闭] 6 月前 |
![]() |
Bobby · 复合字面值总是左值吗? 6 月前 |
![]() |
9-Pin · C: 嵌套结构的堆栈内存分配 6 月前 |