5 ms·
I think DJB at some point expressed the desire for a boring compiler with an absolute minimum of UB, and I concur. I want some sort of flag that disables all t
by dale_glass 3y ago
I think DJB at some point expressed the desire for a boring compiler with an absolute minimum of UB, and I concur.
I want some sort of flag that disables all this nonsense.
* Uninitialized variable? Illegal, code won't compile.
* Arithmetic overflow? Two's complement
* Null pointer call? Either won't compile, or NULL pointer dereference at runtime worst case.
Yeah, I'm aware -fwrapw and the like exist. But it'd be nice to have a -fno-undefined-behavior for the whole set.
- coliveira 3y agoJust turn on all warnings in your C++ compiler, and make warnings the same as error. For example, uninitialized variables are easy to catch as a warning and then turned into an error. More sophisticated compilers can warn about the other issues too.
- UncleMeat 3y agoThis handles a few cases but nowhere near all of them. Null pointer dereference, use-after-free, data races, and much much more are all global properties with no hope of the compiler protecting you.
- coliveira 3y agoWe're only considering UB conditions here, not errors in general that may be impossible to detect. Every UB condition can be detected by the compiler because, after all, the compiler needs to check for UB to generate code. All it takes is for the compiler to generate warning/errors when this occurs. If this is not done by your compiler, ask them to add this feature instead of just complaining about UB in general.
- UncleMeat 3y agoAll of the things I described are UB.
- dureuill 3y ago> , after all, the compiler needs to check for UB to generate code. No the compiler does not check for UB to generate code. A lot of UB (runtime out of bounds, use after free) are very difficult to detect statically (at least without specialized annotations). The compiler applies transformations to the code that are valid (in the sense that they produce an equivalent program) only in the absence of UB. So if the input program contains UB, the transformed program produced by the compiler may or may not be valid
- TillE 3y agoStuff like this sounds great but if you dig into the details of actually trying to implement it, it's not at all simple, and it would inevitably slow down the already-terrible compilation speed of C++. We have static analysis tools which do a decent job. But like, there's a reason Clang has UBSan to detect undefined behavior at runtime. It's a hard problem.
- kelnos 3y agoNot really, at least for two of the three examples mentioned. Failing compilation on an uninitialized variable is easy. The compiler can already warn about this situation (and gcc and clang at least allow you to promote individual warnings to errors). Making this default would be simple, and not at all a performance concern. Allowing signed arithmetic to overflow (in a defined 2's complement manner) would be just exactly what the hardware does on modern machines, so there'd be no slowdown there. Sure, the compiler would no longer be allowed to omit code that checks for overflow, but that's fine: if the programmer truly doesn't care, they won't write that check in the first place. (Changing these two behaviors might have backward-compatibility concerns, though.) You are of course correct that NULL dereference checking is would incur a performance penalty at runtime. However, the compiler should be able to catch some subset of cases at compile time. At the very least, there could be a mode where it could at least warn you at compile-time that some dereferences could result in SIGSEGV. Unfortunately, I think it would be hard to get that warning to a point where there weren't a lot of false positives, so such a warning would be routinely ignored to the point of uselessness.
- agwa 3y ago> You are of course correct that NULL dereference checking is would incur a performance penalty at runtime. However, the compiler should be able to catch some subset of cases at compile time. At the very least, there could be a mode where it could at least warn you at compile-time that some dereferences could result in SIGSEGV. SIGSEGV on null pointer deference is not the problem. It's actually fine, in the sense that it predictably terminates the program. The problem is that modern optimizing compilers don't guarantee that SIGSEGV will happen; they may rewrite your program to do insane shit instead, as the example in the blog post shows. So we don't need NULL checks at runtime; we just need the compiler to stop doing insane optimizations.
- kelnos 3y ago> Arithmetic overflow? Two's complement I don't really want this either; I'd rather the program abort. The vast majority of situations where I'm using signed arithmetic, I never want a positive number to overflow to negative (and vice versa). Unsigned arithmetic is already designed to wrap back to zero; that's useful for things like sequence numbers where wrapping is ok after you "run out".
- toast0 3y agoThis is possible, but unless processor accelerated comes at a great cost. You'd need a branch after every math, unless the compiler could prove the math wouldn't overflow. The ARM mode described elsewhere in the thread where there's an overflow flag that persists across operations would help; then you could do a check less frequently. A mode were you get a processor exception would be great, if adding that doesn't add significant cost to operations that don't overflow. Assuming the response to such an exception is expected to be a core dump, the cost of generating such an exception can be high; of course if someone builds their bignumber library around the exception, that won't be great.
- pdw 3y agoTrapping on integer overflow was completely standard before C came along. Fortran and Pascal compilers did it. (Lisp compilers transparently switched to bignums :)
- kccqzy 3y ago> You'd need a branch after every math A predictable branch is basically free. In your case such a branch is almost never taken.
- celeritascelery 3y ago"basically free" is very different then actually "free". Adding a ton of extra branches comes at a cost. Even though it is cheap it still takes an instructions that could be used for something else. You fill your branch prediction target cache must faster, ejecting branches you actually care about. It also makes it harder for the compiler to move code around and harder for the prefetcher to break data dependencies. This all adds up to non-trival overhead. You can tell your C compiler to always check arithmetic, but most don't because of the cost.
- UncleMeat 3y agoThis already mostly exists. It means building with asan/msan/tsan/ubsan enabled. And... you'll slow your code down by several factors.
- sweetjuly 3y agoSanitizers are debugging tools; they don't guarantee you'll actually catch issues when you run into them.
- UncleMeat 3y agoRight. This is why I said "mostly" in my post. If you want guaranteed detection of all forms of UB you'll need even higher overheads. Like, I really don't know how you'd design a system that is guaranteed to detect all data races.
- adrian_b 3y agoWith gcc you can use e.g. "-fsanitize=undefined,address -fsanitize-undefined-trap-on-error". In my opinion, any C or C++ program must always be compiled with such options by default, both for debug and release builds. Only when an undesirable influence on performance is measured, the compilation options can be relaxed, but only for those functions where this matters.