4 ms·
The Little Bool of Doom (2025)
- deleted 8mo ago[deleted]
- djoldman 8mo ago> For this, I used the Godbolt compiler explorer. > Ah-ha! The generated instructions were ever so slightly different. This would be great news, if it wasn't for me forgetting about one little detail: I have zero knowledge of x86 assembly. Lol'd at this. I've been there: "ah hah! I found you. hrm, now what does this mean..." TFA makes me thankful my work doesn't involve C / C++. Learning it earlier in life was enough.
- kccqzy 8mo agoExcellent debugging journey. If I were the author, I would skip the part about using the compiler explorer and reading the assembly. When I write C code, I need to satisfy the rules of the C language. Unless I’m debugging the compiler or dealing with performance issues, my experience is that reading the generated assembler and understanding it is usually a slow way of debugging. The author eventually did compile with -fsanitize=undefined but I would honestly do that first. It prints a nice error message about the exact issue as the author has shown.
- direwolf20 8mo agoI have to disagree. If you merely want to fix the problem, you can stop as soon as you find something that's awry and whose alteration removes the problem. But don't you want to understand the problem? Don't you want to see how the compiler can reasonably generate code that says a bool variable is true and false at the same time?
- kccqzy 8mo agoIt’s about abstraction layers. Most of the time, understanding the generated assembler code isn’t useful when it comes to understanding the problem. It satisfies my curiosity sure, but the problem is at the C level, an undefined behavior. Understanding what the C compiler generates is interesting, but without a corresponding intuition about the optimizer passes, such understanding is shallow and unlikely to be generalized to other problems in the future. You probably won’t even remember this the next time you debug another undefined behavior. On the other hand, if you were to know the optimizer passes employed by the compiler and could deduce this code from that, then it is a useful exercise to enhance your intuition about them.
- exmadscientist 8mo agoI think it depends on your experience. I have a lot of experience from the Old Days™ and from developing for microcontrollers, so I find reading assembly very natural and straightforward. When coding for the really small MCUs I've often had the disassembly generated and shown on another window every time I incrementally build, and can check and make sure it's what I was expecting to see. I do agree that knowledge of compiler optimizations is really important to working this way, though you'll eventually pick them up anyway. I don't see much value in looking at -O0 or -Og disassembly. You want the strongest stuff the compiler can generate if you're going to do this, which is usually either -O3 or -Oz (both of which are strong in their own ways). -O0 disassembly is... just so much pain for so little gain. Besides, -O3 breaks more stuff anyway! For someone without this level of experience (and who isn't interested in learning)... yeah, I can see why you'd want to do this another way. But if you've got the experience already, it's plenty fast enough.
- lmm 8mo agoThe thing is, you're gaining a bunch of knowledge about compiler internals and optimisations, but those aren't necessarily specified or preserved, so it's questionable how valuable that experience actually is. The next release of the compiler might rewrite the optimiser, or introduce a new pass, and so your knowledge goes out of date. And even if you have perfect knowledge of the optimiser and can write code that's UB according to the standard but will be optimised correctly by this specific compiler... would that actually be a good idea? All of that is less true in the microcontroller world where compilers change more slowly and your product will likely be locked to a specific compiler version for its entire lifecycle anyway (and certainly you don't have to worry about end users compiling with a different compiler). In that case maybe getting deeply involved in your compiler's internals makes more sense.
- exmadscientist 8mo agoLearning about how compilers optimize code isn't really knowledge that goes out of date. Yes, things get reshuffled or new ideas appear, but everything builds on what's already there. You'd never want (except in extreme desperation) to use this knowledge to to justify undefined behavior in your code. You use it to make sure you don't have any UB around! Strategies like "I wrote a null pointer check, why isn't it showing up anywhere in the assembly?" can be really helpful to resolve problems.
- niobe 8mo agoI think that's the kind of intuitive decision that comes from years of troubleshooting experience. It's not obvious that would be the place to start. It's impressive to me at least he got there.
- pseudohadamard 8mo agoThe author knew there was a problem, knew where the problem was, and knew what the problem was, but even with that and an extensive perusal of the standard he still only thinks he knows what part of the standard was violated, presumably because nothing else fit ("didn't explicitly say anything about invalid _Bool values. After some more searching, I believe I've found my answer"). There's no way anyone without omniscience could have predicted this obscure issue being present somewhere in Doom's 60,000 lines of code.
- saagarjha 8mo agoWith the behavior specified UB sanitizer would have been my first tool to try.
- adrian_b 8mo agoPresumably also the author has now learned this, after discovering at the end that compiling with the sanitizer enabled immediately showed which is the bug, which would have saved a lot of time if he had always used it.
- turol 8mo agoUnfortunately Doom has a lot of undefined behavior (because it's a 30 year old codebase) so if you enable UBSAN you're inundated with warnings. Finding this specific needle in that haystack would take quite a lot of effort.
- Joker_vD 8mo ago> 1. Explicitly set the C standard to C17 or older, so the code is built using the custom boolean type. > Option 1) seemed like the easiest one, but it also felt a bit like kicking the can down the road – plus, it introduced the question of which standard to use. Arguably, that's the sanest one: you can't expect the old C code to follow the rules of the new versions of the language. In a better world, each source file would start with something like #pragma lang_ver stdc89 and it would automatically kick off the compatibility mode in the newer compilers, but oh well. Even modern languages such as Go miss this obvious solution. On the topic of the article, yeah, sticking anything other than 0 or 1 into C99 bool type is UB. Use ints.
- Tuna-Fish 8mo agoRust does the right thing, with the per-crate edition = statement.
- layer8 8mo agoWith C you put that information as a build option in your Makefile or similar. That’s a consequence of C only standardizing the actual language (and a runtime library), not the build environment.
- wk_end 8mo agoYeah, it’s only kicking the can down the road if you’re the actual maintainer of the software. If you’re just a packager, it’s your job to get the package to build and work correctly; for your own sanity, you should be making minimal changes to the underlying code to facilitate that. Get it building with the old language version and file a bug report.
- nomel 8mo ago> you can't expect the old C code to follow the rules of the new versions of the language Well, to be pedantic, the entire point of the C standard, and the standard body, is that you should expect it to work, as long as you're working within the standard!
- theamk 8mo agoThis brings memories - back when I was a student programming in Turbo Pascal 6, I got the same invalid bool (due to array range overflow) which was both true and false at the same time.
- lowbloodsugar 8mo agoThat is a fucking travesty. If there’s one thing we should be able to rely on C for it’s that it works with assembly, and it’s always been the case that 0 is false and any other value is true. That’s a compiler bug as far as I’m concerned. I don’t use C++ because it’s gone in a ludicrous unhelpful direction since 2000 or so, but it’s sad to learn that C of all languages decided to favor pedantry over working code.
- munchler 8mo agoThe code in question is: if (sprtemp[frame].rotate == false) Note that this is explicitly comparing two values, which is very different from checking whether a single value is true. Surely you wouldn't expect -1 == 0 to evaluate to true.
- robinsonb5 8mo ago> Surely you wouldn't expect -1 == 0 to evaluate to true. I wouldn't, no - but that's exactly what's happening in the test case. Likewise, I wouldn't expect -1 == 1 to evaluate to true, but here we are. The strict semantics of the new bool type may very well be "correct", and the reversed-test logic used by the compiler is certainly understandable and defensible - but given the long-established practice with integer types - i.e "if(some_var) {...}" and "if(!some_var) {...}" - that non-zero is "true" and zero is "false", it's a shame that the new type is inconsistent with that.
- lowbloodsugar 8mo ago>Surely you wouldn't expect -1 == 0 to evaluate to true. No, but that's not what happened. What happened was that 255 == 0 was true. That's a bug.
- direwolf20 8mo agoYou're not wrong. The processor has a perfectly good zero/notzero check. There was absolutely no reason for the compiler to check if x^1==0.
- 8mo ago
- munchler 8mo ago> memset (sprtemp,-1, sizeof(sprtemp)); Yikes. I think this article undersells the point somewhat. This line of code undermines the type system by spraying -1's into an array of structs, so the only surprise to me is that it took this long to break.
- direwolf20 8mo agoC has no particularly strong type system, and it works on typical platforms with all types up to the introduction of _Bool. And maybe float/double, but I think it gives a NaN.
- Sharlin 8mo ago`spriteframe_t` is defined as typedef struct { // If false use 0 for any position. // Note: as eight entries are available, // we might as well insert the same name eight times. boolean rotate; // Lump to use for view angles 0-7. short lump[8]; // Flip bit (1 = flip) to use for view angles 0-7. byte flip[8]; } spriteframe_t; which is okay to splat with all-ones as long as `boolean` is either a typedef for a fundamental type(*) or an enum – because C enums are just ints in a trenchcoat and have no forbidden bit patterns! The C99 `bool`/`_Bool` is, AFAICS, the first type in C that has fewer values than possible bit patterns. So yeah, on C99 and C++ this always had UB and could've broken at any time – though I presume compiler devs were not particularly eager to make it ill-behaved just because. But in pre-C99 it's entirely fine, and `rotate == true || rotate == false` could easily be false without UB. --- (*) other than `char` for which setting the MSB is… not UB but also not the best idea in general.
- zozbot234 8mo agoC99 does not use bool unless you explicitly include stdbool.h, which won't be in any pre-C99 code. By default, you just get _Bool.
- direwolf20 8mo agoMaybe changing the type of the variable to int was the better solution?
- deleted 8mo ago[deleted]
- allreduce 8mo ago> After consulting some sources, I started translating the assembly instructions into plain Polish Ah yes, obfuscation
- i-con 8mo agoThere's another way to explain the UB: IIRC, any value when stored to a _Bool is supposed to be converted to 0 or 1. The memset() bypasses this rule, boom.
- smddang 8mo ago[dead]
- userbinator 8mo agoI've never felt the need to use a separate boolean type in C; zero and nonzero are enough and very natural to use. Seeing "== false" and variations thereof always triggers the suspicion that its author doesn't fully understand boolean expressions. I have once seen the even worse "(x == false) == true".
- Joker_vD 8mo agoYeah, but it's a shame that almost every language gets it wrong (and even traditional presentation of mathematical logic) and uses zero as representation of FALSE. It should represent TRUE!
- rhplus 8mo agoPedantic: the axioms of Boolean algebra don’t assign any natural numbers to the elements “top” and “bottom” of the set it operates on. The notation is usually “1” and “0” but it doesn’t have to be. It’s a convenience that many computer languages have named those elements “true” and “false”, and yes, it’s totally valid that in some representations, top = 0 = true and bottom = 1 = false.
- pwdisswordfishy 8mo agoTechnically true, in practice using 0 for the bottom element and 1 for the top is a pretty strong convention; justified, for example, by the connection to probability measures and isomorphism with the Boolean ring ℤ/2ℤ.
- jabl 8mo agoIf you want to make an argument for something else being the representations of boolean variables than 0 for false and 1 for true, one could make the case for true being all bits set. That would make it slightly easier to do things like memset()'ing a vector of boolean, or a struct containing a boolean like in this case. Backwards compatibility with pre-_Bool boolean expressions in C99 probably made that a non starter in any case.
- _0ffh 8mo agoAs someone who has always despised the usage of "foo == true" and "foo == false" in predicates instead of just "foo" or "not foo" (or "!foo"), this pleases me. That practice misses the point of what it means for a value to be a boolean. (Even in C, if it's actually an int.)
- guerrilla 8mo agoWhat is this guy doing... you just build it against the standard it was written for. Yeesh. What a mess.
- Thorrez 8mo agoSmall errror: the demo program says some_var[1] = 500; however, seeing the output, what was actually run must have been some_var[1] = 1;
- deleted 8mo ago[deleted]