简体   繁体   English

C++ 标准是否允许未初始化的 bool 使程序崩溃?

[英]Does the C++ standard allow for an uninitialized bool to crash a program?

I know that an "undefined behaviour" in C++ can pretty much allow the compiler to do anything it wants.我知道 C++ 中的“未定义行为”几乎可以让编译器做任何它想做的事情。 However, I had a crash that surprised me, as I assumed that the code was safe enough.然而,我遇到了一次让我感到惊讶的崩溃,因为我认为代码足够安全。

In this case, the real problem happened only on a specific platform using a specific compiler, and only if optimization was enabled.在这种情况下,真正的问题只发生在使用特定编译器的特定平台上,并且只有在启用优化的情况下才会发生。

I tried several things in order to reproduce the problem and simplify it to the maximum.我尝试了几种方法来重现问题并将其简化到最大程度。 Here's an extract of a function called Serialize , that would take a bool parameter, and copy the string true or false to an existing destination buffer.这是一个名为Serialize的函数的摘录,它接受一个 bool 参数,并将字符串truefalse复制到现有的目标缓冲区。

Would this function be in a code review, there would be no way to tell that it, in fact, could crash if the bool parameter was an uninitialized value?这个函数会在代码审查中吗,如果 bool 参数是一个未初始化的值,实际上没有办法告诉它会崩溃吗?

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

If this code is executed with clang 5.0.0 + optimizations, it will/can crash.如果使用 clang 5.0.0 + 优化执行此代码,它将/可能会崩溃。

The expected ternary-operator boolValue ? "true" : "false"预期的三元运算符boolValue ? "true" : "false" boolValue ? "true" : "false" looked safe enough for me, I was assuming, "Whatever garbage value is in boolValue doesn't matter, since it will evaluate to true or false anyhow." boolValue ? "true" : "false"对我来说看起来足够安全,我假设,“ boolValue中的任何垃圾值都无关紧要,因为无论如何它都会评估为 true 或 false。”

I have setup a Compiler Explorer example that shows the problem in the disassembly, here the complete example.我已经设置了一个Compiler Explorer 示例,它显示了反汇编中的问题,这里是完整的示例。 Note: in order to repro the issue, the combination I've found that worked is by using Clang 5.0.0 with -O2 optimisation.注意:为了重现这个问题,我发现有效的组合是使用带有 -O2 优化的 Clang 5.0.0。

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

The problem arises because of the optimizer: It was clever enough to deduce that the strings "true" and "false" only differs in length by 1. So instead of really calculating the length, it uses the value of the bool itself, which should technically be either 0 or 1, and goes like this:问题是由优化器引起的:它足够聪明地推断出字符串“true”和“false”的长度仅相差 1。因此,它没有真正计算长度,而是使用 bool 本身的值,这应该从技术上讲,要么是 0,要么是 1,就像这样:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

While this is "clever", so to speak, my question is: Does the C++ standard allow a compiler to assume a bool can only have an internal numerical representation of '0' or '1' and use it in such a way?虽然这很“聪明”,但可以这么说,我的问题是: C++ 标准是否允许编译器假设 bool 只能具有“0”或“1”的内部数字表示并以这种方式使用它?

Or is this a case of implementation-defined, in which case the implementation assumed that all its bools will only ever contain 0 or 1, and any other value is undefined behaviour territory?或者这是实现定义的情况,在这种情况下,实现假设其所有布尔值只包含 0 或 1,并且任何其他值都是未定义的行为领域?

Yes, ISO C++ allows (but doesn't require) implementations to make this choice. 是的,ISO C ++允许(但不要求)实现做出这种选择。

But also note that ISO C++ allows a compiler to emit code that crashes on purpose (eg with an illegal instruction) if the program encounters UB, eg as a way to help you find errors. 但另请注意,如果程序遇到UB,ISO C ++允许编译器发出故意崩溃的代码(例如,使用非法指令),例如,作为帮助您查找错误的方法。 (Or because it's a DeathStation 9000. Being strictly conforming is not sufficient for a C++ implementation to be useful for any real purpose). (或者因为它是一个DeathStation 9000.严格遵守是不足以使C ++实现对任何真正的目的都有用)。 So ISO C++ would allow a compiler to make asm that crashed (for totally different reasons) even on similar code that read an uninitialized uint32_t . 因此,即使在读取未初始化的uint32_t类似代码上,ISO C ++也允许编译器使asm崩溃(出于完全不同的原因)。 Even though that's required to be a fixed-layout type with no trap representations. 即使这需要是一个没有陷阱表示的固定布局类型。

It's an interesting question about how real implementations work, but remember that even if the answer was different, your code would still be unsafe because modern C++ is not a portable version of assembly language. 这是一个关于真实实现如何工作的有趣问题,但请记住,即使答案不同,您的代码仍然不安全,因为现代C ++不是汇编语言的可移植版本。


You're compiling for the x86-64 System V ABI , which specifies that a bool as a function arg in a register is represented by the bit-patterns false=0 and true=1 in the low 8 bits of the register 1 . 你正在编译为x86-64的系统V ABI ,它指定了一个bool作为在寄存器中的功能ARG由位模式表示的false=0true=1在寄存器1的低8位。 In memory, bool is a 1-byte type that again must have an integer value of 0 or 1. 在内存中, bool是1字节类型,同样必须具有0或1的整数值。

(An ABI is a set of implementation choices that compilers for the same platform agree on so they can make code that calls each other's functions, including type sizes, struct layout rules, and calling conventions.) (ABI是一组实现选择,同一平台的编译器同意这样做,因此他们可以创建调用彼此函数的代码,包括类型大小,结构布局规则和调用约定。)

ISO C++ doesn't specify it, but this ABI decision is widespread because it makes bool->int conversion cheap (just zero-extension) . ISO C ++没有指定它,但是这个ABI决定很普遍,因为它使bool-> int转换变得便宜(只是零扩展) I'm not aware of any ABIs that don't let the compiler assume 0 or 1 for bool , for any architecture (not just x86). 对于任何体系结构(不仅仅是x86),我都不知道任何不让编译器为bool假设为0或1的ABI。 It allows optimizations like !mybool with xor eax,1 to flip the low bit: Any possible code that can flip a bit/integer/bool between 0 and 1 in single CPU instruction . 它允许像!myboolxor eax,1这样的优化来翻转低位: 在单CPU指令中可以在0和1之间翻转位/整数/布尔值的任何可能代码 Or compiling a&&b to a bitwise AND for bool types. 或者将a&&b编译为bool类型的按位AND。 Some compilers do actually take advantage Boolean values as 8 bit in compilers. 有些编译器确实在编译器中利用布尔值作为8位。 Are operations on them inefficient? 对他们的操作是否效率低下? .

In general, the as-if rule allows allows the compiler to take advantage of things that are true on the target platform being compiled for , because the end result will be executable code that implements the same externally-visible behaviour as the C++ source. 通常,as-if规则允许编译器利用正在编译的目标平台上的事物 ,因为最终结果将是实现与C ++源相同的外部可见行为的可执行代码。 (With all the restrictions that Undefined Behaviour places on what is actually "externally visible": not with a debugger, but from another thread in a well-formed / legal C++ program.) (具有Undefined Behavior放置在实际上“外部可见”的所有限制:不是使用调试器,而是来自格式良好/合法的C ++程序中的另一个线程。)

The compiler is definitely allowed to take full advantage of an ABI guarantee in its code-gen, and make code like you found which optimizes strlen(whichString) to 绝对允许编译器在其代码中充分利用ABI保证,并使您发现的代码优化strlen(whichString)
5U - boolValue . 5U - boolValue (BTW, this optimization is kind of clever, but maybe shortsighted vs. branching and inlining memcpy as stores of immediate data 2 .) (顺便说一句,这种优化有点聪明,但可能是近视与分支和内联memcpy作为即时数据存储2。

Or the compiler could have created a table of pointers and indexed it with the integer value of the bool , again assuming it was a 0 or 1. ( This possibility is what @Barmar's answer suggested .) 或者编译器可以创建一个指针表并用bool的整数值对其进行索引,再次假设它是0或1.( 这种可能性是@Barmar的答案所建议的 。)


Your __attribute((noinline)) constructor with optimization enabled led to clang just loading a byte from the stack to use as uninitializedBool . 启用了优化的__attribute((noinline))构造函数导致clang只是从堆栈加载一个字节以用作uninitializedBool It made space for the object in main with push rax (which is smaller and for various reason about as efficient as sub rsp, 8 ), so whatever garbage was in AL on entry to main is the value it used for uninitializedBool . 它使用push rax main对象创建了空间(由于各种原因,它与sub rsp, 8一样有效sub rsp, 8 ),因此在进入main AL中的垃圾是用于uninitializedBool的值。 This is why you actually got values that weren't just 0 . 这就是为什么你实际上得到的值不仅仅是0

5U - random garbage can easily wrap to a large unsigned value, leading memcpy to go into unmapped memory. 5U - random garbage可以很容易地换成大的无符号值,导致memcpy进入未映射的内存。 The destination is in static storage, not the stack, so you're not overwriting a return address or something. 目标位于静态存储中,而不是堆栈中,因此您不会覆盖返回地址或其他内容。


Other implementations could make different choices, eg false=0 and true=any non-zero value . 其他实现可以做出不同的选择,例如false=0true=any non-zero value Then clang probably wouldn't make code that crashes for this specific instance of UB. 然后clang可能不会使代码崩溃为这个特定的UB实例。 (But it would still be allowed to if it wanted to.) I don't know of any implementations that choose anything other what x86-64 does for bool , but the C++ standard allows many things that nobody does or even would want to do on hardware that's anything like current CPUs. (但是如果它想要它仍然会被允许。)我不知道任何其他选择x86-64为bool做什么的实现,但是C ++标准允许许多人没有做甚至想做的事情在硬件上,就像当前的CPU一样。

ISO C++ leaves it unspecified what you'll find when you examine or modify the object representation of a bool . 当您检查或修改bool的对象表示时,ISO C ++没有指定您将找到的内容 (eg by memcpy ing the bool into unsigned char , which you're allowed to do because char* can alias anything. And unsigned char is guaranteed to have no padding bits, so the C++ standard does formally let you hexdump object representations without any UB. Pointer-casting to copy the object representation is different from assigning char foo = my_bool , of course, so booleanization to 0 or 1 wouldn't happen and you'd get the raw object representation.) (例如,通过memcpy荷兰国际集团的boolunsigned char ,你会允许这样做,因为char*可以别名任何东西,而且unsigned char是保证没有填充位,所以C ++标准并正式让你进制打印对象表示没有任何UB 。复制对象表示的指针转换与赋值char foo = my_bool ,当然,布局化为0或1也不会发生,你将获得原始对象表示。)

You've partially "hidden" the UB on this execution path from the compiler with noinline . 您使用noinline从编译器中部分 “隐藏”了UB Even if it doesn't inline, though, interprocedural optimizations could still make a version of the function that depends on the definition of another function. 但是,即使它不是内联的,过程间优化仍然可以使函数的版本依赖于另一个函数的定义。 (First, clang is making an executable, not a Unix shared library where symbol-interposition can happen. Second, the definition in inside the class{} definition so all translation units must have the same definition. Like with the inline keyword.) (首先,clang正在创建一个可执行文件,而不是一个可以发生符号插入的Unix共享库。其次, class{}定义中的定义所以所有翻译单元必须具有相同的定义。与inline关键字一样。)

So a compiler could emit just a ret or ud2 (illegal instruction) as the definition for main , because the path of execution starting at the top of main unavoidably encounters Undefined Behaviour. 所以编译器只能发出一个retud2 (非法指令)作为main的定义,因为从main顶部开始执行的路径不可避免地遇到Undefined Behavior。 (Which the compiler can see at compile time if it decided to follow the path through the non-inline constructor.) (如果编译器决定遵循通过非内联构造函数的路径,编译器可以在编译时看到。)

Any program that encounters UB is totally undefined for its entire existence. 任何遇到UB的程序都是完全未定义的。 But UB inside a function or if() branch that never actually runs doesn't corrupt the rest of the program. 但是在函数或if()分支中从不实际运行的UB不会破坏程序的其余部分。 In practice that means that compilers can decide to emit an illegal instruction, or a ret , or not emit anything and fall into the next block / function, for the whole basic block that can be proven at compile time to contain or lead to UB. 在实践中,这意味着编译器可以决定发出非法指令或ret ,或者不发出任何内容并进入下一个块/函数,以获得可以在编译时证明包含或导致UB的整个基本块。

GCC and Clang in practice do actually sometimes emit ud2 on UB, instead of even trying to generate code for paths of execution that make no sense. GCC和锵在实践中确实有时发出ud2上UB,而不是甚至还试图生成,使没有意义的执行路径代码。 Or for cases like falling off the end of a non- void function, gcc will sometimes omit a ret instruction. 或者对于非void函数结束的情况,gcc有时会省略ret指令。 If you were thinking that "my function will just return with whatever garbage is in RAX", you are sorely mistaken. 如果你认为“我的函数只会返回RAX中的任何垃圾”,你就会非常错误。 Modern C++ compilers don't treat the language like a portable assembly language any more. 现代C ++编译器不再将语言视为可移植汇编语言。 Your program really has to be valid C++, without making assumptions about how a stand-alone non inlined version of your function might look in asm. 你的程序实际上必须是有效的C ++,而不假设你的函数的独立非内联版本可能在asm中看起来如何。

Another fun example is Why does unaligned access to mmap'ed memory sometimes segfault on AMD64? 另一个有趣的例子是为什么对mmap的内存进行未对齐访问有时会在AMD64上出现段错误? . x86 doesn't fault on unaligned integers, right? x86对未对齐的整数没有错,对吧? So why would a misaligned uint16_t* be a problem? 那么为什么一个错位的uint16_t*会成为一个问题呢? Because alignof(uint16_t) == 2 , and violating that assumption led to a segfault when auto-vectorizing with SSE2. 因为alignof(uint16_t) == 2 ,并且违反该假设导致在使用SSE2自动向量化时出现段错误。

See also What Every C Programmer Should Know About Undefined Behavior #1/3 , an article by a clang developer. 另请参阅 每个C程序员应该知道的关于未定义行为的内容#1/3 ,这是clang开发人员的一篇文章。

Key point: if the compiler noticed the UB at compile time, it could "break" (emit surprising asm) the path through your code that causes UB even if targeting an ABI where any bit-pattern is a valid object representation for bool . 关键点:如果编译器在编译时注意到UB,它可以 “破坏”(发出令人惊讶的asm)通过代码的路径导致UB,即使针对ABI,其中任何位模式都是bool的有效对象表示。

Expect total hostility toward many mistakes by the programmer, especially things modern compilers warn about. 期待程序员对许多错误的完全敌意,特别是现代编译器警告的事情。 This is why you should use -Wall and fix warnings. 这就是你应该使用-Wall并修复警告的原因。 C++ is not a user-friendly language, and something in C++ can be unsafe even if it would be safe in asm on the target you're compiling for. C ++不是一种用户友好的语言,即使在你编译的目标上asm是安全的,C ++中的东西也是不安全的。 (eg signed overflow is UB in C++ and compilers will assume it doesn't happen, even when compiling for 2's complement x86, unless you use clang/gcc -fwrapv .) (例如,签名溢出是C ++中的UB,编译器会认为它不会发生,即使编译2的补码x86,除非你使用clang/gcc -fwrapv 。)

Compile-time-visible UB is always dangerous, and it's really hard to be sure (with link-time optimization) that you've really hidden UB from the compiler and can thus reason about what kind of asm it will generate. 编译时可见的UB总是危险的,并且很难确定(使用链接时优化)你真的从编译器中隐藏了UB,因此可以推断出它会产生什么样的asm。

Not to be over-dramatic; 不要过于戏剧化; often compilers do let you get away with some things and emit code like you're expecting even when something is UB. 通常编译器会让你逃避一些事情并发出像你期望的那样的代码,即使是某些东西是UB。 But maybe it will be a problem in the future if compiler devs implement some optimization that gains more info about value-ranges (eg that a variable is non-negative, maybe allowing it to optimize sign-extension to free zero-extension on x86-64). 但是,如果编译器开发人员实现一些优化以获得关于值范围的更多信息(例如,变量是非负的,可能允许它优化符号扩展以在x86上释放零扩展),那么将来可能会出现问题。 64)。 For example, in current gcc and clang, doing tmp = a+INT_MIN doesn't optimize a<0 as always-false, only that tmp is always negative. 例如,在当前的gcc和clang中,执行tmp = a+INT_MIN并不tmp = a+INT_MIN a<0始终为false,而只是tmp始终为负。 (Because INT_MIN + a=INT_MAX is negative on this 2's complement target, and a can't be any higher than that.) (因为INT_MIN + a=INT_MAX在这个2的补码目标上是负的,并且a不能高于此值。)

So gcc/clang don't currently backtrack to derive range info for the inputs of a calculation, only on the results based on the assumption of no signed overflow: example on Godbolt . 所以gcc / clang目前没有回溯来计算输入计算的范围信息,只是基于没有签名溢出的假设的结果: 例如Godbolt I don't know if this is optimization is intentionally "missed" in the name of user-friendliness or what. 我不知道这是优化是故意“错过”的用户友好的名义或什么。

Also note that implementations (aka compilers) are allowed to define behaviour that ISO C++ leaves undefined . 另请注意, 允许实现(也称为编译器)定义ISO C ++未定义的行为 For example, all compilers that support Intel's intrinsics (like _mm_add_ps(__m128, __m128) for manual SIMD vectorization) must allow forming mis-aligned pointers, which is UB in C++ even if you don't dereference them. 例如,支持Intel内在函数的所有编译器(如_mm_add_ps(__m128, __m128)用于手动SIMD向量化)必须允许形成错误对齐的指针,即使您取消引用它们,也是C ++中的UB。 __m128i _mm_loadu_si128(const __m128i *) does unaligned loads by taking a misaligned __m128i* arg, not a void* or char* . __m128i _mm_loadu_si128(const __m128i *)通过取错__m128i* arg而不是void*char*执行未对齐的加载。 Is `reinterpret_cast`ing between hardware vector pointer and the corresponding type an undefined behavior? 在硬件向量指针和相应类型之间`reinterpret_cast`是一个未定义的行为吗?

GNU C/C++ also defines the behaviour of left-shifting a negative signed number (even without -fwrapv ), separately from the normal signed-overflow UB rules. GNU C / C ++还定义了左移一个负的有符号数(即使没有-fwrapv )的行为,与普通的有符号溢出UB规则分开。 ( This is UB in ISO C++ , while right shifts of signed numbers are implementation-defined (logical vs. arithmetic); good quality implementations choose arithmetic on HW that has arithmetic right shifts, but ISO C++ doesn't specify). 这是ISO C ++中的UB ,而有符号数的右移是实现定义的(逻辑与算术);高质量的实现选择具有算术右移的HW算术,但ISO C ++没有指定)。 This is documented in the GCC manual's Integer section , along with defining implementation-defined behaviour that C standards require implementations to define one way or another. 这在GCC手册的Integer部分中有记录 ,同时定义了C标准要求实现以这种或那种方式定义的实现定义的行为。

There are definitely quality-of-implementation issues that compiler developers care about; 编译器开发人员肯定会关注实现的质量问题; they generally aren't trying to make compilers that are intentionally hostile, but taking advantage of all the UB potholes in C++ (except ones they choose to define) to optimize better can be nearly indistinguishable at times. 他们通常不会试图制造故意不利的编译器,但是利用C ++中的所有UB坑洼(除了他们选择定义的那些)来进行更好的优化,有时几乎无法区分。


Footnote 1 : The upper 56 bits can be garbage which the callee must ignore, as usual for types narrower than a register. 脚注1 :高56位可以是被调用者必须忽略的垃圾,通常用于比寄存器窄的类型。

( Other ABIs do make different choices here . Some do require narrow integer types to be zero- or sign-extended to fill a register when passed to or returned from functions, like MIPS64 and PowerPC64. See the last section of this x86-64 answer which compares vs. those earlier ISAs .) 其他的ABI在这里做的做出不同的选择 。有些人需要窄的整数类型是零或符号扩展传递时,或者从函数返回,就像MIPS64和PowerPC64填补寄存器见的最后一节这x86-64的答案与早期的ISA相比较 。)

For example, a caller might have calculated a & 0x01010101 in RDI and used it for something else, before calling bool_func(a&1) . 例如,在调用bool_func(a&1)之前,调用者可能已在RDI中计算a & 0x01010101并将其用于其他内容。 The caller could optimize away the &1 because it already did that to the low byte as part of and edi, 0x01010101 , and it knows the callee is required to ignore the high bytes. 调用者可以优化掉&1因为它已经作为and edi, 0x01010101一部分对低字节进行了and edi, 0x01010101 ,并且它知道被调用者需要忽略高字节。

Or if a bool is passed as the 3rd arg, maybe a caller optimizing for code-size loads it with mov dl, [mem] instead of movzx edx, [mem] , saving 1 byte at the cost of a false dependency on the old value of RDX (or other partial-register effect, depending on CPU model). 或者如果bool作为第3个arg传递,也许优化代码大小的调用者使用mov dl, [mem]而不是movzx edx, [mem]加载它,节省1个字节,代价是对旧的错误依赖RDX的值(或其他部分寄存器效果,取决于CPU型号)。 Or for the first arg, mov dil, byte [r10] instead of movzx edi, byte [r10] , because both require a REX prefix anyway. 或者对于第一个arg, mov dil, byte [r10]而不是movzx edi, byte [r10] ,因为两者都需要REX前缀。

This is why clang emits movzx eax, dil in Serialize , instead of sub eax, edi . 这就是为什么clang在Serialize发出movzx eax, dil ,而不是sub eax, edi (For integer args, clang violates this ABI rule, instead depending on the undocumented behaviour of gcc and clang to zero- or sign-extend narrow integers to 32 bits. Is a sign or zero extension required when adding a 32bit offset to a pointer for the x86-64 ABI? So I was interested to see that it doesn't do the same thing for bool .) (对于整数args,clang违反了这个ABI规则,而是取决于gcc和clang的未记录行为,将零或符号扩展为窄整数为32位。 当向指针添加32位偏移时,是否需要符号或零扩展x86-64 ABI?所以我有兴趣看到它对bool没有做同样的事情。)


Footnote 2: After branching, you'd just have a 4-byte mov -immediate, or a 4-byte + 1-byte store. 脚注2:分支后,你只需要一个4字节的mov -immediate,或一个4字节+ 1字节的存储。 The length is implicit in the store widths + offsets. 长度隐含在商店宽度+偏移中。

OTOH, glibc memcpy will do two 4-byte loads/stores with an overlap that depends on length, so this really does end up making the whole thing free of conditional branches on the boolean. OTOH,glibc memcpy会做两个4字节的加载/存储,其重叠取决于长度,所以这确实最终使得整个事物在布尔值上没有条件分支。 See the L(between_4_7): block in glibc's memcpy/memmove. 请参阅glibc的memcpy / memmove中的L(between_4_7): Or at least, go the same way for either boolean in memcpy's branching to select a chunk size. 或者至少,对于memcpy分支中的任一布尔值选择块大小,以同样的方式。

If inlining, you could use 2x mov -immediate + cmov and a conditional offset, or you could leave the string data in memory. 如果内联,您可以使用2x mov -immediate + cmov和条件偏移量,或者您可以将字符串数据保留在内存中。

Or if tuning for Intel Ice Lake ( with the Fast Short REP MOV feature ), an actual rep movsb might be optimal. 或者,如果调整Intel Ice Lake( 使用Fast Short REP MOV功能 ),实际的rep movsb可能是最佳的。 glibc memcpy might start using rep movsb for small sizes on CPUs with that feature, saving a lot of branching. glibc memcpy可能会开始在具有该功能的CPU上使用rep movsb来实现小尺寸,从而节省了大量的分支。


Tools for detecting UB and usage of uninitialized values 用于检测UB和未初始化值的使用的工具

In gcc and clang, you can compile with -fsanitize=undefined to add run-time instrumentation that will warn or error out on UB that happens at runtime. 在gcc和clang中,您可以使用-fsanitize=undefined进行编译,以添加将在运行时发生的UB上发出警告或错误的运行时检测。 That won't catch unitialized variables, though. 但是,这不会捕获单元化变量。 (Because it doesn't increase type sizes to make room for an "uninitialized" bit). (因为它不会增加类型大小以为“未初始化”位腾出空间)。

See https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/ 请参阅https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/

To find usage of uninitialized data, there's Address Sanitizer and Memory Sanitizer in clang/LLVM. 要查找未初始化数据的用法,请在clang / LLVM中使用Address Sanitizer和Memory Sanitizer。 https://github.com/google/sanitizers/wiki/MemorySanitizer shows examples of clang -fsanitize=memory -fPIE -pie detecting uninitialized memory reads. https://github.com/google/sanitizers/wiki/MemorySanitizer显示了clang -fsanitize=memory -fPIE -pie检测未初始化内存读取的示例。 It might work best if you compile without optimization, so all reads of variables end up actually loading from memory in the asm. 如果您在没有优化的情况下进行编译,它可能效果最佳,因此所有变量读取最终实际上都是从asm中的内存加载的。 They show it being used at -O2 in a case where the load wouldn't optimize away. 他们表明,在负载无法优化的情况下,它在-O2处使用。 I haven't tried it myself. 我自己没试过。 (In some cases, eg not initializing an accumulator before summing an array, clang -O3 will emit code that sums into a vector register that it never initialized. So with optimization, you can have a case where there's no memory read associated with the UB. But -fsanitize=memory changes the generated asm, and might result in a check for this.) (在某些情况下,例如,在对数组求和之前不初始化累加器,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. 通常,MemorySanitizer以静默方式跟踪未初始化数据在内存中的传播,并在根据未初始化值获取(或不获取)代码分支时报告警告。

MemorySanitizer implements a subset of functionality found in Valgrind (Memcheck tool). MemorySanitizer实现了Valgrind(Memcheck工具)中的一部分功能。

It should work for this case because the call to glibc memcpy with a length calculated from uninitialized memory will (inside the library) result in a branch based on length . 它应该适用于这种情况,因为使用从未初始化的内存计算的length调用glibc memcpy将(在库内)导致基于length的分支。 If it had inlined a fully branchless version that just used cmov , indexing, and two stores, it might not have worked. 如果它内联一个完全无cmov版本,只使用了cmov ,索引和两个商店,它可能没有用。

Valgrind's memcheck will also look for this kind of problem, again not complaining if the program simply copies around uninitialized data. Valgrind的memcheck也会寻找这种问题,如果程序只是简单地复制未初始化的数据,也不会抱怨。 But it says it will detect when a "Conditional jump or move depends on uninitialised value(s)", to try to catch any externally-visible behaviour that depends on uninitialized data. 但它表示它会检测“条件跳转或移动取决于未初始化的值”,以尝试捕获依赖于未初始化数据的任何外部可见行为。

Perhaps the idea behind not flagging just a load is that structs can have padding, and copying the whole struct (including padding) with a wide vector load/store is not an error even if the individual members were only written one at a time. 也许不标记一个加载背后的想法是结构可以有填充,并且使用宽向量加载/存储复制整个结构(包括填充)不是错误,即使每个成员一次只写一个。 At the asm level, the information about what was padding and what is actually part of the value has been lost. 在asm级别,有关填充内容和实际值的一部分的信息已丢失。

The compiler is allowed to assume that a boolean value passed as an argument is a valid boolean value (ie one which has been initialised or converted to true or false ). 允许编译器假定作为参数传递的布尔值是有效的布尔值(即已初始化或转换为truefalse )。 The true value doesn't have to be the same as the integer 1 -- indeed, there can be various representations of true and false -- but the parameter must be some valid representation of one of those two values, where "valid representation" is implementation-defined. true值不必与整数1相同 - 实际上,可以有各种表示truefalse - 但参数必须是这两个值之一的有效表示,其中“有效表示”是实现定义的。

So if you fail to initialise a bool , or if you succeed in overwriting it through some pointer of a different type, then the compiler's assumptions will be wrong and Undefined Behaviour will ensue. 因此,如果您未能初始化bool ,或者如果您通过某个不同类型的指针成功覆盖它,那么编译器的假设将是错误的,并且随后会出现未定义的行为。 You had been warned: 你被警告过:

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. 50)以本国际标准描述的方式将bool值用作“未定义”,例如通过检查未初始化的自动对象的值,可能会使其表现为既不是真也不是假。 (Footnote to para 6 of §6.9.1, Fundamental Types) (§6.9.1第6段脚注,基本类型)

The function itself is correct, but in your test program, the statement that calls the function causes undefined behaviour by using the value of an uninitialized variable. 函数本身是正确的,但在测试程序中,调用函数的语句通过使用未初始化变量的值导致未定义的行为。

The bug is in the calling function, and it could be detected by code review or static analysis of the calling function. 该错误在调用函数中,可以通过代码检查或调用函数的静态分析来检测。 Using your compiler explorer link, the gcc 8.2 compiler does detect the bug. 使用编译器资源管理器链接,gcc 8.2编译器会检测错误。 (Maybe you could file a bug report against clang that it doesn't find the problem). (也许你可以提交针对clang的bug报告,它没有发现问题)。

Undefined behaviour means anything can happen, which includes the program crashing a few lines after the event that triggered the undefined behaviour. 未定义的行为意味着任何事情都可能发生,其中包括程序在触发未定义行为的事件之后崩溃几行。

NB. NB。 The answer to "Can undefined behaviour cause _____ ?" 答案“未定义的行为会导致_____吗?” is always "Yes". 总是“是”。 That's literally the definition of undefined behaviour. 这就是未定义行为的定义。

A bool is only allowed to hold the values 0 or 1 , and the generated code can assume that it will only hold one of these two values. bool只允许保存值01 ,生成的代码可以假定它只保存这两个值中的一个。 The code generated for the ternary in the assignment could use the value as the index into an array of pointers to the two strings, ie it might be converted to something like: 在赋值中为三元生成的代码可以使用该值作为指向两个字符串的指针数组的索引,即它可能转换为类似的内容:

     // the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

If boolValue is uninitialized, it could actually hold any integer value, which would then cause accessing outside the bounds of the strings array. 如果boolValue未初始化,它实际上可以保存任何整数值,这将导致访问strings数组的边界之外。

Summarising your question a lot, you are asking Does the C++ standard allow a compiler to assume a bool can only have an internal numerical representation of '0' or '1' and use it in such a way? 总结你的问题很多,你问的是C ++标准是否允许编译器假设bool只能有一个'0'或'1'的内部数字表示并以这种方式使用它?

The standard says nothing about the internal representation of a bool . 该标准没有说明bool的内部表示。 It only defines what happens when casting a bool to an int (or vice versa). 它只定义了将bool转换为int时会发生什么(反之亦然)。 Mostly, because of these integral conversions (and the fact that people rely rather heavily on them), the compiler will use 0 and 1, but it doesn't have to (although it has to respect the constraints of any lower level ABI it uses). 大多数情况下,由于这些完整的转换(以及人们非常依赖它们的事实),编译器将使用0和1,但它不必(尽管它必须遵守它使用的任何较低级别ABI的约束) )。

So, the compiler, when it sees a bool is entitled to consider that said bool contains either of the ' true ' or ' false ' bit patterns and do anything it feels like. 所以,编译器在看到bool有权考虑所说的bool包含' true '或' false '位模式中的任何一种并做任何感觉。 So if the values for true and false are 1 and 0, respectively, the compiler is indeed allowed to optimise strlen to 5 - <boolean value> . 因此,如果truefalse的值分别为1和0,则确实允许编译器将strlen优化为5 - <boolean value> Other fun behaviours are possible! 其他有趣的行为是可能的!

As gets repeatedly stated here, undefined behaviour has undefined results. 正如在此重复陈述的那样,未定义的行为具有未定义的结果。 Including but not limited to 包括但不仅限于

  • Your code working as you expected it to 您的代码按预期工作
  • Your code failing at random times 您的代码随机失败
  • Your code not being run at all. 您的代码根本没有运行。

See What every programmer should know about undefined behavior 请参阅每个程序员应该了解的未定义行为

Does the C++ standard allow a compiler to assume a bool can only have an internal numerical representation of '0' or '1' and use it in such a way? C++ 标准是否允许编译器假设 bool 只能具有“0”或“1”的内部数字表示并以这种方式使用它?

Yes indeed, and in case it's useful to anyone, here's another real-world example that happened to me.是的,如果它对任何人有用,这里是另一个发生在我身上的真实例子。

I once spent several weeks tracking down an obscure bug in a large codebase.我曾经花了几个星期来追踪大型代码库中的一个不起眼的错误。 There were several aspects that made it challenging, but the root cause was an uninitialized boolean member of a class variable.有几个方面使它具有挑战性,但根本原因是类变量的未初始化布尔成员。

There was a test involving a complicated expression involving this member variable:有一个测试涉及一个涉及这个成员变量的复杂表达式:

if(COMPLICATED_EXPRESSION_INVOLVING(class->member)) {
    ...
}

I began to suspect that this test was not evaluating "true" when it should.我开始怀疑这个测试在它应该评估的时候没有评估“真实”。 I don't remember whether it was not convenient to run things under a debugger, or if I didn't trust the debugger, or what, but I went for the brute-force technique of augmenting the code with some debugging printouts:我不记得在调试器下运行是否不方便,或者我是否不信任调试器,或者什么,但我选择了使用一些调试打印输出来扩充代码的蛮力技术:

printf("%s\n", COMPLICATED_EXPRESSION_INVOLVING(class->member) ? "yes" : "no");

if(COMPLICATED_EXPRESSION_INVOLVING(class->member)) {
    printf("doing the thing\n");
    ...
}

Imagine my surprise when the code printed " no " followed by " doing the thing ".想象一下当代码打印“ no ”后跟“ doing the thing ”时我的惊讶。

Disassembly revealed that sometimes, the compiler (which was gcc) was testing the boolean member by comparing it to 0, but other times, it was using a test-least-significant-bit instruction.反汇编显示,有时,编译器(即 gcc)通过将布尔成员与 0 进行比较来测试它,但其他时候,它使用的是测试最低有效位指令。 The uninitialized boolean variable happened to contain the value 2. So, in machine language, the test equivalent to未初始化的布尔变量恰好包含值 2。因此,在机器语言中,测试等效于

if(class->member != 0)

succeeded, but the test equivalent to成功,但测试相当于

if(class->member % 2 != 0)

failed.失败的。 The variable was literally true and false at the same time!该变量实际上同时是真和假! And if that's not undefined behavior, I don't know what is!如果这不是未定义的行为,我不知道是什么!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM