我知道C++中的一个"未定义的行为"几乎可以让编译器做任何它想做的事情。但是,我发生了一次使我吃惊的崩溃,因为我认为代码是足够安全的。
在这种情况下,真正的问题只发生在使用特定编译器的特定平台上,并且仅在启用了优化的情况下发生。
为了重现这个问题并最大限度地简化它,我尝试了几个方法。这里是一个名为Serialize的函数的提取,它将接受一个bool参数,并将字符串true或false复制到现有的目标缓冲区。
如果bool参数是一个未初始化的值,那么这个函数是否在代码检查中,就无法判断它是否可能崩溃?
1 2 3 4 5 6 7 8 9 10 11 12 13
| // Zero-filled global buffer of 16 characters
char destBuffer[16];
void Serialize(bool boolValue) {
// Determine which string to print based on boolValue
const char* whichString = boolValue ?"true" :"false";
// Compute the length of the string we selected
const size_t len = strlen(whichString);
// Copy string into destination buffer, which is zero-filled (thus already null-terminated)
memcpy(destBuffer, whichString, len);
} |
如果使用clang 5.0.0+优化执行此代码,它将/可能崩溃。
预期的三元运算符boolValue ?"true" :"false"对我来说已经足够安全了,我假设,"boolValue中的垃圾值无关紧要,因为它无论如何都将被评估为真或假。"
我已经设置了一个编译器资源管理器示例,显示了反汇编中的问题,这里是完整的示例。注意:为了重新解决这个问题,我发现有效的组合是使用clang 5.0.0和-o2优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream>
#include <cstring>
// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
bool uninitializedBool;
__attribute__ ((noinline)) // Note: the constructor must be declared noinline to trigger the problem
FStruct() {};
};
char destBuffer[16];
// Small utility function that allocates and returns a string"true" or"false" depending on the value of the parameter
void Serialize(bool boolValue) {
// Determine which string to print depending if 'boolValue' is evaluated as true or false
const char* whichString = boolValue ?"true" :"false";
// Compute the length of the string we selected
size_t len = strlen(whichString);
memcpy(destBuffer, whichString, len);
}
int main()
{
// Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
FStruct structInstance;
// Output"true" or"false" to stdout
Serialize(structInstance.uninitializedBool);
return 0;
} |
这个问题是由于优化器引起的:它非常聪明,可以推断字符串"true"和"false"的长度只相差1。因此,它不是真正计算长度,而是使用bool本身的值,从技术上讲,它应该是0或1,如下所示:
1 2
| const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue; // clang clever optimization |
虽然这是"聪明的",可以说,我的问题是:C++标准是否允许编译器假设BoL只能具有"0"或"1"的内部数字表示,并以这样的方式使用它?
或者,这是一个定义了实现的情况,在这种情况下,实现假定其所有的bools都只包含0或1,而任何其他值都是未定义的行为领域?
- 这是个很好的问题。这是一个很好的例子,说明未定义行为不仅仅是一个理论问题。当人们说任何事情都可能因UB而发生时,"任何事情"真的会令人惊讶。有人可能会假设未定义的行为仍然以可预测的方式表现,但现在使用的现代优化器根本不是这样。OP花了些时间创建了一个MCVE,彻底调查了问题,检查了拆卸过程,并提出了一个明确、直接的问题。无法要求更多。
- 注意,"非零计算为true"的要求是关于布尔运算的规则,包括"赋值给bool"(根据具体情况,可能隐式调用static_cast())。但是,对于编译器选择的bool的内部表示形式,这不是一项要求。
- 评论不用于扩展讨论;此对话已被移动到聊天。
- 在一个非常相关的注释中,这是二进制不兼容的"有趣"来源。如果在调用函数之前有一个abi a值为零的pad s,但编译函数时假定参数为零填充,而abi b值则相反(不为零填充,但不假定参数为零填充),那么它将主要起作用,但使用b abi的函数如果使用a abi调用函数时会导致问题。采用"small"参数。iirc你在x86上有这个,有clang和icc。
- @TLW:虽然标准不要求实现提供任何调用或被外部代码调用的方法,但是如果有一种方法可以为相关的实现指定这样的内容,这会很有帮助(如果这些细节不相关的实现可以忽略这些属性)。
- 在实施过程中,除了根据"假设"规则影响外部可见的效果之外,讨论允许某些事情发生的标准是没有意义的。该标准是从文本到效果序列集映射的规范。(地图领域中的文本被称为具有定义的行为。)
- @菲利普斯:问题是什么构成了"效应"。我建议"调用一个名为foo的外部函数,它接受一个bool和一个int,但希望小的参数被调用者扩大到一个int,这应该是一个"效果",其行为将是做任何命名函数发生的事情。标准不需要关心这些函数的影响细节,除了实现可以通过实际执行指定的函数调用来满足其义务之外。
- @我不明白你的意思。我所说的"效果"是指构成"可观察行为"的那些术语,即通过"假设"规则来描述语义的技术术语。它与实施无关,只是实施必须影响效果。调用无效。调用是一种语法,在执行抽象机时具有相应的步骤,但该步骤是不可观察的行为,并且不必与执行过程中的任何内容对应。
- @Philipxy:我的观点是,当源文本中不存在具有特定名称的函数时,使用特定调用约定调用具有特定名称的函数的行为应该被视为支持外部函数调用并可以定位具有该名称的函数的实现的"可观察行为"。大多数C程序(包括几乎所有用于独立实现的程序)都依赖于"抽象机"与标准管辖范围之外的事物进行接口的能力。标准不需要指定外部事物是如何工作的…
- @超级卫星"应该"不是"是"。"您的观点":您仍然不会将您的评论与我的原始评论联系起来,因为我的原始评论正确地告诉提问者,他们对标准所说的和程序语义实际相关的内容有误解。当然,人们可以问"内部"的其他概念(彼得·卡德斯的回答),但目前的问题缺乏对更基本概念的理解——首先,必须理解抽象机器和假设规则。
- 如果bool在堆栈上,则很有可能,但堆栈指针显示为无法访问的页。虽然它是一个CPU架构的东西,而不是C++的东西。
- 是的,因为有些平台有一个内存地址的位标志,它将其指定为未初始化,读取未初始化的内存将异常终止您的程序。UB(未定义的行为)是危险的。它可能会破坏你的程序,或者更糟…看起来工作正常。它甚至可以摧毁地球。这很糟糕,因为我把所有的东西都放在那里。
是的,ISO C++允许(但不需要)实现做出这种选择。
但是请注意,如果程序遇到UB,则ISO C++允许编译器发出故意崩溃的代码(例如,用非法指令),例如帮助您发现错误。(或者因为它是一个死亡站9000。严格遵守是不足以实现C++实现对任何实际目的有用的。因此,ISO C++将允许编译器使ASM崩溃(因为完全不同的原因),即使在读取未初始化EDCOX1×0的类似代码的情况下。即使这需要是一个没有陷阱表示的固定布局类型。好的。
这是一个关于实际实现如何工作的有趣问题,但是请记住,即使答案不同,您的代码仍然是不安全的,因为现代C++不是汇编语言的便携版本。好的。
您正在为x86-64 System v ABI编译,它指定作为寄存器中函数arg的bool由寄存器1的低位8位中的位模式false=0和true=1表示。在内存中,bool是一种1字节类型,它必须具有0或1的整数值。好的。
(ABI是一组实现选项,由同一平台的编译器商定,以便他们可以生成调用彼此函数的代码,包括类型大小、结构布局规则和调用约定。)好的。
ISO C++没有指定它,但是这个ABI决策是广泛的,因为它使布尔-int转换便宜(只是零扩展)。我不知道有任何ABI不允许编译器为任何体系结构(不仅仅是x86)假设bool为0或1。它允许像!mybool和xor eax,1这样的优化翻转低位:在单CPU指令中,任何可能在0到1之间翻转位/整数/bool的代码。或将a&&b编译为按位和按bool类型。有些编译器确实在编译器中利用布尔值作为8位。他们的行动效率低下吗?.好的。
一般来说,IAF规则允许编译器利用在编译的目标平台上真实的东西,因为最终结果将是实现与C++源相同的外部可见行为的可执行代码。(带有未定义行为的所有限制,实际上是"外部可见的":不是用调试器,而是来自一个格式良好/合法C++程序中的另一个线程。)好的。
编译器绝对可以充分利用其代码生成中的ABI保证,并使代码像您发现的那样优化strlen(whichString)到江户十一〔11〕。(顺便说一句,这种优化有点聪明,但与分支和内联memcpy相比,它可能是短视的,因为它存储即时数据2。)好的。
或者编译器可以创建一个指针表,并用EDOCX1的整数值(0)对其进行索引,同样假定它是0或1。(这种可能性是@barmar的答案所建议的。)好的。
启用了优化的__attribute((noinline))构造函数导致clang只从堆栈中加载一个字节用作uninitializedBool。它为main中的对象腾出了空间(push rax比sub rsp, 8小,而且由于各种原因,效率与sub rsp, 8一样高),所以进入main时的所有垃圾都是用于uninitializedBool的价值。这就是为什么你得到的值不仅仅是0。好的。
5U - random garbage可以很容易地包装成一个大的无符号值,从而导致memcpy进入未映射的内存。目的地在静态存储中,而不是堆栈中,因此您不会覆盖返回地址或其他内容。好的。
其他实现可以做出不同的选择,例如false=0和true=any non-zero value。那么,clang可能不会使这个特定的ub实例的代码崩溃。(但如果愿意的话,它仍然是允许的。)我不知道任何其他的选择,比如X86—64对EDCOX1的0度的选择,但是C++标准允许很多人在硬件上不做任何事情,甚至不想在当前的CPU上做任何事情。好的。
当你检查或修改一个EDCOX1(0)项的对象表示时,ISO C++就不知道你会发现什么。(例如,通过memcpy将bool转换成unsigned char,您可以这样做,因为char*可以对任何事物进行别名。并且EDCOX1的16保证不具有填充位,因此C++标准在没有任何UB的情况下,正式地让您进行HOXDIP对象表示。复制对象表示形式的指针转换与分配char foo = my_bool是不同的,因此不会发生对0或1的布尔化,您将得到原始对象表示形式。)好的。
您已经将这个执行路径上的ub部分"隐藏"在使用noinline的编译器中。即使它没有内联,但是,过程间优化仍然可以生成依赖于另一个函数定义的函数版本。(首先,clang正在生成一个可执行文件,而不是一个可以发生符号插入的Unix共享库。第二,class{}定义中的定义,因此所有翻译单元必须具有相同的定义。就像使用inline关键字。)好的。
因此,编译器可以只发出一个ret或ud2(非法指令)作为main的定义,因为从main顶部开始的执行路径不可避免地遇到未定义的行为。(如果编译器决定通过非内联构造函数遵循路径,则在编译时可以看到。)好的。
任何遇到ub的程序对于其整个存在都是完全不定义的。但是在一个从未实际运行的函数或if()分支中的ub不会破坏程序的其余部分。在实践中,这意味着编译器可以决定为整个基本块发出一条非法指令或一个ret,或者不发出任何内容,并进入下一个块/函数,在编译时可以证明这些基本块包含或导致ub。好的。
实际上,gcc和clang有时确实会在ub上发出ud2,而不是试图为毫无意义的执行路径生成代码。或者,对于从非void函数的末尾脱落的情况,gcc有时会省略ret指令。如果你在想"我的功能将随rax中的垃圾一起返回",那你就大错特错了。现代C++编译器不再把语言当作一种可移植的汇编语言来对待。您的程序必须是有效的C++,而不必假设ASM中函数的独立非内联版本如何看起来。好的。
另一个有趣的例子是,为什么对mmap'ed内存的未对齐访问有时会在amd64上出现segfault?.x86不会在未对齐的整数上出错,对吗?那么,为什么失调的uint16_t*会是一个问题?因为alignof(uint16_t) == 2违反了这一假设,在使用sse2进行自动矢量化时会导致segfault。好的。
另请参阅每个C程序员应该知道的关于未定义行为的内容1/3,一篇由Clang开发人员撰写的文章。好的。关键点:如果编译器在编译时注意到ub,它可能会"中断"(发出令人惊讶的asm)导致ub的代码路径,即使目标是任何位模式都是bool的有效对象表示形式的ABI。
期望程序员对许多错误完全敌视,尤其是现代编译器警告的事情。这就是为什么您应该使用-Wall并修复警告的原因。C++不是一种用户友好的语言,C++中的某些东西即使在编译的目标上是安全的,也可能是不安全的。(例如,在C++中签名溢出是UB,编译器会假设它不会发生,即使编译2的补码x86,除非您使用EDCOX1×9)。好的。
编译时可见的ub总是很危险的,很难确定(通过链接时间优化)您是否真的从编译器中隐藏了ub,从而可以推断出它将生成哪种类型的asm。好的。
不要过于戏剧化;编译器通常会让您摆脱一些事情,并像在UB中一样发出您所期望的代码。但是,如果编译器开发人员实现一些优化以获得有关值范围的更多信息(例如变量为非负,可能允许它在x86-64上将符号扩展优化为自由零扩展),那么将来可能会出现问题。例如,在当前的GCC和CLANG中,执行tmp = a+INT_MIN并不能像往常那样优化a<0,只是tmp总是负数。(因此,它们不会从计算的输入中回溯以获得范围信息,而只是基于没有符号溢出的假设的结果:godbolt上的示例。我不知道这是有意的用户友好还是只是错过了优化。)好的。
还要注意,实现(AKA编译器)允许定义ISO C++留下未定义的行为。例如,支持英特尔内部的所有编译器(如EDCOX1对手动SIMD矢量化3)必须允许形成错误对齐的指针,即使是在不引用它们的情况下,也可以是C++中的UB。__m128i _mm_loadu_si128(const __m128i *)通过采用未对准的__m128i*arg而不是void*或char*来进行未对准负载。硬件向量指针和相应类型之间的"reinterpret"转换是否为未定义的行为?好的。
GNU C/C++还定义了左移位负符号数的行为(即使没有EDCOX1〔8〕),也与正常签名的溢出UB规则分开。(这是在ISO C++中的UB,而符号数的右移是实现定义的(逻辑与算术);好的质量实现在算术右移的HW上选择算术,但ISO C++没有指定)。这一点记录在GCC手册的整数部分,同时定义了C标准要求实现以某种方式定义的实现定义行为。好的。
编译器开发者肯定关心的是实现质量问题;他们通常不想让有意恶意的编译器,但是利用C++中的所有UB坑(除了它们选择定义的)来更好地优化,有时几乎是不可区分的。好的。
脚注1:上面的56位可能是垃圾,被调用方必须忽略,对于比寄存器窄的类型,这是正常的。好的。
(其他ABI在这里做了不同的选择。有些函数在传递给mips64和powerpc64等函数或从函数返回时,需要将窄整数类型扩展为零或符号来填充寄存器。请参阅此x86-64答案的最后一节,该部分将与以前的ISA进行比较。)好的。
例如,在调用bool_func(a&1)之前,调用方可能已经计算了RDI中的a & 0x01010101,并将其用于其他用途。调用者可以对&1进行优化,因为它已经将其作为and edi, 0x01010101的一部分对低字节进行了优化,并且它知道被调用者需要忽略高字节。好的。
或者,如果一个bool作为第三个参数传递,可能是一个优化代码大小的调用者用mov dl, [mem]而不是movzx edx, [mem]加载它,以牺牲对RDX旧值的错误依赖(或其他部分寄存器效果,取决于CPU模型)为代价,节省了1个字节。或者对于第一个arg,mov dil, byte [r10]而不是movzx edi, byte [r10],因为两者都需要rex前缀。好的。
这就是为什么clang在Serialize中发射movzx eax, dil而不是sub eax, edi的原因。(对于integer参数,clang违反了ABI规则,而取决于gcc的未记录行为和clang为零-或符号将窄整数扩展到32位。为x86-64 ABI的指针添加32位偏移量时是否需要符号或零扩展?所以我很感兴趣的是,它对bool没有同样的作用。)好的。
脚注2:在分支之后,您只需要一个4字节的mov立即数,或者一个4字节+1字节的存储。长度隐含在存储宽度+偏移中。好的。
另外,glibc memcpy将执行两个4字节的加载/存储,其重叠部分取决于长度,因此这确实会使整个过程不受布尔值上的条件分支的影响。见glibc的memcpy/memmove中的L(between_4_7):块。或者至少,对memcpy分支中的布尔值使用相同的方法来选择块大小。好的。
如果是内联,则可以使用2x mov—immediate+cmov和条件偏移量,也可以将字符串数据保留在内存中。好的。
或者,如果针对Intel Ice Lake进行调优(具有快速的短rep mov功能),实际的rep movsb可能是最佳的。glibc memcpy可能会开始在具有该功能的CPU上使用小尺寸的rep movsb,从而节省了大量分支。好的。用于检测ub和使用未初始化值的工具
在gcc和clang中,可以使用-fsanitize=undefined编译以添加运行时检测,它将在运行时发生的ub上发出警告或出错。不过,这不会捕获统一的变量。(因为它不会增加类型大小来为"未初始化"位腾出空间)。好的。
参见https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/好的。
要查找未初始化数据的用法,clang/llvm中有地址消毒剂和内存消毒剂。https://github.com/google/sanitizers/wiki/memorysanitizer显示了clang -fsanitize=memory -fPIE -pie检测未初始化内存读取的示例。如果在没有优化的情况下编译,它可能会工作得最好,因此对变量的所有读取最终都会从ASM中的内存中实际加载。他们显示,在负载无法优化的情况下,它被用于-O2。我自己也没试过。(在某些情况下,例如在求和数组之前不初始化累加器,clang-o3将发出代码,这些代码求和到一个它从未初始化过的向量寄存器中。因此,通过优化,您可以得到一个没有与UB相关联的内存读取的情况。但-fsanitize=memory会更改生成的asm,并可能导致对此进行检查。)好的。
It will tolerate copying of uninitialized memory, and also simple logic and arithmetic operations with it. In general, MemorySanitizer silently tracks the spread of uninitialized data in memory, and reports a warning when a code branch is taken (or not taken) depending on an uninitialized value.
Ok.
MemorySanitizer implements a subset of functionality found in Valgrind (Memcheck tool).
Ok.
对于这种情况,它应该有效,因为从未初始化的内存计算对glibc memcpy的调用(在库内)将导致基于length的分支。如果它内联了一个完全没有分支的版本,只使用了cmov、索引和两个存储,那么它可能不会工作。好的。
Valgrind的memcheck也会寻找这种问题,如果程序只是复制未初始化的数据,那么它也不会抱怨。但它表示,它将检测"条件跳转或移动何时依赖于未初始化的值",以尝试捕获依赖于未初始化数据的任何外部可见行为。好的。
也许不标记负载的背后的想法是,结构可以有填充,并且用宽向量加载/存储复制整个结构(包括填充)不是一个错误,即使单个成员一次只写一个。在asm级别,有关填充内容和值的实际组成部分的信息已丢失。好的。好啊。
- 我知道在GCC和铿锵ud2有时人的实践。or for the end of cases样落下非空函数。我尝试——> to this sentence博尔登特异性这是真的,那么如果你在普通SIGILLignore to get that编译器警告。几乎每个人都认为我漂亮encountered this has before。
- 建议:"好,liliscent that kind of when is visible UB的行为在不同的编译时间是从fundamentally compilers used to how is probably surprising的举止,和很多人的学院。(我喜欢它是好东西;吵闹的失败,没有明确的especially when something in the Way to解释类源码,off the end of a降函数。)
- 我见过更糟的房屋在where the variable value range of在我不安的8位整数,but only of the全部CPU寄存器。在一个与原有Itanium has yet,可以使用变量为outright of an碰撞。
- 约书亚:哦,好吧,"itanium' S点,显speculation标签寄存器值将与"not a number of an等价that using the value",这样的故障。
- 好漂亮的xkcd.com 499是解释什么是UB。
- moreover,this was also the UB illustrates为什么在设计featurebug languages of the the正在介绍C和C++的地方:因为茶叶中的第一页给编译器确切的说因为是this kind of the Freedom,which has the most to permitted现代compilers现在做优化,使这些高质量的C / C + +这样的高性能级语言中。
- 布尔战争between the知道C++编译器的作家和C + + programmers想继续写有用的项目。在回答这个问题,综合全answering this is also be used as,as会在convincing静态分析工具vendors of copy for…
- 约书亚说:"outright是毁了好东西,很compared与optimize if (x > 300) FATAL_ERROR(); else {foo[x]=23;}编译器在检查模式的条件;除尘x茶",不能因为"过255,but then to allowing overwrite任意存储队列,因为x其实有比。
- sympathizer Bu was the _ @:to allow to implementations included在任何方式最常用的摇篮,useful to Customers。它是not to that intended蓝晶石should be useful equally事情考虑的行为。
- 不幸的是他/她outright @ supercat恩是在低碰撞的概率。
- @Joshua:在某些实现中,许多形式的UB都会直接设计出崩溃,概率非常高(有时是100%)。对各种错误动作的可靠捕获通常会导致显著的运行时性能损失,但如果对公路桥梁执行负载计算,则确保溢出不会导致程序产生错误结果的保证可能值得增加执行时间,并且标准WO的作者我不想禁止这样的实施。
编译器可以假定作为参数传递的布尔值是有效的布尔值(即已初始化或转换为true或false的布尔值)。true值不必与整数1相同——实际上,true和false可以有各种表示,但参数必须是这两个值之一的有效表示,其中定义了"有效表示"。
因此,如果您未能初始化bool,或者您通过不同类型的指针成功地覆盖了它,那么编译器的假设将是错误的,并且将导致未定义的行为。你受到警告:
50) Using a bool value in ways described by this International Standard as"undefined", such as by examining the value of an uninitialized automatic object, might cause it to behave as if it is neither true nor false. (Footnote to para 6 of §6.9.1, Fundamental Types)
- "true值不必与整数1相同"是一种误导。当然,实际的位模式可能是其他的,但是当隐式转换/提升(您看到除true/false以外的值的唯一方式),true总是1,false总是0时。当然,这样的编译器也不能使用这个编译器试图使用的技巧(使用事实,bool的实际位模式只能是0或1),所以这与OP的问题无关。
- @shadowranger始终可以直接检查对象表示。
- @shadowranger:我的观点是由实现负责。如果它将true的有效表示限制为位模式1,这就是它的特权。如果它选择了其他一些表示,那么它确实不能使用这里提到的优化。如果它确实选择了那个特定的表示,那么它可以。它只需要在内部保持一致。您可以通过将bool复制到字节数组中来检查它的表示;这不是ub(但它是由实现定义的)。
- 是的,优化编译器(即现实世界C++实现)通常会发出依赖于EDOCX1×7的代码,它具有EDOCX1、6或EDCOX1×4的位模式。它们不会每次从内存(或保存函数arg的寄存器)读取bool时都对它进行布尔化。这就是答案。示例:gcc4.7+可以在返回bool的函数中优化return a||b到or eax, edi,或者msvc可以优化a&b到test cl, dl的函数。x86的test是按位的and,所以如果cl=1和dl=2测试根据cl&dl = 0设置标志。
- 关于未定义行为的要点是允许编译器得出更多关于它的结论,例如,假设一个代码路径将导致访问一个未初始化的值,根本不会被采用,因为这正是程序员的责任。所以这不仅仅是低水平值可能不同于零或一的可能性。
- (我不熟悉C++)。是否有(运行时)方法来断言值未初始化?还是只有静态语言分析才能做到这一点?
- @ BurnsBa:无论是C还是C++都不提供任何运行时机制来测试未初始化的值。如果没有硬件支持(至少可以说,这是不常见的),任何这样的机制都将涉及巨大的成本。静态分析也不能总是捕获错误,但是目视检查会显示变量在声明中没有初始化。如果始终提供初始化器,则不会遇到此特定问题。
- @霍尔格:关于未定义行为的"要点"是,为了避免要求编译器花费更多的精力来处理它,而不是为了更好地为用户服务,标准避免强加任何要求。质量编译器的作者应该比标准的作者更好地定位,当他们的客户从比标准规定的更强的行为保证中受益时,以及当基于UB的"死分支消除"更有用时,他们应该能够认识到这一点。
- "burnsba:一些实现等(包括GCC和clang)可以添加运行时检测仪表有两种形式,不损失量总是要编译的时间。例如gcc -fsanitize=undefined -O3 foo.c。在2014年developers.redhat.com /博客/ / 10 / 16 / & hellip;。两部找到使用未初始化的数据,我们的地址在记忆的消毒剂和消毒clang / LLVM。谷歌github.com消毒剂/ / / /维基memorysanitizer节目实例检测未初始化的内存读大学。
- "我supercat意味着:太多的方案,可以认为,这是最坏的一件事,可以发生的,这是个未初始化的bool值会比一个不同的两个合法的值。但效果可以分四部。例如当你需要if(condition1) foo=expression; /* the only initialization of foo */ if(condition2) bar(foo); /* the only use of foo */,编译器可能意味着,condition2assume' condition1,没有需要测试它。在另一面,失神的效果,它可以if(condition2) bar(expression);变换它;它可能即使你使用的assumption在后续的代码。
- "说的:当使用编译器,不是特别设计的suitable写作计划,将对他们的行为untrustworthy从输入源,这是一定的威胁。计划需要一定的感知,许多"通用"的编译器是特别为只读suitable几个专业用途,除非他们二优化器的冰。
函数本身是正确的,但在您的测试程序中,调用函数的语句使用未初始化变量的值导致未定义的行为。
错误存在于调用函数中,可以通过代码复查或调用函数的静态分析来检测。使用编译器资源管理器链接,GCC8.2编译器会检测到该错误。(也许你可以针对clang提交一份bug报告,说明它没有发现问题)。
未定义的行为意味着任何事情都可能发生,包括程序在触发未定义行为的事件之后崩溃几行。
铌。回答"未定义的行为会导致什么?"总是"是"。这就是未定义行为的定义。
- 冰第一条款的威胁吗?不只是个未初始化复制bool触发UB吗?
- 〔〕/EEA dcl.init @ joshuagreen 12"如果安安地价值评估是生产模式,the the following is Undefined除了在行为(例:"我有一个和它那些cases of for bool例外)。Copying评教源茶……
- joshuagreen and the reason for"that is,你可能有一个平台,如果你接入的硬件容错触发器值INVALID for some一些类型。这些都是所谓的"陷阱representations有时。"
- 我是安腾CPU,朦胧,这仍然在生产,和TRAP值有,至少有两个半的现代C + + compilers(英特尔/ HP)。它literally has truefalsenot-a-thingvalues for布尔,布尔。
- on the to the侧翻转,回答"does the process to something compilers标准要求在一定的方式",甚至是"不"generally especially cases where(在任何编译器,它应该给你--质量-- something is the more,the less,there should be for the need to of the标准作者说真的。
- upvoted for the last款。它说,真的。
- thanks for that".,参考类,和它如何看你绝对正确。found this page与更多的细节。因为它在unsigned narrow charas"专门开发了特别强的证据,"我认为,boolis not,嗯,是的。
- 作者@ joshuagreen:the of the标准不认为它会让人的意识在写作implementations as some other types意向治疗的特殊需要,你会做什么,从意识到标准制造。作家did not the of the标准imply that to打算一切可能给安执行一致性标准should be viewed as with the"强"。
bool只允许保存值0或1,生成的代码可以假定它只保存这两个值中的一个。为赋值中的三元生成的代码可以将该值用作指向两个字符串的指针数组的索引,也就是说,它可以转换为如下类型:
1 2 3
| // the compile could make asm that"looks" like this, from your source
const static char *strings[] = {"false","true"};
const char *whichString = strings[boolValue]; |
如果boolValue未初始化,它实际上可以保存任何整数值,这将导致访问超出strings数组的边界。
- "婴儿猝死综合症。谢谢。theoretically,内部representations可以对面部是如何从他们两个integers /铸造的,所以你会堕落。
- 你是对的,和你的实例也会崩溃。然而,它是"可见"的评论,你的代码是使用未初始化的变量作为一个指标的阵列。它也会崩溃,甚至在DEBUG(例如一些调试器/编译器将初始化与特异性的方式,使它更容易吗?当它看到crashes)。在我方,例如,令人惊讶的是,在使用bool的冰不可见:《优化器决定使用它在一个不通过指尖的源代码。
- "我只是在remz使用数组的两个节目《生什么我可以是两个等价的代码,没有人会suggesting,实际上写的是.
- "remz recast《bool两int与*(int *)&boolValue打印调试和它的用途,看它是否比其他任何0冰或1crashes当它。如果你的案例,它多漂亮的理论,confirms编译器内联优化的冰,如果作为一个数组,explains为什么它crashlng冰。
- "havenard,int冰可能是较高的比boolSO,不会做任何事。
- "婴儿猝死综合症:是什么呢?int或冰的机器字大小的逻辑,与short储存在变异的改进。城市设计bool不是命中注定要*存储改进无论如何;平衡(1字节bool废物最小的一87.5 %。由于它不存储优化设计的,它有两个是有道理的bool也是自然对象的尺寸,即sizeof(int)==sizeof(bool)。
- "msalters必须sizeof(bool) == 1在线平台。这将是很长的,如果孩子在8 bools放在一些struct 31字节而不是浪费的只是7。
- tavianbarnes:当然,如果你是在很多bools在一struct你会做最好的两位字段或显式使用位屏蔽所以你minimize的废物。
- "tavianbarnes的std::bitset<8>:有,嗯,这是改进的空间。std::vector冰渣。
- "msalters吧,忘了这是一个C + +在想问题,C。
- "msalters std::bitset<8>:不给我,我所有的好名称不同的旗。取决于他们是什么,那可能是重要的。
总结你的问题很多,你是问C++标准允许编译器假设一个EDCOX1?5,它只能有一个内部的数字表示"0"或"1",并用这种方式吗?
该标准没有说明bool的内部表示。它只定义了将bool强制转换为int时发生的情况(反之亦然)。大多数情况下,由于这些积分转换(以及人们非常依赖它们的事实),编译器将使用0和1,但它不必使用(尽管它必须遵守所使用的任何较低级别ABI的约束)。
因此,当编译器看到bool时,它有权认为所述bool包含'true或'false位模式中的任何一种,并做任何它感兴趣的事情。因此,如果true和false的值分别为1和0,编译器确实可以将strlen优化为5 - 。其他有趣的行为也是可能的!
正如这里反复提到的,未定义的行为有未定义的结果。包括但不限于
- 您的代码按预期工作
- 您的代码在随机时间失败
- 您的代码根本没有运行。
看看每个程序员应该知道什么关于未定义的行为