17 ms·
A Guide to Undefined Behavior in C and C++ (2010)
- jbandela1 3y agoIf you want some nice examples of how undefined behavior results in weirdness, see https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=633 https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=63... An interesting example from there is how the compiler can turn int table[4]; bool exists_in_table(int v) { for (int i = 0; i <= 4; i++) { if (table[i] == v) return true; } return false; } Into: bool exists_in_table(int v) { return true; }
- JonChesterfield 3y agoThe amazing part about examples like that is people read them, check that the compiler really does work on that basis, and then continue writing things in C++ anyway. Wild. Suppose I should expand on this. The idea seems to be either 1/disbelief - compilers wouldn't really do this or 2/ infallibility - my code contains no UB. Neither of those positions bears up well under reality. Programming C++ is working with an adversary that will make your code faster wherever it can, regardless of whether you like the resulting behaviour of the binary. I suspect rust has inherited this perspective in the compiler and guards against it with more aggressive semantic checks in the front end.
- _eojb 3y agoIt's just as "amazing" to read these takes from techno purists. You use software written in C++ daily, and it can be a pragmatic choice regardless of your sensibilities.
- JonChesterfield 3y agoDefinitely. There's loads of value delivered by C++ implementations, including implementations of C++ and other languages. The language design of speed over safety mostly imposes a cost in developer / debugging time and fear of upgrading the compiler toolchain. Occasionally it shows up in real world disasters. I think we've got the balance wrong, partly because some engineering considerations derive directly from separate compilation. ODR no diagnostic required doesn't have to be a thing any more.
- johnbellone 3y agoBut it isn’t Rust.
- angiosperm 3y agoHardly anything is. Literally none of the programs on my machine are coded in Rust. (Firefox is reputed to have a bit in it.)
- jacquesm 3y agoAbout FF and Rust: https://news.ycombinator.com/item?id=30743577 https://news.ycombinator.com/item?id=30743577
- dralley 3y agoIf you're running Windows 11, you have Rust running in your kernel. And also some userspace system libraries. https://www.bleepingcomputer.com/news/microsoft/new-windows-11-build-ships-with-more-rust-based-kernel-features/ https://www.bleepingcomputer.com/news/microsoft/new-windows-... Someone else posted statistics that show Firefox being 10% Rust, but I'm not sure it makes sense to include HTML and Python and JavaScript in the comparison. If you compare Rust against C/C++, it's 20%
- angiosperm 3y agoWho runs Windows? There is no such language as C/C++. I presume you mean the sum of C and C++, and that you omit external libraries from the tally.
- dralley 3y ago> I presume you mean the sum of C and C++ "I'd just like to interject for a moment. What you're refering to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX." Isn't this a tad pedantic? You obviously understood what I was saying. >and that you omit external libraries from the tally. Mozilla vendors their dependencies. They're counted.
- erik_seaberg 3y agoAnd we have the core dumps to prove it. When any Costco sells a desktop ten thousand times faster than the one I started on, we can afford runtime sanity checks. We don’t have to keep living like this, with stacks that randomly explode.
- _eojb 3y agoI don't know what line of work you're in, but I use a desktop orders of magnitude faster than my first computer also, and image processing, compilation, rendering, and plenty of other tasks aren't suddenly thousands of times faster. Not to mention that memory safety is just one type of failure in a cornucopia of potential logical bugs. In addition, I like core dumps because the failure is immediate, obvious, and fatal. Finally, stacks don't "randomly explode." You can overflow a stack in other languages also, I really just don't see what you're getting at.
- erik_seaberg 3y agoI don’t mean a call stack, I mean “stack” in the LAMP sense—the kernel, drivers, shared libraries, datastores, applications, and display servers we try to depend on.
- dureuill 3y ago> Not to mention that memory safety is just one type of failure in a cornucopia of potential logical bugs. You can die of multiple illnesses so there's no point in developing treatment for any particular one of them. > I like core dumps because the failure is immediate, obvious, and fatal. Core dumps provide a terrible debugging experience, as the failure root cause is often disjoint from the dump itself. Not to mention that core dumps are but one outcome of memory errors, with other funnier outcomes such as data corruption and exploitable vulnerabilities as likely. Lastly, memory safe languages throw an exception or panic on out of bound access, which can be made as immediate and fatal as core dumps. And much more obvious to debug, since you can trust that the cause starts indeed at the point of failure
- patrick451 3y agoI dunno, my computers seems to keep running slower and slower despite being faster and faster. I blame programmers increasingly using languages with more and more guardrails which are slower. I'd rather have a few core dumps and my fast computer back.
- peppermint_gum 3y ago>The amazing part about examples like that is people read them, check that the compiler really does work on that basis, and then continue writing things in C++ anyway. Wild. Well, in modern C++ this code would look like this: std::array<int, 4> table; bool exists_in_table(int v) { for (auto &elem : table) { if (elem == v) return true; } return false; } Or even simpler: std::array<int, 4> table; bool exists_in_table(int v) { return std::ranges::contains(table, v); } There's no shortage of footguns in C++, but nonetheless, modern C++ is safer than C.
- mike_hock 3y agoWeirdly, GCC fails to optimize this, but Clang does (if you make the table static as in the original example).
- gizmo686 3y agoI actually would prefer to get the second output. The result is wrong, but consistantly and deterministically so. The naive implementation of the broken code is a heisenbug. Sometimes it will work, and sometimes it won't, and any attempt to debug it would likely perterb the system enough to make the issue not surface. It wouldn't suprise me if I have run into the latter situation without relizing it. When I got the the problem, I would have just (incorrectly) assumed that the memory right after the array happened to have the relevent value. I would be counting my blessings that it happened consistantly enough to be debuggable.
- jll29 3y agoI agree that it is better to get deterministic and predictable behavior. Reminds me of when for a while, I worked on HP 9000s under HP-UX and in parallel on an Intel 80486-based Linux box, and what I noticed is that the Unix workstations crashed sooner and more predictably with segmentation faults than Linux on the PC (not sure if this has changed since the early 1990s - probably had to do with the MMU); so developing on HP under Unix and then finally compiling under Linux led to better code quality.
- jandrewrogers 3y ago> The amazing part about examples like that is people read them, check that the compiler really does work on that basis, and then continue writing things in C++ anyway. That isn't idiomatic C++ and hasn't been for a long time. Sure, it's possible to do it retro C-style, because backward compatibility, but you generally don't see that in a modern code base.
- JonChesterfield 3y agoThe modern codebase has grown from a legacy one. The legacy one with parts of the codebase that were C, then got partially turned into object oriented C++, then partially turned into template abstractions. The parts least likely to have comprehensive test coverage. That place is indeed where a compiler upgrade is most likely to change the behaviour of your application.
- jcelerier 3y agoevery day new greenfield projects start in C++ - nowadays, 20, 23...
- uecker 3y agoThere are plenty of similar things in C++. I do not think C++ is safer than C. std::vector does not do bounds by default checking last time I checked.
- Arech 3y agoAnd thank G-d it doesn't do it!
- TwentyPosts 3y agoRemind me, how is this a good thing again? Especially considering that (if you write modern C++) the compiler should optimize away bound checks most of the time (and in all critical places) either way.
- Gibbon1 3y agoWhat's amazing is programmers haven't tared and feathered the standards committee and compiler writers for allowing crap like that.
- saagarjha 3y agoThey've tried. Language lawyers are very good at their jobs, though.
- Gibbon1 3y agoI think at this point unfortunately regulators needs to start getting involved.
- uecker 3y agoWell, the argument brought up is that users want it this way, so this is existing practice which is implemented and should be standardized. So please complain and file bugs. Also use the compiler and language feature that help, such as variably-modified types such as bare pointers, attributes, compiler flags, etc.
- _gabe_ 3y ago> check that the compiler really does work on that basis, and then continue writing things in C++ anyway. Wild. My compiler (MSVC) doesn't do that[0]. Clang also doesn't do this[1]. It's wild to me that GCC does this optimization[2]. It's very subtle, but Raymond Chen and OP both say a compiler can create this optimization, not that it will. [0]: https://godbolt.org/z/bdx4EMzxe https://godbolt.org/z/bdx4EMzxe [1]: https://godbolt.org/z/z833Wa391 https://godbolt.org/z/z833Wa391 [2]: https://godbolt.org/z/6b8aq59M9 https://godbolt.org/z/6b8aq59M9
- fluoridation 3y agoWhat's odd about that example is that the optimization is only valid if the loop in fact overflows the array every time. So the compiler is proving that the array is being overflowed and rather than emitting a warning to that effect, it generates absurd code.
- kllrnohj 3y ago> So the compiler is proving that the array is being overflowed and rather than emitting a warning to that effect <source>:5:13: warning: unsafe buffer access [-Wunsafe-buffer-usage] 5 | if (table[i] == v) return true; | ^~~~~ https://godbolt.org/z/zGxnKxvz6 https://godbolt.org/z/zGxnKxvz6 This one is weirdly hard to get a compiler warning out of which is a fair critque, but so many of the "Look what the compiler did to my UB silently!" issues are not at all silent and would have been stopped dead with "-Wall -Wextra -Werror"
- iainmerrick 3y agoAs noted elsewhere in this thread, GCC by default does the "optimization" and doesn't warn. No doubt there are other examples where Clang is the one that misbehaves. How are we supposed to know whether our code is being compiled sensibly or not, without poring over the disassembly? Just set all the warning flags and hope for the best?
- UncleMeat 3y agoI think that a big problem is that for every compile that seems "not sensible" and is actually not sensible, there are 100s or 1000s of compiles that would look absolutely insane to a human but are actually exactly what you want when you sit down and think about it for a long time. Almost all of the "don't do the overly clever stuff!" proposals would throw away a huge amount of actually productive clever stuff.
- fluoridation 3y agoI think what the GP means by "not sensible" is that proving that the code is broken in order to silently optimize it more aggressively is not sensible. If your theorem proven can find a class of bugs then have it emit diagnostics. Don't only use those bugs to make the code run faster. Yes, make the code run faster, but let me know I may be doing something nonsensical, since chances are that it is nonsensical and it doesn't cost anything at run time.
- vkazanov 3y agoThis is a great example. There's an obvious UB, the compiler sees it at compile time and should just stop the programmer from doing the mistake at all times. Instead, it's totally possible to just let the programmer tear his leg off for no clear reason.
- uecker 3y agoNote that not all claims you find about UB on the internet are true. For example, in C, UB can not time-travel before observable behavior. And in general UB can not time-travel before any function call when the compiler can not show that the function returns to the caller (MSVC got this wrong though).
- zer8k 3y ago> In the long run, unsafe programming languages will not be used by mainstream developers, but rather reserved for situations where high performance and a low resource footprint are critical. I see no world where so-called "unsafe" languages would not be used. Most graduates of Computer Science programs can, perhaps with some trouble, implement a half decent C compiler in a weekend or two. This is not a footnote. This fact alone means that for any given piece of hardware you're more likely to find a random C compiler you can use than anything else. Rust, being the most likely contender to replace it, still cannot self-host and the grammar is exponentially more complicated than C. It is more like C + <whatever> will co-exist peacefully than something like C being replaced (even ignoring the millions of lines of code that already exist). Not for performance reasons but more that you can churn out a C compiler quickly for almost anything given a spec of the hardware. On topic, I find a desk reference for this is very useful. The CERT C standard is pretty good to thumb through even if you don't adhere to every suggestion.
- dale_glass 3y agoI don't see how that reasoning is supposed to work in modern times. Who out there is seriously using a compiler churned out in a weekend? The fact that you can do it doesn't mean anybody seriously would use that. We're also not really creating architectures anymore. There's RISC-V, and Rust already supports that.
- zer8k 3y ago> Who out there is seriously using a compiler churned out in a weekend? Someone at a chip manufacturer writing something for a brand new chipset, for example. It takes a long time to get stuff shoved into GCC. It's only in recent history has life settled on one or two "big" compilers. There are still plenty of other places where you will find bespoke compilers. Perhaps not commonly, but they do exist (especially in embedded).
- tempest_ 3y agoAre they actually bespoke compilers, or just some decade old fork of gcc that works for that chipset and is never updated again? ( I am not being facetious )
- AnimalMuppet 3y ago(2010)
- dang 3y agoAdded. Thanks!
- matt3210 3y agoWhat behaviors are undefined in rust? Oh wait nobody knows, since it has no standard or language spec.
- lionkor 3y agoThe ultimate "the code is the documentation" is "the compiler is the language spec".
- thesuperbigfrog 3y ago>> The ultimate "the code is the documentation" is "the compiler is the language spec". Rust has a great potential to become a replacement for C and C++, but the lack of a language specification is a shortcoming that needs to be addressed for it to see wider adoption, especially for safety-critical systems. If the Rust compiler does something surprising, people will ask, "Is this a bug?" and without a spec the answer becomes the language developers or the community asking, "What should the compiler do in this situation?". It makes sense because the correct behavior (whatever that is) has not been defined, but it has a feeling of "we are making this up as we go along" because there is no formalized answer defined. While this approach is fine for running your website or building a command line tool, it is not acceptable for safety-critical software. If the software breaks and people die, the "we are making this up as we go along" approach is not acceptable because it has too much risk.
- lionkor 3y agoI fully agree, and its definitely a strange feeling coming from C++ to not have a single, complete and extensive spec to read up on if all else fails. I want to like Rust, but its already a kitchen sink on par with C++ in complexity and misused quirks, not to mention macros which hide complexity just like C macros did, that the lack of a committee and spec makes it very difficult to trust that it won't get more and more features as time goes on (becoming like C++, in only the bad ways). I understand they have an RFC process, but thats not enough for a language which is now so commonplace in discussion (usually in the form of "if you did it in Rust, this problem wouldnt exist", which is often even true).
- Animats 3y agoThe three big questions: 1. How big is it? 2. Who owns it? 3. Who locks it? Most undefined behavior in C/C++ involves those three questions. #1 is historically the most troublesome. And the most inexcusable. Pascal, which predates C, didn't have that problem, because arrays carried size info. Nor did Algol, Modula I, Modula II, and Modula III. Modula I was a very low level language - device registers were a language concept. Something I wrote on this back in 2012.[1] There was some consensus at the time that this would work and would be backwards compatible with C. But it would be a tough sell, and I didn't want to spend my life selling it. [1] http://animats.com/papers/languages/safearraysforc43.pdf http://animats.com/papers/languages/safearraysforc43.pdf
- JonChesterfield 3y agoI think a C implementation with overhead instead of UB is implementable. I'd like to know what the fundamental performance delta we get from UB is. Likewise not sure it's the right choice for my life's work.
- Quekid5 3y agoThe MINIMUM baseline is probably somewhere around ASAN/UBSAN/etc. and those aren't exactly cheap... and they don't even promise to catch all the problems. The problem is that almost every single little thing you can do in C has potential for UB, even just the + operator. So it would absolutely come at a HUGE performance cost, unfortunately. More esoteric stuff is: If you do pointer arithmetic that technically goes out of bounds and then in bounds again... that's technically UB (can't remember if this is C++ only or both), so you can't rely on knowing where everything is + bounds checks.
- CryZe 3y agoI'd say that those are not actually the minimum. You don't need to detect undefined behavior, you just need to "make it implementation defined", so integers could just wrap around without needing to check for overflow, unitialized memory would just be random bytes, and so on. At least that would be one way to do it. Of course the checks would ensure the program shuts down more cleanly.
- Joker_vD 3y ago> Case 2: (b == 0) || ((a == INT32_MIN) && (b == -1)) > A Java compiler, in contrast, has obligations in Case 2 and must deal with it (though in this particular case, it is likely that there won’t be runtime overhead since processors can usually provide trapping behavior for integer divide by zero). Actually, there will be runtime overhead on x86/x64: Java mandates that Integer.MinValue / (-1) evaluates to Integer.MinValue (see 15.17.2. "Division Operator /" of the Java Language Specification) but IDIV instruction raises #DE in such circumstance. So the JITter actually emits cmp eax, 0x80000000 jne .normalCase xor edx, edx cmp $reg, -1 je .specialCase .normalCase: cdq idiv $reg .specialCase: code sequence as you can see in its source ([0][1]) instead of simplistic "cdq; idiv $reg": because it does not want trapping behaviour in this particular case; but e.g. AArch64 doesn't trap neither division by zero nor INT_MIN / -1. That's why accurately implementing your language's semantics on different platforms is so annoying and why C standard left itself a nice shortcut. [0] https://github.com/openjdk/jdk/blob/d27daf01d6361513a815e78327961f9367887163/src/hotspot/cpu/x86/c1_LIRAssembler_x86.cpp#L2632 https://github.com/openjdk/jdk/blob/d27daf01d6361513a815e783... [1] https://github.com/openjdk/jdk/blob/d27daf01d6361513a815e78327961f9367887163/src/hotspot/cpu/x86/macroAssembler_x86.cpp#L1938 https://github.com/openjdk/jdk/blob/d27daf01d6361513a815e783...
- fluoridation 3y agoOn the other hand, C left the burden of implementing portable semantics to its users.
- Joker_vD 3y agoYes, but when C was being made, the application-level programmers knew the quirks of the platforms they used just as well as the compiler writers because they were almost precisely the same people.
- dang 3y agoRelated: A Guide to Undefined Behavior in C and C++ (2010) - https://news.ycombinator.com/item?id=18372613 https://news.ycombinator.com/item?id=18372613 - Nov 2018 (103 comments) A Guide to Undefined Behavior in C and C++ (2010) - https://news.ycombinator.com/item?id=9884074 https://news.ycombinator.com/item?id=9884074 - July 2015 (10 comments) A Guide to Undefined Behavior in C and C++, Part 1 - https://news.ycombinator.com/item?id=2544159 https://news.ycombinator.com/item?id=2544159 - May 2011 (2 comments)
- zabzonk 3y agoperhaps it is just me, but i have never experienced any of the problems outlined in the comments here, despite of writing a shedload of C and C++ code (and fortran, assembler and other stuff). and i don't think i am a coding god.
- kazinator 3y agoArticle misses the angle that "undefined behavior" is a formal term which means that ISO C doesn't define behavior, not that nobody whatsoever defines the behavior. The following is undefined behavior: #include <unistd.h> A conforming implementation (one that obviously isn't POSIX) can behave such that the hard drive is wiped out either at compile time, or at the run-time of the translated and linked translation unit. ISO C defines the behavior up until specifying that the header is searched for somehow and that a diagnostic is required if it is not found. Since the header is nonstandard, it's possible that it is found. And then, the standard says nothing about it.
- spacechild1 3y agoCould it be that you're confusing "undefined behavior" with "implemented-defined behavior"?
- kazinator 3y agoNo, it coudln't. If you think so, find the chapter and verse in ISO C which says that implementors must choose a behavior for #include <unistd.h> and document it ("implementation-defined") or without being required to document it ("unspecified"). Can you prove that a conforming implementation of any version of ISO C, from the standard alone, may not segfault or wipe storage device as a result of #include <unistd.h>? (I.e. that if that were to happen, the implementation would be nonconforming?) When a POSIX implementation gives you #include <unistd.h>, it is "behaving in a documented manner characteristic of the implementation": one of the possible ways that undefined behavior plays out!
- spacechild1 3y agoI have to admit that your original post confused me. Why exactly do think that #include <unistd.h> is undefined behavior? Here's what the C11 standard draft says in 6.10.2 (source file inclusion): A preprocessing directive of the form # include <h-char-sequence> new-line searches a sequence of implementation-defined places for a header identified uniquely by the specified sequence between the < and > delimiters, and causes the replacement of that directive by the entire contents of the header. How the places are specified or the header identified is implementation-defined. Seems pretty well-defined to me. EDITED to include C standard reference.