7 ms·
Undefined Behavior in C and C++
- rwallace 3y agoNot the first discussion of this topic, by any means. In this case, I've tried to boil it down to the essential points a practical programmer needs to know, but the article still ended up longer than I initially aimed for.
- wrs 3y agoOne hopefully constructive comment… I didn’t find this a motivating example as intended: int foo_or_bar(int which) { // Assumes you don't mind both functions being called int x = foo(); int y = bar(); return *(&x + which); } The argument being (if I understood right) if x has to have an address, it can’t be put in a register, so that must be UB or we can’t use registers. Well, how about the rule is that if I take the address of x, it can’t be put in a register? That seems like an obvious rule, and I seem to remember that was a safe assumption before the “great UBification” of compilers. I’m sure there’s a better example of why UB helps optimization, but this one didn’t work for me.
- josephcsible 3y ago> Well, how about the rule is that if I take the address of x, it can’t be put in a register? The issue is that y might end up in a register, and you didn't take the address of y.
- wrs 3y agoOhhh. I misunderstood the example. That would not only depend on a lack of register usage, but depend on where the variables are stored on the stack, which I don’t think anyone could reasonably demand even in 1976. So I still feel there must be a more reasonable and therefore motivating example of optimizations one would want that are enabled by surprising you with UB. (Which I think was the idea behind this example.)
- int_19h 3y ago> depend on where the variables are stored on the stack, which I don’t think anyone could reasonably demand even in 1976 You'd be surprised. A typical compiler of that era would be single-pass, and allocating variables on the stack in order in which they were declared was not uncommon. Don't forget, we're talking about a language that actually had a "register" keyword solely to tell the compiler to enregister the variable!
- wrs 3y agoWell, now that you mention it, that’s true, I was there. :) But I just want to go back to 1992, not 1976.
- tyfighter 3y agoI keep finding myself angry about the recent (some number of years) focus on C and C++'s undefined behavior. I have been writing C and C++ for 27 years, 16 years professionally, and despite all the scary implications, I do not understand why ANYONE cares. I do not get it. This is yet another article that goes on and on about nonsensical situations that are just shitty code. Integer overflow? Who cares? Unless you're targeting a specific compiler and architecture, it doesn't matter. C and C++ have footguns. Everyone knows that. Who cares? I am anger commenting, because I'm just sick of this, but this article still says nothing to convince me that any of this matters.
- rwallace 3y agoRight. To be clear, the purpose of the article is not 'zounds, C and C++ have footguns!' but 'C and C++ have footguns – yeah, this is not exactly breaking news – but here is a hopefully helpful summary of where they are, why they exist, and what you can do to avoid them'. If you are already satisfied you know how to avoid them, and you don't need any more help with that, then you are not the target audience, and should by all means ignore the article.
- AnimalMuppet 3y agoBut tyfighter has some reason. People take such articles, and use them to beat up anyone writing C++, arguing that they are stupid to use such an undependable tool. So, yes, people who know what they're doing can ignore such articles, as a first-order effect. But there are second-order effects from such articles, and while they don't change anything, they are rather unpleasant. Hence tyfighter's anger - he gets tired of being on the receiving end of the fallout from such articles.
- rwallace 3y agoI do actually sympathize with that! I tried to keep a level tone, and maximize the ratio of useful information to flame-war ammunition, but that ratio unfortunately has an upper bound well short of infinity.
- 3y ago
- Jun8 3y agoHere's another interesting post if you want to delve further into an example of undefined behavior created by gcc optimization: https://thephd.dev/c-undefined-behavior-and-the-sledgehammer-guideline https://thephd.dev/c-undefined-behavior-and-the-sledgehammer.... Also, this quote comes to mind: "C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off": https://www.stroustrup.com/quotes.html https://www.stroustrup.com/quotes.html
- deleted 3y ago[deleted]
- photochemsyn 3y agoHere's some more on gcc optimization from 2010, discusses how GCC optimization started eliminating null pointer checks in Linux kernel code necessiting compiling at a lower optimization level (towards the bottom). On the 'good' side, it also mentions tight loops can speed up 30-50% if the compiler can ignore signed integer overflow. Also has 'best practices' list: https://blog.regehr.org/archives/213 https://blog.regehr.org/archives/213
- mjevans 3y agoOnce again, I want to plead. At least have a Warning option to annotate any time undefined behavior is encountered by a compiler. The goal should be to promote optimizations to written code and improve code quality. Not just the result of one particular compiler.
- layer8 3y agoUndefined behavior are runtime conditions, in the general case, not compile-time conditions.
- wrs 3y agoEliminating a statement by assuming UB will never happen is a compile-time condition. I think the problem is more that it’s not as if there’s a single place in the compiler saying “aha! UB! let’s surprise the developer!”. It’s the effect of propagation through multiple optimization steps.
- layer8 3y agoNot quite, it’s the fact that if you have to assume the UB condition and make behavior defined for that condition, then you can’t apply certain optimizations, and/or you have to generate extra code to detect and handle the UB condition. In any case, it would mean that existing programs that do not exhibit UB (but that contain expressions that could be UB when executed in the context of a different program) would suddenly compile to less efficient code. It’s not surprising that compiler vendors have little interest in agreeing to such changes to the C standard, which effectively would mean a performance regression for their compilers. You can’t change the situation without either forfeiting some performance or changing existing programs. This is how UB came about in the first place, in the first ANSI C version. Everything the compiler vendors couldn’t agree on to specify even just an implementation-defined behavior for, became UB.
- nlewycky 3y agoHi! I'm a former compiler engineer who specialized on undefined behaviour. Would you like warnings on: * int f(int x, int y) { return x + y; } * int get_x_coord(Point *p) { return p->x; } * void compute_and_cache(const char *key) { *get_cache_bucket_for(key) = compute_value_for(key); } I'm curious, what would you do with a warning on every load or store through a pointer? On the flip side, I can offer -fsanitize=undefined which will catch when you do many things that have UB at runtime. It does not change the ABI which means that there are some bugs it can't catch, but deploying it is easier since you do not need to recompile all your libraries with it (like your C++ standard library and C library, in particular). You can use this to help you build unit tests that send intentionally overflowing values into your functions and show that they do not overflow. It turns untestable problem (since you cannot check for UB after it happens) into a problem you can write deterministic tests for.
- olliej 3y agoA core part of the problem of UB in C and C++, is that it is gratuitously over applied. Mercifully the article calls out the BS argument of "old hardware" justifying UB. It is simply a false argument. The overwhelming majority of UB in C and C++ should be either implementation defined or unspecified behaviour. Security vulnerabilities due to overflow or null dereferences being UB should never have been possible because there are no platforms in which those operations are not defined (some trap, some wrap, some go to infinity), but that is all under the banner of implementation defined behavior. Labelling these things as UB is _solely_ to allow performance optimizations in narrow cases, at the cost of safety in all cases. In committee meetings I've been in recently the new refrain I'm hearing/reading that has replaced "we need to support various hardware" is an even more stupid argument: if we make it so that these aren't UB then people will rely on the common behavior and write code that is incorrect on platforms that behave differently. e.g. instead of software that is always wrong on one platform, you make software that is semi-randomly wrong on all platforms (because whether or not a compiler removes UB in one case is dependent on compiler version, flags, inlining, etc and if any of those change then suddenly the same code you had yesterday has a security bug when shipped).
- bluGill 3y agoUb is a bug. We can define what happens, but your code is still wrong if it gets there. Leaving it undefined mean the optimizer can make useful optimizations sith no harm as your code should be useful anyway.
- olliej 3y agoIn what sense? C and C++ aren’t memory safe, so the specification has to say something about what happens if you’re dereferencing an invalid pointer (random value, out of bounds, frees pointer, etc). That’s what UB exists for: there’s no behaviour we can actually define for some operations.
- ack_complete 3y agoNot always. [fs.race.behavior] makes it undefined behavior to use the C++ filesystem library in a way that introduces a race on the filesystem, including with _other processes_: https://eel.is/c++draft/fs.race.behavior https://eel.is/c++draft/fs.race.behavior I'm not sure how it is possible for a program to avoid this.
- andy99 3y agoIn the bit where he shows void error(const char* msg); int successor(int a) { if (a + 1 < a) error("Integer overflow!"); return a + 1; } and says the if is compiled away at -O3, does any one know if it remains at any lower optimization level? I know some of the more aggressive optimizations intentionally ignore some checks, I don't know if that applies here. I found the -O3 odd for trying to help make his point, unless it doesn't work at -O2.
- dzaima 3y agoIt's optimized out on both gcc and clang on -O1 and above. -O3 is presumably just what the author defaults to for enabling optimizations (I also write -O3 everywhere by default).
- nullhole 3y agoMy favourite description of undefined behaviour. The poster is corrected later on in the thread about whether the specific operation discussed would invoke undefined behaviour, but the description of what happens when undefined behaviour occurs is gold: https://groups.google.com/g/comp.lang.c/c/ZE2B2UorTtM/m/1ROv8gTwuEAJ https://groups.google.com/g/comp.lang.c/c/ZE2B2UorTtM/m/1ROv... Joona I Palaste, 2001-01-19, comp.lang.c This isn't about the post-increment operator, this is about the order of evaluation of the operands. Since you're modifying the value of i twice without a sequence point in between, either of the two results are exactly as much "expected". Also, equally "expected" behaviour includes incrementing every variable in the array, flipping all the bits in every variable in the array, converting all instances of the text string "/usr" in memory to "fsck", changing the colours of your screen to purple, calling the police on your modem line and telling them you're being attacked by a one-eyed Martian wielding a herring while singing "Hi ho, it's off to work we go", and even weirder stuff. So... what it all boils to... when writing your compiler, just flip a coin and use the one of the two behaviours you listed that corresponds with the coin's face.
- tom_ 3y agoAnd yet the standard explicitly states that undefined behaviour can behave in some documented manner characteristic of the environment. As a simple question of quality of implementation, we should surely be able to demand that nothing confusing happens.
- nullhole 3y agoI don't disagree, and I think the quote above follows that idea. Undefined behaviour means that anything _could_ happen, but compiler writers should ensure something sensible happens in those cases. At least, that's what I took from it.
- int_19h 3y agoAt this point we should just bite the bullet and make it not just defined, but defined in a way that results in safe code, even if that code is slower (e.g. for overflow, panic). We have computers that are many orders of magnitude faster than anything that was around back in the days C++, much less C, was originally designed. And most code that runs on them is not performance critical, so we could absolutely turn on null checks, overflow checks, bounds checks etc most everywhere and things would still be fine - but with less (and more visible, thus easier to find) bugs. This whole mentality that if you are writing in C++, you must let the compiler squeeze every last bit of perf out of your code, is both dangerous and unneeded.
- layer8 3y agoI recommend reading the resources under https://en.cppreference.com/w/c/language/behavior#External_links https://en.cppreference.com/w/c/language/behavior#External_l... (–> External links).
- deleted 3y ago[deleted]
- iwsk 3y agoI don't get it. How can UB on double-free, use-after-free, dangling pointers, etc lead to optimizations?
- lifthrasiir 3y agoMaking double-free an UB makes `free` more efficient because there are less checks to make. Combined with use-after-free as an UB, that deallocated memory can be immediately reused for the next allocation without any repercussion. And making dangling pointer an UB makes most pointer analysis much more doable.