8 ms·
C compiler undefined behavior: calls never-called function
- jhasse 9y agoThere's only one value "Do" will ever have. He might as well set it from the beginning ;)
- caf 9y agoThe only value it will ever have is NULL (but calling through a NULL function pointer is UB so it can instead call system() :)
- jhasse 9y agoIt's uninitialized, not NULL. edit: I was wrong, see below.
- unwind 9y agoIt's a global static without an initializing expression; it is set to 0 before main() starts. 0 for a function pointer is of course the same as nullptr/NULL.
- anon1385 9y agoC11 6.7.9 (10): If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate. If an object that has static or thread storage duration is not initialized explicitly, then: — if it has pointer type, it is initialized to a null pointer;
- deleted 9y ago[deleted]
- sigjuice 9y agoHow is the C11 standard applicable to a C++ program?
- anon1385 9y agoI didn't notice it was C++. In C++14: From 3.6.2: >Variables with static storage duration (3.7.1) or thread storage duration (3.7.2) shall be zero-initialized (8.5) before any other initialization takes place and from 8.5: >To zero-initialize an object or reference of type T means >--if T is a scalar type (3.9), the object is initialized to the value obtained by converting the integer literal 0 (zero) to T and from 3.9: >Arithmetic types (3.9.1), enumeration types, pointer types, pointer to member types (3.9.2), std::nullptr_- t, and cv-qualified versions of these types (3.9.3) are collectively called scalar types
- caf 9y agoicc does this as well on the same example.
- HelloNurse 9y agoGCC is the most fun, jmp straight into the uninitialized function pointer.
- deleted 9y ago[deleted]
- deleted 9y ago[deleted]
- titzer 9y agoThis is a technically correct compilation of the given C program. All executions of a program containing undefined behavior are invalid. That means, not just the part of execution "after" the undefined behavior, but the part of execution "before" as well. Since the program unconditionally calls an uninitialized (actually zero-initialized) function pointer, that is undefined behavior. It therefore has no valid executions. The compiler is legally allowed to generate whatever code it wants, and that is considered valid according to the standard. It just so happens in this case that it probably generated no code at all for main, falling through to code that it did generate. (and my guess as to why it did generate code for NeverCalled() is because it's non-static and could be called by functions outside this compilation unit; NeverCalled keeps EraseAll() alive.) edit: the above is only true if the compiler knows main is the entrypoint to the program.
- sddfd 9y agoI agree that the generated code is leagal wrt. the standard and that code for NeverCalled is generated because it is visible outside the translation unit. However, to me it looks as if the assembly contains code for main (label main: is present) and EraseAll got inlined into main. This seems reasonable once you believe that the compiler can, due to undefined behavior, assume DO to be EraseAll.
- dom0 9y agoNo, if you try this with the various optimization levels you can see that it's not a bug; without optimization it doesn't happen. Instead what seems plausible to me is that some optimization pass analyses the possible values for "Do", which is tractable, since it's static and can't be aliased. Because the value must be initialized before Do() the optimizer seems to unconditionally use the only possible initialization, even if it is unreachable. With -O1 you can see that this part already happens. With optimization cranked further up it just inlines EraseAll, and EraseAll ceases to exist as a separate symbol. This is possible since Do is static and not aliased. Meanwhile NeverCalled continues to exist as a retq, since it is not static.
- titzer 9y ago
- sddfd 9y agoSo, the compiler finds out that the function pointer Do is either undefined or points to EraseAll, and since calling an undefined function is undefined behavior, the compiler assumes that 'Do' always points to EraseAll? Is this what is happening? While this is certainly unexpected, I can't fault the compiler here. The example emphasizes the need for tools to check for undefined behavior.
- dzdt 9y agoThe undefined behavior is that the static variable "Do" (a function pointer) is used without initialization. The compiler optimization is that since, in the whole program, the only value to ever be assigned to "Do" is the function "EraseAll", the compiler statically initializes "Do" to "EraseAll". Doesn't seem too crazy.
- arximboldi 9y agoActually the optimization is not statically initializing Do, it is inlining the function calls via the Do pointer. I actually think this a quite useful optimization. You can see that if you "fix" the program by checking Do before calling it, it still inlines the call in the True branch: https://godbolt.org/g/9KS2r9 https://godbolt.org/g/9KS2r9
- _pmf_ 9y agoOf course, in some cases NULL checks will be optimized out (this is why GCC has acquired the -fno-delete-null-pointer-checks flag ...). At one point, this has been a huge source of conflict: basically, when you have something like if (NULL != s) { s->a = 1; } s->b = 1; then GCC may optimize away the NULL check because referring to s->b after the block protected by the NULL check indicates that the NULL check is superfluous. I this case, this is actually true, but in cases that involve aliasing that is not detected by dataflow analysis, there might be circumstances where the NULL check is required. Discussion of something related is here[0]; my example might not be very good. An even more insidious case is [1](strong language), where manual overflow checks are optimized out because the overflow technically invokes UB. Compiler optimization gone wild. [0] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71892 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71892 [1] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475
- pxndx 9y agoYour example also presents undefined behaviour, so the compiler is free to do whatever it wants.
- Piskvorrr 9y agoStill better than having demons fly out of your nose.
- exikyut 9y agoSo, is this on the underhanded C shortlist yet? I wonder how many static analysis tools will pick up that the function will be run. And then I start wondering if the static analysis tool "scene" realizes how important their work is for security...
- unwind 9y agoI would hope none, since no static analysis tool can make that assumption without knowing the exact compiler you're thinking about and a great deal else. This is not the "expected" behavior; undefined behavior doesn't have an expected behavior. This is what the compiler at godbolt.com happens to generate for this particular code. At best, an analysis tool can warn that the code has undefined behavior.
- justin66 9y ago> At best, an analysis tool can warn that the code has undefined behavior. The least obvious things, like undefined behavior, are exactly the sort of thing a static analysis tool should be expected to warn about.
- junke 9y ago> the function will be run. You can't be sure, that's just what one compiler does today. Static analysis tools tend to refer to the standard. $ tis-analyzer foo.c -val /tmp/foo.c:16:[kernel] warning: Function pointer and pointed function have incompatible types. Pointer type: int () Non-function: {0} assert \valid_function(Do); .... /tmp/foo.c:16:[value] Assertion 'Value,function_pointer' got final status invalid. In other words, the pointer is not initialized.
- gambiting 9y agoWhat worries me is that almost every other compiler supported by godbolt would wipe your drive. It's not an anomaly of Clang - it's something fundamentally wrong in the design of compilers that allows them to do this at right optimization levels. Which means that if I have a method that wipes my entire production database but never ever call it, it might still get called because of mistake like this one where someone forgot to initialize a function pointer.
- ben0x539 9y agoThis is a really cool, graphic example of the importance of avoiding undefined behavior even if you can't see it segfault or whatever.
- Davidbrcz 9y agoOn that subject: CppCon 2016: Michael Spencer “My Little Optimizer: Undefined Behavior is Magic" (https://www.youtube.com/watch?v=g7entxbQOCc https://www.youtube.com/watch?v=g7entxbQOCc)
- alkonaut 9y agoDo compilers emit warnings here (do they usually do that when there is a known UB?) Here it's clear that "NeverCalled" is never called so it's clear that Do is never assigned, so the Call can't produce anything useful. EDIT: Ugh missed the lack of "static" on the NeverCalled. Never mind. Is this too hard to decide with certainty in C because of how you could do some pointer gymnastics and just fiddle with the field without the compiler realizing, so in order not to spit out false positives, compilers just have to assume the author knows what he's doing and allow the suspected UB without warning? I'm just used to higher level "safe" languages (Rust, C#, etc), where lack of assignment is usually determined with certainty.
- kbeckmann 9y agoWith clang you can enable run time UB checks using the -fsanitize=undefined flag[0]. But in this case, the sanitizer doesn't detect undefined behavior. I tried to build the example (and changing the system to an echo) using "clang a.cpp -o a -Os -std=c++11 -lstdc++ -fsanitize=undefined", but the sanitizer didn't detect the UB. [0] https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
- _pmf_ 9y ago> but the sanitizer didn't detect the UB. Isn't this because the sanitizer works with the instrumented "optimized" code, which does no longer invoke UB? Or is the clang toolchain intelligent enough to not perform UB dependent optimizations when the sanitizer is specified?
- umanwizard 9y agoWhat would it even mean for compiled, machine language code to invoke UB or not? UB is mostly a C concept, not a lower-level one. The semantics of assembly language programs are well-defined much more often (the only counterexample I can think of is stuff involving using threading, memory barriers, etc. incorrect)
- batat 9y ago
- jbb67 9y agoThis might well be valid according to the standard, but I find it very depressing how many people consider it reasonable behavior.
- sddfd 9y agoIf we want zero-overhead abstraction (i.e. don't generate null checks in front of every division, etc.) and a (reasonably) safe language, more static analysis is in order. The case in point is Rust: Its type system is basically an elaborate static analysis. Rusts advantage over tools like UBsan is that the programmer expects to interact with the type system. Many static analysis tools get rejected by programmers, because the expectation is that they "just work", when really one needs to interact with such a tool. This means bidirectional communication: The programmer tells the tool what invariant he wants to get, and the tool tells the programmer which invariants it is missing.
- umanwizard 9y agoWhy? This sort of optimization is necessary in order to make programs run fast.
- megous 9y agoWhat kind of optimization? I'd rather this kind of code would crash on me, like the code that gcc compiles out of this, rather than compiler silently doing crazy inferences out of obviously buggy code.
- umanwizard 9y agoFor this particular edge case, yes it seems absurd. However, many optimization passes are based on simplifying code by figuring out what range of values something can take. Most optimization passes don't know what the original code looks like, they only see it after it's been passed through a bunch of other optimization passes and preprocessing, so they can't reason about whether a particular optimization would seem "completely natural" or "abuse of UB" to the original programmer.
- 9y ago
- MichaelBurge 9y agoIf you rename "NeverCalled" to "Initialize" and "main" to "ExecuteAfterInitialization", the behavior is much less mysterious. NeverCalled is a "public" function since it isn't marked static, so the author of this code intended for it to be called externally to initialize Do. The compiler's optimization produces a correct program along the only valid execution path: Compile this as an object file, and link it with some other code that calls NeverCalled and then main. If you mark NeverCalled as static, the compiler outputs an explicit "Undefined Instruction", because there are then no execution paths that don't invoke undefined behavior.
- HelloNurse 9y agoNo, there is no valid execution path because of undefined behaviour. It doesn't cease to be undefined because of imaginary additional code. Compilers are not in the business of creatively guessing what "the author of this code intended".
- MichaelBurge 9y agoIt's not creative or guessing. If you write a function without "static" it means you intended for it to be called externally. It's not theoretical at all: Before I saw this thread, I was in the middle of writing some Haskell that loads some non-main symbols from an object file derived from an executable's source code, and invokes them. People carefully mark which functions are or are not static to ensure their executables don't get bloated. I'd be very annoyed if my C compiler didn't take symbol visibility into account when optimizing, because I depend on it all the time.
- HelloNurse 9y agoIf I write a function without "static" it means I want to allow functions in other modules module to call it, it isn't implied that it will be actually called. Real life analogy: declaring a static function is like locking your car because you don't want it stolen: only hackers can access it through forbidden means. If instead you leave your nonstatic car parked in the street, open and with keys in the ignition, you are making it easy to steal (probably you want it to be stolen, there might be an UB corpse in the trunk you want to get rid of), but you cannot be sure that a thief will actually come along and take it away; you are just advertising that they should.
- narrator 9y agoI just read the C++ Primer because I wanted to hack on some C++ code and the number of things you can do in C or C++ that result in "undefined behavior" is annoyingly large. There are some functions where they could have added a bounds check to the implementation, but the whole philosophy of the language seems to be that when choosing between lack of bult-in checks that would lead to easy to make errors that would cause undefined behavior and small speed increases on microbenchmarks the language designers choose the latter.
- maxlybbert 9y agoFor what it's worth, in Design and Evolution of C++, Stroustrup says that when he was designing features, he would often think of a simple rule that he couldn't enforce (e.g., "couldn't enforce" being "if you violate this rule, the results are undefined") and a complex rule that he could enforce. He generally tried to pick the simpler, easier to teach rule. Unfortunately, that means there's more undefined behavior if those simpler rules are violated.
- Upvoter33 9y agoCompilers (and thus, compiler writers, i.e., people) have gone off the deep end with C. In search of endless over-optimization (don't worry, they will tell you that this optimization is "important" because some stupid benchmark runs 21% faster), programmer intent (as well as mistakes) are turned into bizarro behavior. Why we accept this is beyond me. For C, in particular, it has made writing code that uses plain old C ints way more challenging than it needs to be, among other things. We need to rethink what is important: a little more performance, or representing (to our best possible guess) what the programmer actually intended. </rant>
- tom_mellior 9y ago> representing (to our best possible guess) what the programmer actually intended. If the programmer intended to write a correct program, and the programmer wrote an undefined program instead, you should blame the programmer, not the compiler.
- maxlybbert 9y ago> We need to rethink what is important: a little more performance, or representing (to our best possible guess) what the programmer actually intended. I remember reading an old book about implementing a compiler. The book actually recommended that compilers should make an effort to fix simple mistakes; e.g., compilers should add semicolons if needed, check whether an undefined variable or function was actually a misspelled variable or function, etc. I wasn't sure whether I should laugh at the naivety or shudder about what kind of code that would have led to. Especially given that each compiler would be lenient in different ways from competing compilers.
- umanwizard 9y agoThis is basically what happened in real life with HTML, with the results you predict.
- umanwizard 9y agoDo you have an example of an optimization pass you would turn off in Clang? I mean, do you actually know which one you'd turn off and what the pros and cons are. Not just answer like "of course, whichever one caused this behavior!" I'm asking because I noticed nobody who rants about compiler behavior on UB code actually seems to have a detailed understanding of what they're talking about. They always seem to just be shooting from the hip.