[HN Gopher] ISO C became unusable for operating systems development
___________________________________________________________________
ISO C became unusable for operating systems development
Author : pcw888
Score : 173 points
Date : 2022-01-21 11:20 UTC (11 hours ago)
(HTM) web link (arxiv.org)
(TXT) w3m dump (arxiv.org)
| throwawayvibes wrote:
| foxfluff wrote:
| I haven't got the time to read the paper yet but I believe I'd
| emerge with the more or less the same opinion that I've had
| before: nobody's forcing you to pass -O2 or -O3. It's stupid to
| ask the compiler to optimize and then whine that it optimizes. I
| usually am OK with the compiler optimizing, hence I ask it to do
| so. I'm glad that others who disagree can selectively enable or
| disable only the optimizations they're concerned about. Most of
| the optimizations that people whine about seem quite sane to me.
| Of course, sometimes you find real bugs in the optimizer
| (yesterday someone on libera/#c posted a snippet where gcc with
| -O2 (-fdelete-null-pointer-checks) removes completely legit
| checks)
| marcosdumay wrote:
| Optimizations aren't supposed to change the meaning of your
| code. And specifically for C, unsafe optimizations are supposed
| to only apply with -O3. Level 2 is supposed to be safe.
| dooglius wrote:
| I recall reading recently that gcc moved to making overflow
| undefined at -O0
| foxfluff wrote:
| > And specifically for C, unsafe optimizations are supposed
| to only apply with -O3. Level 2 is supposed to be safe.
|
| Citation needed? AIUI optimization levels are entirely up to
| the compiler. GCC's man page says nothing about the safety of
| these levels.
|
| > Optimizations aren't supposed to change the meaning of your
| code.
|
| And they generally won't. The trouble is when the meaning of
| your code wasn't well defined to begin with (or you thought
| it meant something it didn't).
| marcosdumay wrote:
| > GCC's man page says nothing about the safety of these
| levels.
|
| Well, it used to.
| klodolph wrote:
| This is definitely untrue. The difference between -O1, -O2,
| -O3, and -Os is as follows:
|
| - O1 includes optimizations that only require a small amount
| of additional compilation time. (Fast compilation, code is
| much faster than -O0.)
|
| - O2 includes O1, and additional optimizations that use a
| medium amount of compilation time. (Medium compilation,
| reasonably fast code.)
|
| - O3 includes O2, and additional optimizations that take more
| compilation time. (Slower compilation, faster, larger code.
| Unrolled loops and the like.) It often makes sense to compile
| hot inner loops with O3 because the compiler can do some
| fairly aggressive vectorization, if you know what you are
| doing... and the code remains correct in either case, if it
| was correct in the first place. It's basically "free"
| vectorization, when it works.
|
| - Os includes O2, except flags that increase code size.
| (Medium compilation, code is slightly slower and smaller than
| O2.)
|
| The only optimization level that actually changes the meaning
| of correct code is -Ofast, which is _rarely_ used. It 's
| described in the manual with the phrase "Disregard strict
| standards compliance".
|
| ALL levels of optimization will change the meaning of your
| code if your code relies on undefined behavior. For example,
|
| https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
|
| > -faggressive-loop-optimizations
|
| > This option tells the loop optimizer to use language
| constraints to derive bounds for the number of iterations of
| a loop. This assumes that loop code does not invoke undefined
| behavior by for example causing signed integer overflows or
| out-of-bound array accesses. The bounds for the number of
| iterations of a loop are used to guide loop unrolling and
| peeling and loop exit test optimizations. This option is
| enabled by default.
|
| It's always enabled.
| josephcsible wrote:
| Imagine you and the compiler are parties to a contract. Your
| obligation is to not let UB happen. The compiler's obligation
| is to not change the meaning of your code. Insisting that the
| compiler should uphold its end of the deal when you don't
| uphold yours is like saying if you sign a contract to buy
| widgets, you should have to pay even if the vendor fails to
| deliver them.
| marcosdumay wrote:
| > Your obligation is to not let UB happen.
|
| Sorry, no can do. If that's the contract than C is a
| completely useless language.
| Filligree wrote:
| > Sorry, no can do. If that's the contract than C is a
| completely useless language.
|
| That _is_ the contract. Your inference may be correct,
| but it does no good to insist that C should behave
| differently than it does. There are better options.
| mpweiher wrote:
| Nope, that's not the contract.
| 3836293648 wrote:
| That's absolutely the contract compiler implementors are
| following. If you don't want to be a part of that
| contract you need to implement your own compiler from the
| ground up with optimisations based on your interpretation
| of UB.
| Filligree wrote:
| I have always been told that, if a program contains UB,
| then the computer is justified in doing anything at all.
| This would naturally imply I am required to avoid any UB.
|
| Do you disagree with this?
| marcosdumay wrote:
| There's no contract. Nobody ever shared a piece of paper
| saying "your software must not use UB".
|
| The closest thing to a "contract" a language has is
| training material and reference books. And guess what,
| nearly all of C material mostly ignore UB, because
| enumerating it all is completely impractical even for
| reference books.
|
| Most of the C software in use on the wild was created
| before the hyper-optimization trend of C compilers, when
| your program did mostly what you said, and if what you
| said is undefined, it would still behave in some way that
| is close to a literal reading of the code. For most of
| the C software that exists, the modern treatment of UB is
| a huge post-facto unilateral contract change.
| plorkyeran wrote:
| The C specification is the contract between you and the
| compiler.
| marcosdumay wrote:
| It's not a contract if you can't expect one of the
| parties to have read it.
|
| Compiler developers do not need a contract with the
| users. No compiler team works with that impression. No
| compiler user expects one either.
|
| But if you want to use it to justify some impractical
| behavior, well, both your justification is a complete
| fabrication, and justifying it is completely meaningless.
| It doesn't become practical because you have some
| sociological theory.
| Filligree wrote:
| You seem to be using the legal definition of 'contract'.
|
| We're using the computer science definition, which is
| simply "assuming it's correctly implemented, if you feed
| this program Foo, it'll do Bar".
| int_19h wrote:
| Which compiler team doesn't take the C standard
| seriously?
|
| And the C standard is _by definition_ a contract between
| the implementation and its users.
| dooglius wrote:
| I didn't sign a contract though, the compilers just do dumb
| things and I am at their mercy. If I were negotiating a
| contract then I wouldn't allow all the cases of UB.
| [deleted]
| gpderetta wrote:
| Writing a C compiler that does a trivial translation to
| ASM is not very hard, in fact there are already quite a
| few. I don't understand why people don't use that if
| that's what they want.
| josephcsible wrote:
| Ironically, it's because the code they generate is too
| slow.
| phicoh wrote:
| It seems to me a flaw in a language and in a compiler if the
| average programmer has to avoid higher levels of optimizating
| because it cannot be predicted what they do.
| foxfluff wrote:
| I don't see it that way. Isn't that precisely the point of
| higher levels? Don't go there if you don't know what you're
| doing. Since we can't magically install knowledge into
| average programmer's heads, the only other choice would be to
| have those higher levels not exist because the average
| programmer will never understand them, and also the compiler
| can't mind-read the average programmer to understand that
| what they want isn't exactly what they said they want.
| fennecfoxen wrote:
| I think the general problem is that if so much is going to
| hinge on this, it should be much easier to tell whether or
| not you know what you're doing. Undefined behavior really
| wouldn't be a problem if you could spot it reliably
| (possibly using tools, like the compiler toolchain itself).
|
| This loose mapping between four optimization levels and a
| variety of obscure language situations is invisible and
| situated only in a handful of top engineers' memories.
| That's not a great way to run a language if you actually
| care about the correctness of programs written in that
| language.
| immibis wrote:
| I always thought the main point of the levels was simply
| compilation speed. That and debuggability.
| wongarsu wrote:
| > Isn't that precisely the point of higher levels?
|
| I used to think that higher levels are for giving the
| compiler more time to think so it can come up with a better
| result (and for somewhat specifying the desired space-
| performance tradeoff). In my mind the "don't do it if you
| don't know what you are doing"-things were seperate flags,
| like -ffast-math.
|
| Obviously I eventually learned that bad things can happen,
| but "just build prod with -O2" still seems to be what
| almost everyone does.
| mpweiher wrote:
| Nope.
|
| That optimisations must not change the visible behavior
| (except for running time) of a program was _the_ cardinal
| rule of optimisation.
|
| Then it got flushed down the toilet. (With the trick of
| retroactively redefining the behavior to be undefined in
| the first place and thus all bets being off).
| Wowfunhappy wrote:
| What if the only languages/compilers without that "flaw" had
| similar performance to lower optimization levels?
|
| (I don't know if that's the case, but it's what I thought the
| GP was implying.)
| phicoh wrote:
| There is a saying that you can make any program run fast if
| you don't care about correctness.
|
| We have reached the point where C programmers cannot
| understand the language/compiler anymore.
|
| Given that this has been going on for a long time, my hope
| is that Rust will be the next systems programming language.
| foxfluff wrote:
| Do you think people understand Rust?
| pornel wrote:
| The safe subset of Rust does not permit any UB. This
| greatly simplifies understanding of UB in the majority of
| the codebase -- because there isn't any. Only when
| there's an `unsafe` keyword in Rust, you put your "C" hat
| on.
|
| (it was and continues to be a challenge to uphold this
| guarantee, because UB in LLVM is so ubiquitous, e.g. Rust
| had to patch up float to int conversions and eliminate
| no-op loops)
| duped wrote:
| It's easier to understand production Rust than production
| C
| phicoh wrote:
| Rust can be hard to write, but by and large is not hard
| to read.
|
| Rust does have more abstraction mechanisms than C. So it
| is possible to go overboard in that area. But that is
| more a coding style issue. C programs that nest multiple
| levels of functions pointers can also be hard to
| understand.
| cma wrote:
| Rust code in unsafe blocks has lots of similar issues
| with fuzziness around aliasing rules:
|
| https://github.com/rust-lang/unsafe-code-
| guidelines/blob/mas...
| couchand wrote:
| It doesn't seem like anyone is taking the position here that
| C is flawless. Rather, the question is, given both its flaws
| and its ubitquity, is it acceptable to trade off developer
| head-wall interactions for the possibility of high
| performance.
|
| The only winning move is not to play. (with C)
| maxlybbert wrote:
| I'm amused by the people who ask for optimization and then
| complain about it. Or the "it did what I said, not what I
| meant" crowd.
|
| But, officially, undefined behavior is always undefined, not
| just at higher optimization levels.
| vgatherps wrote:
| This is a pretty dismissive response to something that's a
| real problem.
|
| Sure, the "Why are you deleting my checks for if *this is
| null" is a little silly - but there are definitely sharp
| edges where UB conflicts with actually useful things you
| might want to do. Did you know seqlocks are undefined (benign
| race conditions)? Ever ran into padding concerns playing
| poorly with trying to do atomic CAS?
|
| It's not unreasonable for the standard to say 'padding is
| undefined', 'data-races are undefined' - but having no way to
| say "hey, trust me, please un-poison this thing you don't
| like" is pretty unfortunate.
| klodolph wrote:
| I know this is not relevant to your point... but padding in
| C is not "undefined". The amount of padding is
| implementation-defined, but it can only occur in specific
| places. The value of padding bits is unspecified.
| "Unspecified" in the standard means that it can be various
| different things, but it has to actually _be_ one of those
| things. As opposed to undefined behavior, which is more of
| a "poison value" that trims paths from your CFG.
| jcranmer wrote:
| > Did you know seqlocks are undefined (benign race
| conditions)?
|
| It wouldn't be undefined behavior if you used atomic
| variables--data races involving atomics _aren 't_ undefined
| behavior.
| int_19h wrote:
| UB is only UB when it comes to ISO C++ guarantees. The
| implementations can always make their own guarantees above
| and beyond that; they don't have to restrict themselves to
| those parts that are explicitly "unspecified" or
| "implementation-defined" in the Standard.
| zaphar wrote:
| Alternatively if my compiler has a "run faster" flag I would
| not expect it to change the "semantics" of my code in the
| process.
|
| Additionally in C UB is often not intentional nor trivial to
| detect in your codebase since it may be the interaction of
| two pieces of code that are not anywhere obviously close to
| each other. There comes a point where faster but broken code
| isn't better it's _just_ broken.
| maxlybbert wrote:
| The standard committee's position is that if undefined
| behavior isn't easy for the programmer to detect, why would
| it be easy for the compiler to detect?
|
| I'm a little more familiar with the C++ committee than the
| C committee. The C++ committee prefers to declare something
| an error than to declare it undefined behavior. They only
| declare something undefined when they believe it would be
| unreasonably difficult for the compiler to detect the
| problem (e.g., using multiple, incompatible, definitions
| for an inline function; which can happen if you have a
| macro expanding differently in different parts of the code,
| or you have multiple definitions for the function, but only
| one definition is ever visible to the compiler at any
| moment in time).
|
| I'm pretty sure the "signed overflow is undefined" rule is
| something of a special case: it should be easy to detect
| when source code doesn't have a hard coded upper bound, but
| giving an error or warning in all cases will create too
| many false positives, and declaring that it wraps on
| overflow has been deemed unacceptable by the committee.
| [deleted]
| GoblinSlayer wrote:
| UB problems happen only when compilers detect it,
| understand it is an opportunity for optimization, because
| they are allowed to do anything, then do that
| optimization. When compiler can't detect UB, it actually
| works as expected: when integers overflow they do exactly
| that and nothing else, when null pointers are
| dereferenced they do exactly that and nothing else, when
| uninitialized memory is read it does exactly that and
| nothing else.
| klodolph wrote:
| Reading uninitialized memory is not UB... but anyway.
| Here's an example of how compilers treat null
| dereferencing as UB: void func(struct x
| *aptr, struct x *bptr) { x->a = 5;
| y->a = 10; x->a = 10; }
|
| This is a contrived example but you can imagine writing
| code that makes a sequence of assignments like this. An
| optimizing compiler can remove the "dead store" which
| assigns 5 to x->a. However, this optimization is valid
| because writing to a null pointer is undefined... if
| writing to a null pointer trapped (SIGFAULT), which is
| what it _actually does_ at runtime, then the optimization
| would be incorrect... because you could observe the
| intermediate value of x- >a = 5.
|
| The compiler is not really "detecting that your program
| has undefined behavior". Instead, it is assuming that
| undefined behavior doesn't happen, and optimizes your
| program using that assumption. It's unclear what
| "detecting undefined behavior" would mean here... what
| kind of diagnostic the compiler should emit, or how to
| suppress the diagnostic. It's clear that this simple code
| is pervasive, and the preferred approach for dealing with
| it is somewhat outside the scope of a normal C
| compiler... something like static analysis, runtime
| instrumentation, or some kind of formal methods.
| foxfluff wrote:
| > However, this optimization is valid because writing to
| a null pointer is undefined... if writing to a null
| pointer trapped (SIGFAULT), which is what it actually
| does at runtime, then the optimization would be
| incorrect... because you could observe the intermediate
| value of x->a = 5.
|
| I'm not sure this is a good example, because there's no
| way for you to read them from the same thread (and
| reading from another thread would be a race). Reading
| them from a signal handler would yield unspecified values
| (unless you're using lock-free atomic types, in which
| case we're probably not worried about optimizing these
| assignments).
|
| > When the processing of the abstract machine is
| interrupted by receipt of a signal, the values of objects
| that are neither lock-free atomic objects nor of type
| volatile sig_atomic_t are unspecified, as is the state of
| the floating-point environment.
| klodolph wrote:
| That's a good point, however I think it's not hard to
| come up with a different example that shows how compilers
| assume that pointers aren't NULL. Like,
| void function(struct x *ptr) { ptr->x = 1;
| if (ptr == NULL) {
| some_big_chunk_of_code(); } }
|
| The idea is that this could be the result of inlining,
| where the inlined code does a null check, but in the
| context it's being inlined into, we already know ptr is
| not null.
| mjevans wrote:
| The compiler should help the programmer optimize instead.
|
| "Warning: dead store detected (file):line clobbered by
| (file):line. Use keyword volatile to always store or
| remove (file):line."
|
| This way the optimization is promoted from the resulting
| binary to the source code, and bugs / typos / etc can be
| corrected.
| klodolph wrote:
| That's a completely unworkable solution.
|
| The above code isn't necessarily code _as the programmer
| wrote it,_ but the code after it appears after
| macroexpansion and various optimization passes. If you
| were writing C++, the code may be templated.
| gpderetta wrote:
| You would get literally millions of such warnings on your
| average C codebase.
| zaphar wrote:
| So basically you are saying that in the average C
| codebase there are millions of landmines waiting to
| silently fail on you. This is not a ringing endorsement
| of the language it's more like a ringing indictment
| instead.
| gpderetta wrote:
| I certainly do not think that each dead store which has
| been eliminated is a landmine, this is such an extreme
| position that it isn't even worth considering. In the
| very rare cases were I needed a store to actually happen
| I will use inline asm or some form of volatile.
| foxfluff wrote:
| Optimizations often deal with inlined code. The code in
| the inlined function might be fine in the general sense,
| but once taken in a particular context, there's dead or
| redundant code because the compiler can deduce values
| from context. It might even all fold into a constant.
|
| Telling the developers to optimize these would be
| equivalent to telling them to start inlining their code
| (or creating variants of every function that might be
| inlined, to optimize it for the specific context where it
| will be inlined).
| foxfluff wrote:
| That's not true at all. Compilers can detect some
| instances of UB, and they will happily warn when they do.
|
| Most instances of UB are ones that the compiler could not
| detect. Your compiler isn't going to detect that your
| integer overflows, it assumes that it won't. Shit blows
| up if that assumption was wrong.
|
| If you want to detect UB, you should run ubsan and the
| like. The compiler is not running it.
| jerf wrote:
| In some sense, the problem isn't that the compiler
| "can't" detect that an integer addition will overflow.
| The problem is that with some exceptions (a loop that is
| clearly indexing along an array, for example), the
| compiler will have to flag _every_ integer addition as
| UB. UB notifications aren 't that useful if you're
| getting a dozen for every five line function in your
| entire codebase.
|
| Programmers do not, in general, want to do the amount of
| work it takes to _safely_ add two numbers together. You
| need something "weird". Like an error path for every
| addition. Or a type system for ranges, where you actually
| have to specify ranges (because adding two bytes together
| that you casually left as 0-255 will create a new ranged
| value 0-510; multiplication is even more fun). Or some
| other exotic idea. So we just let them overflow and the
| chips fall where they may.
| foxfluff wrote:
| > In some sense, the problem isn't that the compiler
| "can't" detect that an integer addition will overflow.
|
| It absolutely can't if it doesn't know what the values
| are going to be.
|
| > the compiler will have to flag every integer addition
| as UB
|
| No, I disagree. If it has no knowledge of my runtime
| values, it can't flag addition as UB _because it isn 't_.
| It's UB only if I'm using values that would overflow, and
| the compiler in general can't know that. If I've done my
| program right, it will never overflow. There is no UB,
| any flag would be just absolutely wrong. The compiler
| couldn't detect UB. There's nothing to flag.
|
| If it knows the values, then of course it can detect it
| and flag it: x.c:3:18: warning: integer
| overflow in expression of type 'int' results in '-2'
| [-Woverflow] 3 | int
| a=2147483647+2147483647;
|
| > Programmers do not, in general, want to do the amount
| of work it takes to safely add two numbers together.
|
| I agree that overflow-checking is needlessly sucky in C
| but that's not "the problem." I usually make sure my
| arithmetic is safe but the compiler won't know it.
| MauranKilom wrote:
| > UB problems happen only when compilers detect it,
| understand it is an opportunity for optimization, because
| they are allowed to do anything, then do that
| optimization.
|
| This is a deep misconception, and not at all how most
| undefined behavior is related to optimization.
|
| Trivial example: Compilers assume that your variables
| aren't written to randomly from other threads. Without
| this assumption, virtually no optimization would be
| possible. _Therefore_ , data races are UB - they violate
| a hard assumption of the compiler. But at no point did
| the compiler say "oh, you wrote to this variable without
| synchronization! Now I'll show you, hehehe!".
|
| This is the same for e.g. removed overflow checks.
| Compilers can optimize many loops only _if_ they assume
| signed integer overflow never happens. So, because
| compilers should be able to assume this, it is UB if it
| happens in your code. The same logic that deduces "this
| loop cannot wrap around" deduces "this if condition [an
| overflow check] cannot ever be true".
|
| But it's easier for a programmer who got their UB-reliant
| checks optimized out to attribute malice to the compiler
| than to understand optimization fundamentals, and thus we
| get people complaining to high heaven and back.
| dooglius wrote:
| You are strawmanning the parent, he did not claim that
| compilers will go "oh, you wrote to this variable without
| synchronization! Now I'll show you, hehehe!". Your
| description of the compiler's behavior matches his
| description; the compilers detect a case where the
| program might exhibit behavior defined ISO C spec (signed
| integer overflow), see it as a potential for optimization
| because they are allowed to do anything (certain passes
| can be made if overflow is assumed not to happen), then
| do that optimization.
| MauranKilom wrote:
| You can cast literally any optimization into this shape.
| For starters, virtually any optimization pass requires
| sane assumptions about (absence of) data races and/or out
| of bounds writes. The point you are arguing (or claiming
| OP argued) boils down to "compilers behave as I want when
| I turn all optimization passes off".
|
| No, the OP wrote specifically that "the compiler detects
| [UB]" and then does optimizations exploiting that. Not
| "detects _potential_ UB ". The former is a common
| misconception, the latter is basically a truism.
| dooglius wrote:
| > You can cast literally any optimization into this shape
|
| Tell me how these use UB: integer constant folding,
| unused load removal, optimal use of GP registers to
| minimize stack spilling
|
| >sane assumptions
|
| "sane" here is doing a lot of work. Assuming overflow
| won't happen is not a sane assumption, assuming some
| other thread won't randomly write into your stack memory
| is.
| jcranmer wrote:
| > unused load removal
|
| Oh come on, this is like the most UB-centric thing in the
| entire compiler. The compiler uses UB to know that it has
| enumerated all uses of a memory location, as any pointer
| that didn't get its value from the data dependency tree
| that compiler used had to use UB to compute the address.
| (Be it from buffer overflow, use after end-of-lifetime,
| random integer converted to pointer, etc.)
| dooglius wrote:
| I might be using the wrong terminology, what I'm talking
| about is removing e.g. `int x = *ptr` where x is then not
| used in scope (more realistically,
| `(*struct_inst).member` shouldn't bother to load the
| other members of the struct). What you're doing sounds
| like removing a global it detects is never used, which I
| agree relies too much on UB and should not be done.
| puffoflogic wrote:
| Aside from the possible trap ("segfault") mention in the
| sibling comment, the first example relies on absence of
| UB because with UB you could enumerate the contents of
| the stack to find where `x` is living, and discover it
| isn't there, contra the spec. Even allocating variables
| directly into registers and never onto the stack relies
| on UB.
| dooglius wrote:
| Why is it UB to use registers to store variables? This
| seems like an implementation detail. The ISO C spec
| doesn't even have the word "stack" as far as ctrl-f can
| show me.
| gpderetta wrote:
| If any object can potentially be reached by forging
| pointers or enumerating all memory locations, you can't
| even keep local variables in registers around potential
| stores to unknown pointers (or ever, in a multithreaded
| program).
| puffoflogic wrote:
| > Why is it UB to use registers to store variables?
|
| It isn't; that sentence is even a type error. UB is not a
| property of compiler output. It's a property of compiler
| input.
|
| The use of registers to store variables between uses
| (when those variables never have their address taken)
| _relies_ on the lack of any defined behavior which would
| allow the program to determine this is being done. The
| fact you can 't find this in the spec is precisely
| because it is not defined.
| gpderetta wrote:
| Removing a load is visible behaviour if the load would
| have trapped for example.
|
| Everybody agrees that compilers should only do sane
| optimizations. Nobody agrees on which subset is sane.
| dooglius wrote:
| That's fair, I'll concede that I make some implicit
| assumptions. But the magnitude of people who hit problems
| and accidentally hit UB should give a strong indication
| that a lot of things done now are not close to sane.
| gpderetta wrote:
| Lobbying the standard body to reduce the amount of UB in
| the C standard is a perfectly reasonable position.
| Requiring compilers to DWIM is not.
| MauranKilom wrote:
| > integer constant folding int a = 10;
| int var1; int b = 20; foo(&var1);
| int c = a + b;
|
| You'd like to constant fold c? Better assume no UB:
| void foo(int* x) { *(x-1) = 0; }
|
| > unused load removal
|
| Same idea as above.
|
| > Assuming overflow won't happen is not a sane assumption
|
| If the alternative is loops being much slower _when you
| request optimizations_ , then maybe it is. Consult your
| compiler manuals if this default irks you.
| dooglius wrote:
| Okay, yes, you are technically right. What I mean is,
| making an assumption _other than_ that the code won't
| alter stack/heap/instruction memory that the
| implementation is using (e.g. aliasing, overflow, use of
| NULL)
| gpderetta wrote:
| Isn't TBAA exactly the assumption that the program won't
| alter the stack/head/instruction memory in an illegal
| way?
| maxlybbert wrote:
| The parent said
|
| > UB problems happen only when compilers detect it,
| understand it is an opportunity for optimization, because
| they are allowed to do anything, then do that
| optimization.
|
| Perhaps I'm misunderstanding the comment, but I took
| "detect it, understand it is an opportunity for
| optimization, because they are allowed to do anything,
| ..." to mean "detect that undefined behavior occurs and
| then ..."
|
| Instead, it should be "detect that an optimization can be
| applied and that the optimized code will do the right
| thing in all cases that undefined behavior does not
| occur, and apply the optimization."
|
| I've met programmers who seem to believe that compilers
| intentionally apply weird transformations when they
| detect undefined behavior. But really, the compilers are
| applying sensible transformations for well-defined code
| and ignoring whether those transformations are sensible
| for code that isn't well-defined.
| jcranmer wrote:
| Here is an actual example of taking advantage of UB
| behavior in a compiler.
|
| The compiler sees a variable x that's allocated on the
| stack. It looks at all the uses of the address of x, and
| sees that they are all loads and stores. It therefore
| decides to promote x into a register variable.
|
| Where's the UB, you ask. Well, since the address of x was
| never leaked, it is UB to compute another pointer to that
| address (say, via an integer-to-pointer conversion). The
| compiler _never_ checked for that possibility to affirm
| that it was UB; it knew that any code that did that would
| be UB and simply ignored the possibility.
|
| This makes arithmetic overflow a very poor vehicle for UB
| because it's _unusual_ in that you can 't really take
| advantage of the UB without pointing to the specific
| operation that caused the UB to occur. This is why I
| believe that arithmetic overflow UB is gratuitous, and
| people objecting to UB _because_ of what happens
| specifically with arithmetic overflow go on to make
| suggestions that are completely untenable because they
| don 't have familiarity with how most UB works.
| steveklabnik wrote:
| UB is closer to "we assume this can never happen and make
| decisions accordingly" than "Ha ha, I caught you making a
| mistake, let's actively try to mess up your life now."
| chc wrote:
| The problem is that this doesn't _mean_ anything, at
| least in terms of the C programming language. You say
| "When integers overflow, they do that and nothing else,"
| but there is no definition of signed integer overflow, so
| there is no "that" for them to do. That is what it means
| for something to be undefined behavior. You have some
| definition in your mind, which might happen to match what
| some versions of some compilers do at some optimization
| levels -- but that isn't actually the definition, because
| there isn't one.
| sanxiyn wrote:
| "Run faster" involves things like register allocation. How
| can you justify register allocation doesn't change
| semantics of code? It absolutely does! If you smash the
| stack, variables on stack will be smashed, and variables on
| register will not. This is change of semantics.
|
| Compilers justify register allocation by assuming stack
| can't be smashed, contrary to hardware reality. Because
| smashing the stack is UB. That's what C UB is for.
| puffoflogic wrote:
| No no no; you see, in these kinds of debates "UB" is only
| the things I disagree with!
| derefr wrote:
| C exposes a (fairly low-level) abstract machine with its
| own semantics, encapsulating the operational semantics of
| the CPU. Optimization flags are expected to affect the
| uarch-operational semantics, but not the semantics of the
| C abstract machine.
|
| Think of it like trading one cycle-accurate emulator for
| a better, more tightly-coded cycle-accurate emulator. Or
| a plain loop-and-switch bytecode interpreter for a
| threaded-code bytecode interpreter. The _way_ your code
| is running _on the underlying machine_ changes, but the
| semantics _of your code_ relative to the abstract machine
| the code itself interacts with should not change.
|
| > Compilers justify register allocation by assuming stack
| can't be smashed, contrary to hardware reality.
|
| ...which _should_ be entirely fine, as the existence of a
| stack isn 't part of the exposed semantics of the C
| abstract machine. Values can be "on the stack" -- and you
| can get pointers to them -- but nothing in the C abstract
| machine says that you should be _able_ to smash the
| stack. There 's nothing in the C standard itself
| describing a physically-laid-out stack in memory. (There
| are ABI calling conventions defined in the standard, but
| these are annotations for target-uarch codegen, not facts
| about the C abstract machine.) There could just as well
| be a uarch with only the ability to allocate things "on
| the stack" by putting them in a heap -- in fact, this is
| basically how you'd have to do things if you wrote a C
| compiler to target the JVM as a uarch -- and a C compiler
| written for such a uarch would still be perfectly
| compliant with the C standard.
|
| If C were an interpreted language, _the fact that the
| stack can be smashed_ would be referred to as a "flaw in
| the implementation of the runtime" -- the runtime
| allowing the semantics of the underlying uarch to leak
| through to the abstract-machine abstraction -- rather
| than "a flaw in the interpreter" per se; and you'd then
| expect a _better_ runtime implementation to fix the
| problem, by e.g. making all pointers to stack values
| actually be hardware capabilities, or making each stack
| frame into its own memory segment, or something crazy
| like that.
|
| As a compiled language -- and especially one that has
| encapsulation-breaking holes in its abstract machine,
| like the C inline-assembly syntax -- you can't exactly
| expect a runtime shim to fix up the underlying uarch to
| conform to the C abstract machine. But you _could_ at
| least expect the C compiler to not _let_ you do anything
| that _invokes_ those cases where it 's exposing
| potentially-divergent uarch semantics rather than a
| normalized C-abstract-machine semantics. As if, where
| there _should_ be a shim "fixing up" a badly-behaved
| uarch, you'd instead hit a NotImplementedException in the
| compiler, resulting in a compilation abort.
| gpderetta wrote:
| Even comparing two pointers can expose potentially
| divergent uarch semantics. Should that be disallowed at
| compile time?
| zaphar wrote:
| Exactly, If I wanted to be operating of the level where I
| am worrying about registers and stack smashing and such I
| would just drop down assembly directly. You use C because
| it has an in-theory abstract machine that I can target
| and avoid needing to use assembly.
| gpderetta wrote:
| Compilers should have a -do-what-i-mean-not-what-i-say
| flags. The required mind reading is implementation defined.
| jvanderbot wrote:
| Nothing in this article convinces me the problem is contained
| to _optimized_ C. The strict type aliasing is a general
| problem, right?
| bcrl wrote:
| At least in compilers like gcc, optimization needs to be
| enabled to get sane warnings emitted by the compiler, so some
| people are indeed being forced to pass in -O2 to get sane build
| warnings for projects.
|
| I would really like it for the C standard to clean up Undefined
| Behaviour. Back in the 1980s when ANSI C was first specified, a
| lot of the optimizations that modern compiler writers try to
| justify via Undefined Behaviour simply weren't part of many
| compiler's repertoires, so most systems developers didn't need
| to worry about UD and there was no push for the standard to do
| so as a result.
|
| If people really want the optimizations afforded by things like
| assuming an int can't overflow to a negative number in a for
| loop, my personal position is that the code should be annotated
| such that the optimization is enabled. At the very least, the
| compiler should warn that it is making assumptions about
| potentially UB when applying such optimizations.
|
| There is this false belief that all legacy code should be able
| to compiled with a new compiler with no changes and expect
| improved performance. Anyone who works on real world large
| systems _knows_ that you can 't migrate to newer compilers or
| updated OSes with zero effort (especially if there's any C++
| involved). I understand that compiler writers want to improve
| their performance on SPEC, but the real world suffers from the
| distortions caused by viewing optimizations through the narrow
| scope of benchmarks like SPEC.
| foxfluff wrote:
| > If people really want the optimizations afforded by things
| like assuming an int can't overflow to a negative number in a
| for loop, my personal position is that the code should be
| annotated such that the optimization is enabled.
|
| That's not an entirely absurd idea, especially if you wanted
| to enable optimization piece-wise for legacy code. But in the
| end does it matter whether you specify the optimizations you
| want in the build system or in the code?
|
| > At the very least, the compiler should warn that it is
| making assumptions about potentially UB when applying such
| optimizations.
|
| The assumptions are usually "assume no UB." I don't know what
| kind of warnings you expect but this idea has been brought up
| before and it's not good because it would just make the
| compiler flood warnings on any code that does any arithmetic
| on variables (as opposed to compile time constants).
| foxfluff wrote:
| > At least in compilers like gcc, optimization needs to be
| enabled to get sane warnings emitted by the compiler, so some
| people are indeed being forced to pass in -O2 to get sane
| build warnings for projects.
|
| That's a fair point and it irritates me too. But you're not
| forced to run -O2 in production if you don't want to. I've
| had projects where we compile with multiple compilers and
| different settings -- including different libcs -- just to
| get all the warnings, while we ship non-optimized or
| minimally optimized debug builds to the customer..
| zajio1am wrote:
| > If people really want the optimizations afforded by things
| like assuming an int can't overflow to a negative number in
|
| Perhaps people wanted int overflow to be UD because that is
| semantically clean. Code where possible integer overflow is
| intended and not bug is IMHO significant minority of all
| integer usage, in vast majority it is a bug, similar to
| invalid memory access. So it would be reasonable for some
| compilers/platforms to handle integer overflows by cpu
| exception translated to signal killing the process (similar
| to SIGSEGV for invalid memory access), and only integer
| operations explicitly marked for overflow (i.e. unsigned in
| C) be allowed.
|
| It is just our experience that makes 'integer overflow should
| wrap' and 'invalid memory access should crash' default
| positions, while 'integer overflow should crash' and 'invalid
| memory access should be ignored/return 0' are alternative
| positions. Conceptually, both are just stricter or looser
| ways to handle code errors and it makes sense that C does not
| prescribe neither one.
| h2odragon wrote:
| Torvalds was a strong advocate of GCC 2.95 (iirc), early on in
| Linux history, because he knew the kind of code it would emit and
| didn't trust the newer compilers to produce code that was correct
| in those circumstances.
|
| The workarounds and effort required to tell a compiler today that
| no, you really did want to do the thing you said might well be
| insupportable. I figure they started going astray about the time
| self modifying code became frowned upon.
| mananaysiempre wrote:
| To be fair, the backend in the early GCC 3.x series was just
| kind of stupid sometimes. Even now I find strange if cheap and
| harmless heisengremlins in GCC-produced x86 code (like MOV R,
| S; MOV S, R; MOV R, S) from time to time, while the Clang
| output, even if not always _good_ , is at least reasonable.
| This is not to diss the GCC team--the effort required to port a
| whole compiler to a completely new IR with a completely
| different organizing principle while keeping it working most of
| that time boggles the mind, frankly. But the result does
| occasionally behave in weird ways.
| usbqk wrote:
| Can Linux compile under clang nowadays?
| eminence32 wrote:
| Yes, see:
| https://www.kernel.org/doc/html/latest/kbuild/llvm.html
| pjmlp wrote:
| Yes, when that Linux variant is called Android.
|
| Since almost 5 years by now.
| yjftsjthsd-h wrote:
| While sibling comment is correct that the simple answer is
| "yes", I'd like to add that Google uses Clang as the
| preferred compiler for Android. There's a _little_ bit of
| drift between Android 's version of Linux and mainline, but
| the effect is still that building Linux with Cland is being
| extensively used and tested.
| RustyRussell wrote:
| I still retch when told memcpy and memset cannot take NULL and 0
| length.
|
| Try asserting that they're not NULL in glibc and try to boot your
| machine! Oops... bad compiler people, bad!
| qualudeheart wrote:
| So we can switch to rust now?
| yjftsjthsd-h wrote:
| Sure; we look forward to your patches (keep in mind that they
| must preserve compatibility and portability to all currently
| supported architectures).
| flykespice wrote:
| nn3 wrote:
| Clickbait on arxiv? Et tu, Brute?
|
| No, most operating systems that people are actually use are
| written in ISO C, so the headline is by definition wrong.
| immibis wrote:
| They are written in non-ISO C
| jcranmer wrote:
| I think undefined behavior (as a general concept) gets an unfair
| share of the blame here. It's notable that almost all criticism
| of undefined behavior in C tends to focus on just two sources of
| UB: signed integer overflow and strict aliasing; other sources of
| UB just don't generate anywhere near the same vitriol [1].
| Furthermore, it's notable that people _don 't_ complain about UB
| in Rust... which arguably has a worse issue with UB in that a)
| there's not even a proper documentation of what _is_ UB in Rust,
| and b) the requirement that &mut x be the sole reference to x
| (and therefore is UB if it is not) is far more stringent than
| anything in C (bar maybe restrict), and I'm sure that most Rust
| programmers, especially newbies starting out with unsafe, don't
| realize that that's actually a requirement.
|
| There is a _necessity_ for some form of UB in a C-like language,
| and that has to deal with pointer provenance. You see, in C,
| everything lives in memory, but on real hardware, you want as
| much to live in a register as possible. So a compiler _needs_ to
| be able to have reasonable guarantees that, say, any value whose
| address is never taken can never be accessed with a pointer, and
| so can be promoted to a register. As a corollary, this requires
| that things like out-of-bound memory accesses, or worse,
| converting integers to pointers (implicating pointer provenance
| here) need to have UB in at least some cases, since these could
| in principle "accidentally" compute an address which is the same
| as a memory location whose address was never taken.
|
| That suggests that the problem isn't UB per se. If we look at the
| two canonical examples, arithmetic overflow and strict aliasing,
| we can see that one of the features of these things is that they
| have a pretty obvious well-defined semantics [2] that can be
| given for them, and furthermore, there's no way to _access_ these
| well-defined semantics even avoiding this feature altogether. And
| I think it 's the lack of this ability to work around UB that is
| the real issue with C, not UB itself.
|
| [1] For example, it is UB to pass in rand as the comparison
| function to qsort. I'm sure many people will not realize that
| before I wrote this, and even parsing the C specification to find
| out that this is UB is not trivial. For an interesting challenge,
| try giving a definition of what the behavior should be were it
| not UB--and no, you can't just say it's impl-defined, since that
| still requires you to document what the behavior is.
|
| [2] I will point out that, for arithmetic overflow, this
| semantics is _usually wrong_. There are very few times where you
| want <large positive number> + <large positive number> =
| <negative number>, and so you're mostly just swapping out an
| unpredictably wrong program for a predictably wrong program,
| which isn't really any better. However, the most common time you
| do want the wrapping semantics is when you want to _check_ if the
| overflow happened, and this is where C 's lack of any overflow-
| checked arithmetic option is really, really painful.
| dooglius wrote:
| For [1] I would say that when the compiler does not control the
| standard library (e.g. most linux systems) then the compiler
| should treat qsort like any normal non-libc function call, it
| shouldn't be able to detect that I might be relying on
| implementation-specific guarantees. From a libc perspective, I
| would probably have a safe version of qsort used either on
| request or when NDEBUG is undefined, in which the
| implementation must either detect the inconsistency and abort
| in an implementation-defined matter, or select an ordering such
| that a<b in the ordering implies the existence of a finite
| chain a<x_1<x_2<...<x_n<b of comparison results in which each
| x_i is the equivalence class where the compare function said
| two entries were equal. I think that should be consistent with
| most sorting algorithms. (Whether the normal qsort should be
| the same as the safe version would only be needed if there were
| a significant-enough performance improvement.)
|
| For [2] I disagree for two reasons. For one, merely having an
| answer at all, rather than entering an undefined behavior is
| valuable; having x+y=pseudorand(x,y) would still be better than
| complete UB because the damage that can be done is limited. The
| other reason is that addition/subtraction/multiplication does
| actually work modulo 2^n, so if I write e.g. (a+b-c) where b
| can be large but b-c is known to be small, it works even if the
| language is technically left-associative and having the a+b
| first is UB.
|
| I pretty much agree with your point that the main problem is
| there is no way to work around UB; if there were a workaround
| there would be much less reason to complain. I would go a bit
| further and say such a workaround needs to be in the code
| itself to be effective, not merely a command line arg like
| fwrapv, so that it gets kept around when doing header inlining,
| LTO, copy-pasting code to other projects, etc.
| jcranmer wrote:
| > For [1] I would say that when the compiler does not control
| the standard library (e.g. most linux systems) then the
| compiler should treat qsort like any normal non-libc function
| call, it shouldn't be able to detect that I might be relying
| on implementation-specific guarantees.
|
| I am guessing, for example, that you are not aware that every
| compiler will optimize `printf("Hello, world!\n");` to
| `puts("Hello, world!\n");`. memcpy and memset are even more
| heavily-optimized function calls by the compiler: for
| example, they don't prevent memory-to-register promotion.
| dooglius wrote:
| I am aware of these things, the rant by Ulrich Drepper on
| this breaking glibc guarantees [0] explicitly what I had in
| mind when writing that.
|
| [0] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=25609
| mcguire wrote:
| From the paper:
|
| " _For example, a well-known security issue in the Linux kernel
| was produced by a compiler incorrectly assuming a pointer null
| check was unnecessary ([40] fig. 6) and deleting it as an
| optimization. Or consider this (simplified) patch report for
| Linux [25]: The test [for termination] in this loop: [...] was
| getting completely compiled out by my gcc, 7.0.0 20160520. The
| result was that the loop was going beyond the end of the [...]
| array and giving me a page fault [...] I strongly suspect it's
| because __start_fw and __end_fw are both declared as (separate)
| arrays, and so gcc concludes that __start_fw can never point to
| __end_fw. By changing these variables from arrays to pointers,
| gcc can no longer assume that these are separate arrays._ "
|
| Both of those sound like simple bugs in the compiler optimizer
| implementation, in which case they could hardly be use as
| examples of how the current C standard was bad.
| TazeTSchnitzel wrote:
| They aren't bugs. The C standard considers those cases to be
| undefined.
| foxfluff wrote:
| No, they're definitely features. The removal of null pointer
| checks in gcc is enabled by -fdelete-null-pointer-checks and is
| often enabled by default.
| flykespice wrote:
| Wow, this was an eye-opening read on my trust with ISO C.
|
| It makes much more understandable why linux codebase is riddled
| with compiler extensions, ISO C is simply not reliable anymore.
|
| The issue is bigger than what trembles on the surface, just like
| Dennis Ritchie said, it is a timebomb, soon enough these nuances
| will burst into a big issue in linux kernel, or worse yet, some
| essential system like avionics.
| foxfluff wrote:
| This was discussed somewhat recently:
| https://news.ycombinator.com/item?id=28779036
| zokier wrote:
| ISO C is mostly concerned about making sure that stuff is
| portable; operating systems on the other hand are intrinsically
| platform-specific to a degree. So it is not really surprising
| that pure ISO C is not enough for OS development
| yjftsjthsd-h wrote:
| You can still be portable between compilers; Linux builds with
| GCC and Clang, and used to build with tcc (although that
| probably required patches)
| zokier wrote:
| Linux builds on clang after a decade of dedicated effort to
| make it happen, and that is with clang overall being
| comparatively similar to gcc (e.g clang implements many gcc
| extensions):
| https://github.com/ClangBuiltLinux/linux/wiki/Project-
| histor...
| WalterBright wrote:
| D got around the overflow-is-UB problem by declaring that
| 2s-complement arithmetic will be used with wraparound semantics.
|
| Is there any reason for modern C to still support anything else?
| zajio1am wrote:
| Integer overflow is usually logic error, so a reasonable
| default behavior would be a trap instead of silent overflow
| (regardless of how signed integrers are stored in memory). Some
| architectures support that (e.g. MIPS).
| WalterBright wrote:
| The article mentions cases where integer overflow is expected
| behavior.
| mhh__ wrote:
| There are still processors that don't use two's complement,
| although I'm not sure that should really stop them if they
| wanted to declare all C implementations must be t-c.
| aw1621107 wrote:
| Ralf Jung has a blog post looking at some of the claims in this
| paper [0]. Some hopefully representative quotes:
|
| > The paper makes many good points, but I think the author is
| throwing out the baby with the bathwater by concluding that we
| should entirely get rid of this kind of Undefined Behavior. The
| point of this blog post is to argue that we do need UB by showing
| that even some of the most basic optimizations that all compilers
| perform require this far-reaching notion of Undefined Behavior.
|
| <snip>
|
| > I honestly think trying to write a highly optimizing compiler
| based on a different interpretation of UB would be a worthwhile
| experiment. We sorely lack data on how big the performance gain
| of exploiting UB actually is. However, I strongly doubt that the
| result would even come close to the most widely used compilers
| today--and programmers that can accept such a big performance hit
| would probably not use C to begin with. Certainly, any proposal
| for requiring compilers to curtail their exploitation of UB must
| come with evidence that this would even be possible while keeping
| C a viable language for performance-sensitive code.
|
| > To conclude, I fully agree with Yodaiken that C has a problem,
| and that reliably writing C has become incredibly hard since
| undefined behavior is so difficult to avoid. It is certainly
| worth reducing the amount of things that can cause UB in C, and
| developing practical tools to detect more advanced kinds of UB
| such as strict aliasing violations.
|
| <snip>
|
| > However, I do not think this problem can be solved with a
| platform-specific interpretation of UB. That would declare all
| but the most basic C compilers as non-compliant. We need to find
| some middle ground that actually permits compilers to
| meaningfully optimize the code, while also enabling programmers
| to actually write standards-compliant programs.
|
| [0]: https://www.ralfj.de/blog/2021/11/24/ub-necessary.html
| GoblinSlayer wrote:
| I'm afraid the author as usual confuses the cases when UB
| produces strange results and when the compiler actively and
| deliberately exacerbates the results. He also claims gcc
| interpretation of UB provides a lot of performance. Ironically
| gcc or clang already do experiments on different interpretation
| of UB like signed integer overflow and implicit initialization.
| scott_s wrote:
| HN thread: https://news.ycombinator.com/item?id=29435263
| ghoward wrote:
| >> We sorely lack data on how big the performance gain of
| exploiting UB actually is. However, I strongly doubt that the
| result would even come close to the most widely used compilers
| today--and programmers that can accept such a big performance
| hit would probably not use C to begin with. Certainly, any
| proposal for requiring compilers to curtail their exploitation
| of UB must come with evidence that this would even be possible
| while keeping C a viable language for performance-sensitive
| code.
|
| There actually is data, [1] and it goes against intuition
| because it shows that UB makes code _slower_.
|
| It turns out that when programmers are faced with the
| possibility of UB, they (quite rightly) try to work around it.
| (In my case, I implemented signed two's-complement arithmetic
| entirely with unsigned types. I also implemented my own array
| types, including bounds checks when indexing the arrays.) These
| workarounds make their code slower, in general.
|
| When you think about it that way, it makes sense because
| programmers almost never want UB in their software, so on
| average, the software that people care about will work around
| UB and become slower.
|
| UB in C was defensible when platforms were not homogenous and
| when compiler writers did not abuse the spirit of it. But
| nowadays, most of it _is_ indefensible because platforms are
| mostly homogenous, and compiler writers have been abusing UB
| for "performance."
|
| We saw the same thing happen with speculative execution in
| chips. For years, chip manufacturers got away with lots of
| tricks to increase performance. Then Spectre and Meltdown were
| discovered. As a result, a lot of software had to be changed,
| which resulted in the software slowing down.
|
| (Yes, there are now mitigations in chips, but I think those
| mitigations will be successfully attacked too. I think that it
| will continue that way until most or all speculative execution
| is removed.)
|
| Likewise, with compilers exploiting UB against the original
| spirit of UB, which was just to make C easy to port to any
| architecture, we are probably going to have a reckoning about
| it in the same way we did Spectre and Meltdown. In a way, we
| already do, but it's spread out like a thousand paper cuts.
| Maybe that means that it will stay invisible enough that
| programmers never wake up; I hope not.
|
| tl;dr: compilers exploiting UB actually slows down good code in
| much the same way that Spectre and Meltdown do.
|
| [1]:
| https://www.complang.tuwien.ac.at/kps2015/proceedings/KPS_20...
| bigbillheck wrote:
| > the original spirit of UB, which was just to make C easy to
| port to any architecture
|
| I thought the original spirit of undefined behavior was 'we
| have competing compiler implementations and don't have the
| political will to pick one'.
| ghoward wrote:
| I think those are one and the same; I was just more polite
| about it, I guess. I think yours is closer to the truth
| than mine.
| sanxiyn wrote:
| That data is bullshit. It measures -fno-strict-overflow -fno-
| strict-aliasing. I in fact agree flipping defaults on these
| UBs is reasonable, but that is very far from "portable
| assembler" or UB-free C.
|
| THE primary UB of C is out-of-bounds write. Any proposal for
| UB-free C should address how to compile out-of-bounds write.
|
| If you insist out-of-bounds write should compile in "portable
| assembler" way and reliably corrupt memory instead of
| allowing compilers to assume it doesn't happen, compilers
| can't allocate local variables in register. If it's on stack,
| out-of-bounds write may reliably corrupt stack, so how can it
| be on register?
|
| "Portable assembler" people invariably say "that's not what I
| meant!" when compiler people raise this issue, but the
| question is "then what do you mean?" And I am yet to see any
| good answer.
| mpweiher wrote:
| > and reliably corrupt memory
|
| Who said memory corruption had to occur "reliably"??
|
| "Permissible undefined behavior ranges from ignoring the
| situation completely with unpredictable results"
|
| > compilers can't allocate local variables in register.
|
| Why not?
|
| > If it's on stack, out-of-bounds write may reliably
| corrupt stack, so how can it be on register?
|
| "Permissible undefined behavior ranges from ignoring the
| situation completely with unpredictable results"
|
| > And I am yet to see any good answer.
|
| What would qualify an answer as "good" IYHO?
| indymike wrote:
| >> and reliably corrupt memory > Who said memory
| corruption had to occur "reliably"??
|
| Usually when I write code that corrupts memory, it
| reliably corrupts memory an unreliable way.
| nkurz wrote:
| Could you you explain your position here more fully? I'm a
| C programmer mostly in the "portable assembler" camp, and I
| don't see why insisting on out-of-bound writes to be
| performed prevents compilers from keeping local variables
| in registers. I think the difference might be your use of
| the word "reliably".
|
| I don't think anyone in my camp is insisting that the
| results be reliable in the sense you seem to take for
| granted. What we want is for the compiler to attempt to do
| what it's instructed to do, without eliding code based on
| counterfactual reasoning that assumes the absence of
| undefined behavior. In the absence of an explicit read that
| is being ignored, we're generally fine with cached values
| of local variables being out of date.
|
| Maybe you could give an example that makes your point more
| clearly, and we can see if we actually disagree?
| sanxiyn wrote:
| See https://news.ycombinator.com/item?id=30024534 (by
| jcranmer) and
| https://news.ycombinator.com/item?id=30026095 (by me).
| Does it help?
| nkurz wrote:
| Unfortunately, these don't help much.
|
| I thought about responding to jcranmer's post instead of
| yours, but wasn't sure how to approach it. It's a good
| comment, and I appreciated his attempt, but I feel like
| he's thoroughly missing the point of the complaints about
| UB. The complaint isn't that too much UB exists in C, but
| that compiler writers seem to use the presence of UB as
| an excuse for being allowed to break code that has worked
| historically. And the complaint isn't just that the code
| is broken, but that no apparent care is taken to avoid
| doing so despite the small amount of gain. It's a
| worldview mismatch, and I don't know how to bridge it.
|
| Your comment seems about the same as the one I responded
| to. You seem to assume that people who complain about UB
| in C would have a problem with keeping local variables in
| registers, but I've never seen anyone actually make this
| complaint. Take for example the Arxiv paper we are
| discussing: he doesn't bring this up. This makes me
| suspect that your mental model of the people who are
| complaining about UB in C is probably flawed. I
| understand the technical issue you are gesturing toward,
| I just don't see it as being something that refutes any
| of the issues brought up in the Arxiv paper.
|
| My hope was that a concrete example might help to
| clarify, but I do realize this might not be the right
| forum for that.
| jcranmer wrote:
| My experience with a lot of the UB-is-bad crowd is that
| they don't have much of an appreciation for semantics in
| general. That is to say, they tend to react to particular
| compiler behavior, and they don't have any suggestions
| for how to rectify the situation in a way that _preserves
| consistent semantics_. And when you try to pin down the
| semantics, you usually end up with a compiler that has to
| "do what I mean, not what I said."
|
| A salient example that people often try on me, that I
| don't find persuasive, is the null pointer check:
| void foo(int *x) { int val = *x; if (!x)
| return; /* do real stuff */ }
|
| "The compiler is stupid for eliminating that null check!"
| "So you want the code to crash because of a null pointer
| dereference then." "No, no, no! Do the check first, and
| dereference only when it's not null!" "That's not what
| you said..."
| nkurz wrote:
| > "No, no, no! Do the check first, and dereference only
| when it's not null!"
|
| I don't think I've heard anyone express this preference.
| If they did, I'd agree with you that they are being
| unreasonable. My impression is that practically everyone
| on the "portable assembler" team would think it's
| perfectly reasonable to attempt the read, take the
| SIGSEGV if x is NULL, and crash if it's not handled. Most
| would also be happy skipping the read if the value can be
| shown to be unused. None would be happy with skipping the
| conditional return.
|
| Where it gets thornier is when there is "time travel"
| involved. What if the unprotected read occurs later, and
| instead of just returning on NULL, we want to log the
| error and fall through: void foo(int *x)
| { if (!x) /* log error */; int val = *x;
| return; }
|
| Is it reasonable to have the later unconditional read of
| x cause the compiler to skip the earlier check of whether
| x is NULL? In the absence of an exit() or return in the
| error handler, I think it would be legal for the compiler
| to skip both the error logging and the unused load and
| silently return, but I don't want the compiler to do
| this. I want it to log the error I asked it to (and then
| optionally crash) so I can identify the problem.
| Alternatively, I'd probably be happy with some compile
| time warning that tells me what I did wrong.
| mzs wrote:
| On a lot of systems load from address zero doesn't
| segfault, I'm fine with the cpu loading it. I'm
| disappointed that the new compiler removed the check
| someone wrote a dozen years ago to prevent the cpu from
| overwriting something important though.
| gpderetta wrote:
| The null pointer need not map to address zero exactly to
| cater to this sort of corner cases.
| jcranmer wrote:
| In the case where loads from address zero are legal, the
| dereference-means-non-null-pointer assumption is
| disabled.
| mzs wrote:
| Seems it wasn't disabled in 2009
|
| https://github.com/torvalds/linux/commit/a3ca86aea5079041
| 488...
| jcranmer wrote:
| That case wasn't one of the cases where null pointers are
| valid memory, judging from the commit message. (There
| _was_ a case where gcc had a bug where it didn 't disable
| that check properly, but this doesn't appear to be that
| case.)
| bewaretheirs wrote:
| Actually, yes, I do want that code to crash if x is NULL.
| pcwalton wrote:
| Then you should logically be OK with deleting the null
| pointer check, as that check is unreachable.
| nkurz wrote:
| Are you familiar with this classic example?
| http://blog.llvm.org/2011/05/what-every-c-programmer-
| should-...
|
| The problem (if I understand it right) is that "Redundant
| Null Check Elimination" might be run first, and will get
| rid of the safety return. But then the "Dead Code
| Elimination" can be run, which gets rid of the unused
| read, and thus removes the crash. Which means that rather
| than being logically equivalent, you can end up with a
| situation where /* do real stuff */ (aka /* launch
| missiles */) can be run despite the programmer's clear
| intentions.
| jwatte wrote:
| I would expect the code to crash when passed a null
| pointer, absolutely! And the if statement is there to let
| me recover after using "continue" in the debugger. And if
| I run on an ISA without protected memory, that load will
| not actually crash, and I'm OK with that. That's a level
| of UB differing-behavior (more like ID, really) that's
| reasonable.
|
| I know of no experienced C programmer who would expect
| the compiler to re-order the statements. That sounds very
| much like a strawman argument to me!
|
| I'd also be OK with a compiler that emitted a warning:
| "This comparison looks dumb because the rules say it
| should do nothing." Emitting that warning is helpful to
| me, the programmer, to understand where I may have made a
| mistake. Silently hiding the mistake and cutting out
| parts of code I wrote, is _not_ generally helpful, even
| if there exist some benchmark where some inner loop will
| gain 0.01% of performance if you do so.
|
| After all: The end goal is to produce and operate
| programs that run reliably and robustly with good
| performance, with minimum programmer cost. Saying that
| the possible performance improvements of code that
| _nobody will run because it 's buggy_ absolutely trump
| every other concern in software development, would be a
| statement I don't think reflects the needs of the
| software development profession.
| sanxiyn wrote:
| No, I don't assume people are against register
| allocation, but any concrete proposal I have seen kind of
| _implies_ such conclusion. I am trying to understand what
| people actually want, since they seem clearly different
| from what people _say_ they want.
|
| Okay let's discuss a concrete example.
| *x = 12345678; f(); return *x; // can you
| copy propagate 12345678 to here?
|
| f() does this: for (int *p = 0; p++; p
| < MEMSIZE) if (*p == 12345678)
| *p = 12345679.
|
| That is, f scans the memory for 12345678 and replace all
| instances with 12345679. There is no doubt this actually
| works that way in assembly. Things like cheat engines do
| this! C compilers assume this doesn't happen, because it
| is UB.
|
| Hence, portable assembly C compiler can't omit any load.
| Now I understand there are minority of people who will
| answer "that's what I want!", but like register
| allocation, I think people generally want this to
| optimize. But that necessarily implies memory search-and-
| replace can't compile in portable assembly manner.
| nkurz wrote:
| Thanks for making an example!
|
| I'm against compiling this particular example to return a
| constant. I presumably wrote the awkward and unnatural
| construction return *x because I want to to force a load
| from x at return. If I wanted to return a constant, I'd
| have written it differently! I'm odd though in that I
| occasionally do optimizations to the level where I
| intentionally need to force a reload to get the assembly
| that I want.
|
| Philosophically, I think our difference might be that you
| want to conclude that one answer to this question
| directly implies that the "compiler can't omit any load",
| while I'd probably argue that it's actually OK for the
| compiler to treat cases differently based on the apparent
| intent of the programmer. Or maybe it's OK to treat
| things differently if f() can be analyzed by the compiler
| than if it's in a shared library.
|
| It would be interesting to see whether your prediction
| holds: do a majority actually want to return a constant
| here? My instinct is that C programmers who complain
| about the compiler treatment of UB behavior will agree
| with me, but that C++ programmers dependent on
| optimizations of third party templates might be more
| likely to agree with you.
| sanxiyn wrote:
| Oh, so you are on "that's what I want!" camp. But I am
| pretty sure you are in minority, or at the very least
| economic minority. Slowdown implied by this semantics is
| large, and easily costs millions of dollars.
|
| > while I'd probably argue that it's actually OK for the
| compiler to treat cases differently based on the apparent
| intent of the programmer.
|
| This is actually what I am looking for, i.e. answer to
| "then what do you mean?". Standard should define how to
| divine the apparent intent of the programmer, so that
| compilers can do divination consistently. So far,
| proposals have been lacking in detailed instruction of
| how to do this divination.
| bvrmn wrote:
| > and easily costs millions of dollars
|
| Looks like UB bugs can cost more. It's a new age of UB
| sanitizers as a reaction to a clear problem with UB.
| pcwalton wrote:
| Bugs based on _optimizations_ that compilers make based
| on assumptions enabled by undefined behavior (like the
| null check issue from 2009 in the Linux kernel) actually
| don 't cost very much. They get a disproportionate amount
| of scrutiny relative to their importance.
| mjevans wrote:
| A programmer wrote things in a specific order for a
| specific reason.
|
| Lets instead assume that the variable assignments above
| are to some global configuration variables and then f()
| also references those and the behavior of f() changes
| based on the previously written code.
|
| The objections from the 'C as portable assembler' camp
| are:
|
| * Re-ordering the order of operations across context
| switch bounds (curly braces and function calls). -- re-
| ordering non-volatile store / loads within a context is
| fine, and shouldn't generate warnings.
|
| * Eliminating written instructions (not calling f())
| based on optimizations. -- Modification to computed work
| should always generate a warning so the optimization can
| be applied to the source code, or bugs corrected.
| wbl wrote:
| Globals and locals are different. All compilers will give
| a global a specific memory location and load and store
| from it. Locals by contrast can be escape analyzed.
| mjevans wrote:
| The example didn't show where X was defined; it could be
| anything.
| comp54321 wrote:
| There's not a bright line between optimization passes
| being aware of UB in unreasonable vs "exploitative" ways.
| The principled way to think about optimization is that an
| optimization pass can assume certain invariants about the
| code but must preserve other invariants through the
| transformation as long as the assumptions hold. I think
| basically any invariant you assume in a memory-unsafe
| language C could be invalidated by real code.
|
| A lot of the things people complain about are cases where
| the compiler got better at reasoning through valid
| transformations to the code, _not_ cases where the
| compiler . E.g. I 'm sure that even very old compilers
| have had optimization passes that remove redundant NULL
| checks that logically depend on dereferencing NULL
| pointers being undefined. But these probably were more
| ad-hoc and could only reason about relatively simple
| cases and constrained scopes. Another example is integer
| overflow. I'd bet a lot of older compilers have loop
| optimizations that somehow depend on the loop counter not
| overflowing, or pointer addresses not wrapping around.
|
| I think it's perfectly reasonable to say that C's
| definition of undefined behaviour is too expansive and
| too difficult to avoid in real code.
|
| But I think that a lot of the complaints end up being
| equivalent to "don't depend on UB in ways that I notice
| in my code" or "don't depend on UB in ways that compilers
| from the 90s didn't". That's not something you can
| practically apply or verify when building a compiler
| because there isn't a bright line between assuming an
| invariant in reasonable vs unreasonable ways, unless you
| can define a narrow invariant that distinguishes them.
| ghoward wrote:
| I want a safe C. If that means I have to deal with fewer
| available optimizations, I'm all for that! That's why I
| wrote two's-complement arithmetic with unsigned types and
| why I actually have bounds checks on my array accesses in
| my code.
|
| That means I would gladly take a compiler that could not
| allocate local variables in registers.
|
| Or even better, just do a bounds check. The standard does
| not preclude storing a raw pointer and a length together.
| Why couldn't the compiler do something like that and do a
| bounds check?
|
| (Yes, the compiler would have to use its own version of
| libc to make that happen, but it could.)
|
| In other words, I want correct code first, performant code
| second. I'll take the performance hit for correct code.
|
| People claim that exploiting UB gives 1-2% speedup and that
| that's important. What they conveniently leave out is that
| you only get that speedup _when your code has UB_ , which
| means that you only get that speedup when your code has a
| bug in it.
|
| Fast wrong code is bad. Very bad. Correct code, even if
| slower, is an absolute must.
| foxfluff wrote:
| > People claim that exploiting UB gives 1-2% speedup and
| that that's important. What they conveniently leave out
| is that you only get that speedup when your code has UB,
| which means that you only get that speedup when your code
| has a bug in it.
|
| _Assuming there is no UB_ and optimizing under that
| assumption gives a speedup. You get the speedup whether
| that assumption is wrong or not. If that assumption is
| wrong, your program might also blow up in hilarious ways.
| But in no way does making the assumption that your code
| has no undefined behavior rely on your code having
| undefined behavior.
| ghoward wrote:
| > Assuming there is no UB and optimizing under that
| assumption gives a speedup.
|
| Sure, but to make sure you do not have any UB, at least
| for things like signed arithmetic, you basically have to
| either give up signed arithmetic, which is what I've done
| by using only unsigned, or ensure your arithmetic is
| always within range, which requires formal methods.
| Otherwise, you have UB, and that 1-2% speedup is because
| of that UB. If you do what I do, you get no speedup. If
| you use formal methods to ensure ranges, you do, but at
| an enormous cost.
|
| > But in no way does making the assumption that your code
| has no undefined behavior rely on your code having
| undefined behavior.
|
| But it does. Because the optimizations that assume UB
| only kick in when UB could be present. And if UB _could_
| be present, it most likely is.
|
| Once again, those optimizations will _never_ kick in on
| my code that uses purely unsigned arithmetic. So yes,
| those optimizations _do_ require UB.
| foxfluff wrote:
| Is it formal methods to conclude that the vast majority
| of arithmetic, which for the software I typically write
| consists of iterators and buffer offsets dealing with
| fixed size buffers, can never overflow an int? Is it
| formal methods when I conclude that the 12-bit output
| from an ADC cannot overflow a 32-bit int when squared? Is
| it formal methods to conclude that a system with 256
| kilobytes of RAM is not going to have more than 2^31
| objects in memory? I guess I do formal methods then, at
| an enormous cost. Sometimes I have runtime checks with
| early bail outs that allow code later down the stream to
| be optimized.
| MaxBarraclough wrote:
| > I would gladly take a compiler that could not allocate
| local variables in registers
|
| That sounds rather extreme. I imagine it would be ruinous
| to performance. To my knowledge the various safe
| alternatives to C have no trouble with register
| allocation. Safe Rust and Ada SPARK, for instance, or
| even Java.
|
| > The standard does not preclude storing a raw pointer
| and a length together. Why couldn't the compiler do
| something like that and do a bounds check?
|
| That technique is called _fat pointers_. It 's been well
| researched. Walter Bright, who just posted a comment
| elsewhere in this thread, has a blog post on it. [0] I
| imagine ABI incompatibility is one of the main reasons it
| hasn't caught on.
|
| C++ compilers do something similar, storing array lengths
| in an address like _myArray[-1]_. They need to store
| array lengths at runtime because of the _delete[]_
| operator (unless they can optimise it away of course).
| C++ still allows all sorts of pointer arithmetic though,
| so it wouldn 't be easy for a compiler to offer Java-like
| guarantees against out-of-bounds access. Doing so takes a
| sophisticated tool like Valgrind rather than just another
| flag to gcc.
|
| [0] https://digitalmars.com/articles/C-biggest-
| mistake.html
| pcwalton wrote:
| Why not compile at -O0? That gives you everything that
| you want today, including no register allocation.
| sanxiyn wrote:
| Yes, this is a consistent position, but bounds checking
| all indexing and making pointers larger by including
| length will cause slowdown much larger than 1-2%. My
| estimate is at least 10% slowdown. So citing 1-2%
| slowdown is misleading.
| ghoward wrote:
| I agree 1-2% might be misleading, but Dr. John Regehr
| puts overflow checking overhead at 5%. [1]
|
| I'll take 5% or even 10% for correctness because, again,
| fast and wrong is very bad.
|
| [1]: https://blog.regehr.org/archives/1154
| jwatte wrote:
| The concept of "friendly C" is not new, and this is one of
| the straw men that it knocked down, because it is actually
| possible to define "what I mean."
|
| It's totally OK to say that an out-of-bounds write may or
| may not be detectable by reading any particular other local
| or global variable or heap value. That doesn't in turn
| translate to compilers being allowed to turn a bug in a
| SPECint kernel into an empty infinite loop (a case that
| happened!)
| mlindner wrote:
| I pretty strongly disagree with these quotes you have here. You
| don't need UB to do optimization, in fact because of UB in C
| many forms of optimization are impossible in C because of
| aliasing uncertainty. If things are defined to not alias then
| even though things are fully defined there is a lot more
| optimization that can be performed by not reloading variables
| from memory and instead using local cached-in-register copies
| of variables. This is why naive Rust can in some cases be
| faster than naive C (without using of restrict keyword) and
| this will only improve with time as compilers better support
| this.
| aw1621107 wrote:
| > in fact because of UB in C many forms of optimization are
| impossible in C because of aliasing uncertainty.
|
| This doesn't seem right? Specific types of aliasing make
| optimization more difficult/impossible because they are
| specifically _not_ UB. Rust 's aliasing optimizations can
| (theoretically?) occur specifically because same-type
| aliasing _is_ UB in Rust. I 'm not sure how "UB prevents
| optimization" falls out of that?
| bee_rider wrote:
| See also decades of Fortran performance dominance, only
| overcome by throwing a gazillion C programmers at the
| problem.
| gpderetta wrote:
| but doesn't Fortran get away with it exactly because
| argument aliasing is UB? You can use restrict to get
| Fortran semantics.
| bee_rider wrote:
| Actually yeah, you are right. I somehow misread it as a
| defense of UB because of the last sentence. Total a
| brain-fart on my part, sorry.
| puffoflogic wrote:
| > If things are defined to not alias
|
| ... What do you call it then when you put aliasing pointers
| into pointer variables "defined not to alias"? Can you
| explain how this _reduces_ UB?
| foxfluff wrote:
| I think they're trying to say that if your language
| _prevents_ aliasing (i.e. pointers by definition do not
| alias and it is impossible to make them alias), then the
| compiler doesn 't need to worry about it. C doesn't prevent
| aliasing, therefore the compiler has to worry about every
| case of aliasing that isn't undefined.
|
| But it's not the UB here that prevents optimization, it's
| precisely the fact that aliasing is (sometimes) defined. If
| it were always UB, the compiler wouldn't ever have to worry
| about it... but that'd require some interesting changes to
| the language to keep it usable. Definitely not backwards
| compatible changes.
| puffoflogic wrote:
| andrewla wrote:
| > pointers by definition do not alias and it is
| impossible to make them alias
|
| This is equivalent to saying that pointer arithmetic is
| disallowed. Pointers are by their nature in the C virtual
| machine offsets into a linear memory space, so for any
| two pointers, x and y, there exists a c such that
| (ptr_t)x+c == (ptr_t)y, and thus there can always be
| aliasing.
| gpderetta wrote:
| Actually no, the C abstract machine is not contiguous. If
| x and y point inside distinct objects (recursively) not
| part of the same object, there is no valid c that you can
| add to x to reach y.
|
| edit: allowing that would prevent even basic
| optimizations like register allocation.
|
| edit: s/virtual/abstract/
| andrewla wrote:
| The C abstract machine requires reversible
| transformations from pointer to integral types (7.18.1.4
| in C99, 3.3.4 in C90, the first ISO standard).
|
| Practically speaking modern devices are in fact mostly
| flat, so you can of course do this, although you do brush
| up against undefined behavior to get there.
| int_19h wrote:
| Reversible transformations don't imply a flat address
| space. All it means is that there's an integral type with
| enough bits to record any address in them.
| wbl wrote:
| Pointer provenance is more complex and subtle.
| Shikadi wrote:
| What is the c virtual machine? I thought there wasn't one
| foxfluff wrote:
| They mean the abstract machine, in terms of which the
| semantics are defined.
| skissane wrote:
| > Pointers are by their nature in the C virtual machine
| offsets into a linear memory space
|
| Historically, many platforms - such as Multics or Windows
| 3.x - didn't have a linear memory space, they had some
| kind of memory segmentation. The industry has largely
| moved away from that towards the flat address space
| model. Go back to the 1980s, it was still a much bigger
| thing, and people used C on those platforms, and the
| standard was written to support them. The actual detailed
| control of memory segmentation is inherently non-portable
| so cannot be addressed by the standard, but the standard
| defines pointer arithmetic in such a way to support those
| platforms - pointer arithmetic on unrelated pointers is
| undefined, because if the pointers belong to different
| memory segments the results can be meaningless and
| useless.
| andrewla wrote:
| Even in cases of segmented memory architectures there is
| still a requirement that a void pointer be able to cast
| (reversably) into an integral type (7.18.1.4 in C99,
| 3.3.4 in C90, the first ISO standard).
| Sprocklem wrote:
| This is not true in either of those C versions.
| C99SS7.18.1.4 describes (u)intptr_t as optional, which
| (per SS7.18) means that <stdint.h> need not provide those
| typedefs if the (compiler) implementation doesn't provide
| an integer type that allows reversible casting from a
| void pointer. Similarly, it's not clear in C90SS3.3.4
| that the implementation has to implement these casts in a
| reversible manner, although that is the obvious way of
| implementing it.
|
| That being said, I can't think of an implementation that
| didn't support this, even if they did have to pack
| segment and offset information into a larger integer.
| mzs wrote:
| I really wish we got
|
| cc: warning: UB line: 123 file: foo.c
| foxfluff wrote:
| Yeah, it'd be nice if we could solve the halting problem.
| tlb wrote:
| You can't detect most UB at compile time. LLVM has a system
| to detect it a runtime. There is a significant performance
| penalty, but it can be useful during testing.
|
| See
| https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
| jcranmer wrote:
| How many warnings do you want for this small function?
| void oh_the_humanity(int *ptr, int val) { *ptr = val
| + 1; }
|
| Off the top of my head:
|
| * UB: ptr may be pointing a float variable. (It's not illegal
| to assign a float* to an int*, it's only UB when you actually
| dereference it with the wrong type.)
|
| * UB: val + 1 may overflow.
|
| * UB: potential data race on writing *ptr.
|
| * UB: ptr may be a one-past-the-end-of-the-array pointer,
| which can be validly constructed, but may not be
| dereferenced.
|
| * UB: ptr may be pointing to an object whose lifetime has
| expired.
|
| * UB: ptr may be uninitialized.
|
| * UB: val may be uninitialized.
|
| As you can see, UB is intensely specific to the actual data
| values; it's not really possible to catch even a large
| fraction of UB statically without severe false positive
| rates.
| mzs wrote:
| Yeah I know I get it, it's me more being wishful, but I
| more seriously wish at least compilers could emit a warning
| when they optimize something after UB: %
| cat foo.c #include <stdlib.h> int
| foo(int *bar) { int baz = *bar;
| if (bar == NULL) exit(2); return (baz);
| } % cc -O3 -Wall -Wextra -c foo.c % objdump -dr
| foo.o foo.o: file format Mach-O
| 64-bit x86-64 Disassembly of section
| __TEXT,__text: 0000000000000000 _foo:
| 0: 55 pushq %rbp 1:
| 48 89 e5 movq %rsp, %rbp
| 4: 8b 07 movl (%rdi), %eax
| 6: 5d popq %rbp 7:
| c3 retq % cc -O0 -Wall
| -Wextra -c foo.c % objdump -dr foo.o
| foo.o: file format Mach-O 64-bit x86-64
| Disassembly of section __TEXT,__text:
| 0000000000000000 _foo: 0: 55
| pushq %rbp 1: 48 89 e5
| movq %rsp, %rbp 4: 48 83 ec 10
| subq $16, %rsp 8: 48 89 7d f8
| movq %rdi, -8(%rbp) c: 48 8b 45 f8
| movq -8(%rbp), %rax 10: 8b 08
| movl (%rax), %ecx 12: 89 4d f4
| movl %ecx, -12(%rbp) 15: 48 83 7d f8 00
| cmpq $0, -8(%rbp) 1a: 0f 85 0a 00 00 00
| jne 10 <_foo+0x2a> 20: bf 02 00 00 00
| movl $2, %edi 25: e8 00 00 00 00
| callq 0 <_foo+0x2a> 0000000000000026:
| X86_64_RELOC_BRANCH _exit 2a: 8b 45 f4
| movl -12(%rbp), %eax 2d: 48 83 c4 10
| addq $16, %rsp 31: 5d
| popq %rbp 32: c3
| retq %
|
| That's very similar to something that bit me in embedded
| except it was with pointer to structure. Compiler realizes
| I've derefed NULL and that's UB anyway so no need to do the
| NULL check later and merrily scribble exc vectors or
| whatever.
| foxfluff wrote:
| That's a nice example. It'd definitely be nice to have a
| warning for this one.
|
| Fwiw GCC does have a related warning flag (-Wnull-
| dereference) but I'm not sure it's made exactly for this.
| I believe it works based on functions being annotated for
| possibly returning NULL, e.g. malloc. It's also not
| enabled by -Wall or -W because apparently there were too
| many false positives:
| https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96554
|
| I imagine patches would be welcome. I'm guessing there
| are more people who like to wish for more compiler
| features than there are people who like to develop
| compilers :)
| mzs wrote:
| edit: Thanks, I just checked and the warning doesn't work
|
| https://godbolt.org/z/4TP1hfx4j
|
| But your hint found -fno-delete-null-pointer-checks which
| does the trick
|
| https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
|
| And -fno-delete-null-pointer-checks made it into LLVM
| too. It's a good to know but a little late for when I
| needed it, cheers :) % cc -O3 -fno-
| delete-null-pointer-checks -Wall -Wextra -c foo.c %
| objdump -dr foo.o
| foo.o: file format Mach-O 64-bit x86-64
| Disassembly of section __TEXT,__text:
| 0000000000000000 _foo: 0: 55
| pushq %rbp 1: 48 89 e5
| movq %rsp, %rbp 4: 48 85 ff
| testq %rdi, %rdi 7: 74 04
| je 4 <_foo+0xd> 9: 8b 07
| movl (%rdi), %eax b: 5d
| popq %rbp c: c3
| retq d: bf 02 00 00 00 movl
| $2, %edi 12: e8 00 00 00 00
| callq 0 <_foo+0x17> 0000000000000013:
| X86_64_RELOC_BRANCH _exit
| zajio1am wrote:
| But such code might be generated by macros (or some code
| generator), in which case silent elimination of
| unnecessary code is expected and wanted behavior.
| [deleted]
| gpderetta wrote:
| Compilers will routinely warn if they can detect UB at
| compile time.
|
| The problem is that except in a few trivial cases it is
| impossible to detect UB at compile time. Even whole program
| static analysis can only catch a small subset.
| mpweiher wrote:
| He doesn't really address any of the claims made, and makes
| some startling claims himself. For example:
|
| > This example demonstrates that even ICC with -O1 already
| requires unrestricted UB.
|
| The example demonstrates nothing of the sort. It demonstrates
| that ICC _uses_ unrestricted undefined behaviour, not that this
| is required in any way shape or form. (The only way the word
| "requires" is reasonable here is that the behavior seen
| "requires" use of this kind of UB to be present. But that's
| something very different, and doesn't match with the rest of
| his use).
|
| > writing C has become incredibly hard since undefined behavior
| is so difficult to avoid
|
| No, it has become difficult because compilers exploit UB in
| insane ways. The platform specific UB that he claims is "not an
| option" is, incidentally, exactly how UB is defined in the
| standard:
|
| _Permissible undefined behavior ranges from ignoring the
| situation completely with unpredictable results, to behaving
| during translation or program execution in a documented manner
| characteristic of the environment (with or without the issuance
| of a diagnostic message), to terminating a translation or
| execution (with the issuance of a diagnostic message)._
|
| This was made non-binding in later versions of the standard, so
| optimiser engineers act as if these words don't exist.
|
| And of course there is no reason given for this interpretation
| being "not an option". Except for "but I wanna". Yes, it's not
| an option if you really want to exploit UB in the insane ways
| that compilers these days want to exploit it.
|
| But that's not _necessary_ in any way shape or form.
|
| > That would declare all but the most basic C compilers as non-
| compliant.
|
| Yes, because by any sane interpretation of the standard they
| _are_ non-compliant.
|
| On a more general note, I find this idea that things are
| _necessary_ because I _want_ to do them really bizarre.
| aw1621107 wrote:
| > This was made non-binding in later versions of the
| standard, so optimiser engineers act as if these words don't
| exist.
|
| This is reminiscent of the argument in "How One Word Broke C"
| [0, HN discussion at 1]. I'm not particularly convinced this
| argument is correct. In my opinion, it's "ignoring the
| situation completely" that's the phrase of interest; after
| all, what is assuming that UB cannot occur but "ignoring the
| situation completely"?
|
| I'm a nobody, though, so take that with an appropriate grain
| of salt.
|
| [0]: https://news.quelsolaar.com/2020/03/16/how-one-word-
| broke-c/ (currently broken; archive at https://web.archive.or
| g/web/20210307213745/https://news.quel...
|
| [1]: https://news.ycombinator.com/item?id=22589657
| mpweiher wrote:
| Apologies, but "ignoring the situation completely" is the
| exact opposite of "assuming the situation cannot occur and
| _acting_ on that assumption ".
| aw1621107 wrote:
| I'm not sure how "acting on an assumption that the
| situation cannot occur" is distinguishable from "choosing
| to ignore situations completely whenever they come up".
| The former is a blanket description of how you treat
| individual instances.
|
| For example: int f(int* i) {
| int val = *i; if (i == NULL) { return 0; }
| return val; }
|
| I submit that there are two situations here:
|
| 1. i is NULL. Program flow will be caught by the NULL
| check and the return value is 0.
|
| 2. i is not NULL. The NULL check cannot be hit, and the
| return value is val.
|
| As allowed by the standard, I'll just ignore the
| situation with UB, leaving
|
| > i is not NULL. The NULL check cannot be hit, and the
| return value is val.
|
| Alternatively, I can assume that UB cannot occur, which
| eliminates option 1, leaving
|
| > i is not NULL. The NULL check cannot be hit, and the
| return value is val.
|
| You get the same result either way.
|
| And that gets to the root of the problem: What _exactly_
| does "ignoring the situation completely" mean? In
| particular, what is the scope of a "situation"?
| gpderetta wrote:
| sorry, I don't understand, it seems to me that current
| optimizing compilers are fully compliant with the option of
| "ignoring the situation completely with unpredictable
| results". That's how UB exploiting optimizations work: the
| compiler ignores the possibility that the erroneous case can
| ever happen and takes into account only all valid states.
| mpweiher wrote:
| Apologies, but "ignoring the situation completely with
| undpredictable results" is the exact opposite of "assuming
| the situation cannot occur and _acting_ on that assumption
| ".
|
| If my program attempts to write outside the bounds of
| declared array, ignoring the situation (that this is UB) is
| letting that write happen, and letting the chips fall where
| they might.
|
| How is assuming it cannot/must not happen, and then
| optimising it away because it did happen "ignoring the
| situation"??
| gpderetta wrote:
| So a compiler that is allowed to ignore a situation would
| still be required to generate code for that situation? I
| don't even know how that would be possible.
| commandlinefan wrote:
| I had the same basic thought while reading this: the only
| alternative here is to interpret this sort of undefined
| behavior in a "different" way (such as the way the K&R
| originally interpreted it) and leave it up the programmer to
| intuit what _other_ assumptions the compiler is making on their
| behalf. Like... I can assume that the compiler will interpret:
| if (i++ < i)
|
| as mov ax, i inc ax cmp ax, i
| jge else
|
| ... but that's still me making a (reasonable?) assumption. The
| C spec essentially just says "don't program that way".
| Shikadi wrote:
| Odd intuition, since you never store i back after
| incrementing. Also, I believe i++ might increment after the
| comparison rather than before, vs. ++i incrementing first
| commandlinefan wrote:
| You know what I mean.
| Shikadi wrote:
| I actually don't, but maybe I just lack enough background
| knowledge on the discussion to understand your intent
| saghm wrote:
| I think they're saying that the assembly translation
| doesn't store back `i`, whereas the C version does, so
| it's a not straightforward to assume that the assembly
| compiled from the C won't do that.
| commandlinefan wrote:
| The article talks about a similar code snippet being
| optimized out by the compiler because i++ can never be
| less than i originally was (unless you take into account
| the actual behavior of computers).
| int_19h wrote:
| ++ is itself a store.
| kortex wrote:
| Is it even possible to have zero undefined behavior in languages
| that allow user-defined pointers? It seems like allowing even
| just one degree of memory indirection creates a singularity
| beyond which any kind of formal guarantees become impossible.
| Seems like you'd have to allow only structures which hide memory
| implementation details if you truly want to avoid all UB. Same
| goes for any arithmetic which could overflow.
|
| That would require kernel devs to radically rethink how they
| interact with I/O, which would probably require specific
| architectures.
|
| In other words, writing a kernel portable on any of the existing
| ISAs that is also performant is basically impossible, barring
| some humongous breakthrough in compiler technology.
|
| Seems to me that when it comes to brass tacks, UB is kind of the
| "we are all adults here" engineering tradeoff that enables
| shipping fast and useful software, but is _technically_ not
| strictly defined and thus usually does what you want, but can
| result in bugs.
| immibis wrote:
| Even kernels only interact with memory in reasonably
| predictable ways. I think they could all be hidden behind such
| abstractions, BUT it will make the language a lot more complex.
| kps wrote:
| The original C committee wrote, as part of its guiding
| principle to "Keep the spirit of C", that "To help ensure that
| no code explosion occurs for what appears to be a very simple
| operation, many operations are defined to be _how the target
| machine's hardware does it_ rather than by a general abstract
| rule."1 That is, if you write `a = b + c` you expect the
| compiler to generate an `add a, b, c` instruction, and if that
| happens to trap and burn your house down, well, that 's not C's
| problem.
|
| I'm convinced that the original UB rule was intended to capture
| this, and the wording was an error later seized by compiler
| developers. As evidence, consider Dennis Ritchie's rejection of
| `noalias` as "a license for the compiler to undertake
| aggressive optimizations that are completely legal by the
| committee's rules, but make hash of apparently safe programs"2.
| If _anyone_ at the time had realized that this is what the
| definition of UB implied, it would have been called out and
| rejected as well.
|
| 1 https://www.lysator.liu.se/c/rat/a.html#1-1
|
| 2 https://www.lysator.liu.se/c/dmr-on-noalias.html
| unwind wrote:
| I think expecting the compiler to generate code that makes
| 'a' hold the sum of 'b' and 'c', assuming there is code that
| "observes" the value in some way, is a more useful model.
| gpderetta wrote:
| > if you write `a = b + c` you expect the compiler to
| generate an `add a, b, c`
|
| this could be violated even by simple optimizing compilers
| that do constant propagation or strength reductions.
|
| Again, if you do not want optimizations -O0 is always
| available.
| ErikCorry wrote:
| Nowhere does anyone guarantee that you won't be hit with
| nasal demons if you use -O0
| gpderetta wrote:
| Well, it is hard to guarantee anything about a bit of
| code with no defined semantics, you'll have to settle for
| what O0 gives you.
| kps wrote:
| The statement, in context, is a simple illustration of the
| principle that C operator semantics were defined by the
| target hardware. Not a treatise on compiler construction.
| naniwaduni wrote:
| It would be nice if optimizing compilers' -O0 weren't
| completely insane on the assumption that the insanity would
| get optimized out.
| remram wrote:
| Undefined behavior means something specific in the standard,
| it's not just an operation that might do different things on
| different compilers and machines. It means that if it happens,
| the program is allowed to do anything and everything, even
| before the UB is reached. Undefined behavior is breaking an
| assumption that the compiler is allowed to make.
|
| It is probably impossible to make a low-level language with no
| _implementation-defined_ behavior, but it is certainly possible
| to make one with no _undefined behavior_. For example, you can
| put in your spec that overflowing an unsigned integer can give
| any value; that is different that putting in your spec that it
| doesn 't happen and if you write it the variable might have no
| value, multiple values, or burn your socks off.
|
| https://en.wikipedia.org/wiki/Undefined_behavior
| comp54321 wrote:
| It's actually impossible as far as I can see as long as you
| have unsafe memory access via pointers and memory is used to
| hold information about control flow and local variables,
| because overwriting memory can then do weird and wonderful
| things to the state of the program. E.g. local variables can
| magically change values, but worse, the control flow of your
| program can be hijacked in pretty arbitrary ways.
|
| It would be great if this wasn't possible, because then you
| wouldn't have a whole class of security vulnerabilities.
|
| On older systems without memory protection, the situation is
| even worse - you could scribble all over the OS memory as
| well and who knows what happens then.
| foxfluff wrote:
| > Is it even possible to have zero undefined behavior in
| languages that allow user-defined pointers?
|
| It kinda boils down to what exactly you mean by defined
| behavior. A C programmer's take might be that you can run a
| conforming program in an emulated abstract machine and get
| defined results out of it. And then you can run the same thing
| on real hardware and expect to get the same result (modulo
| implementation defined behavior). This definition leaves some
| things out (e.g. performance, observable effects in the "real
| world") but it captures the computational semantics.
|
| Another programmer's take might be more akin to a portable
| assembler. In that case, you certainly could define reads and
| writes for arbitrary pointers, in the sense that they must
| cause corresponding (attempted) loads and stores at the machine
| level. However, the definition wouldn't be complete since it
| inevitably leaves much to the underlying implementation. Thus
| you could have "defined" C programs that show completely
| different behaviors depending on which implementation and
| hardware you used. It would be impossible to say what the
| program's output must be "in the abstract." For someone who
| just wants to output assembly, maybe that's fine. I'm not sure
| other people would be too satisfied with it. An out of bounds
| write could still blow up your program and be remotely
| exploitable; practically the same thing as undefined behavior,
| except that now your compiler is also barred from optimizing.
|
| There's quite a bit of tension between these two camps.
|
| Alternatively, you could fully define it at a great runtime
| cost and potential exclusion of real hardware implementations.
| pornel wrote:
| If by user-defined pointers you mean arbitrary integer-to-
| pointer casts, then this is a kryptonite for static analysis,
| and I don't think you can have a language that is both fast and
| fully predictable (UB-free) in their presence. It breaks
| pointer provenance and aliasing analysis, and existing
| compilers already struggle with such casts in C.
|
| But apart from that, you can have pointers, with many levels of
| indirection, as long as there are rules that prevent use-after-
| free, unsynchronized concurrent access, and other UB-worthy
| problems. Rust's borrow checker with rules for no mutable
| aliasing and Send/Sync markers for concurrent access comes
| close, but it has to give up on generality for safety (e.g. it
| can't reason about circular data structures).
| ErikCorry wrote:
| Without concurrency you don't have to have UB to have user-
| defined pointers. x86 assembly language has no UB and has user-
| defined pointers.
| ahefner wrote:
| CPUs can and do have UB in the instruction specifications.
| x86 definitely does.
| dooglius wrote:
| In ring zero there are some, but no instructions that
| userspace can use (or else security would be impossible)
| secondcoming wrote:
| Indeed, for example the BSR instruction:
|
| > If the content source operand is 0, the content of the
| destination operand is undefined.
|
| [0] https://www.felixcloutier.com/x86/bsr
| dooglius wrote:
| No, this is not the same thing as "undefined behavior" in
| the sense the ISO C spec defines it. The content of the
| output being unspecified would not, for example, allow
| the processor to write to some part of memory if the
| source is zero, or change unrelated registers. x86 is
| doing "undefined" the right way, unlike the ISO C spec.
| kllrnohj wrote:
| It's exactly the same as "undefined behavior" in C. The
| only difference is in the consequences that follow from
| it when optimizers get ahold of it.
|
| If it's undefined behavior if BSR is given 0, then
| therefore the compiler can assume the value passed to BSR
| isn't 0 (because it's defined that a well-formed program
| doesn't encounter undefined behavior). Therefore a check
| if the value is == 0 can be skipped, because it can't be
| zero since BSR didn't allow it. And since the code inside
| the if isn't reachable, that probably means now other
| things are no longer reachable and can similarly be
| removed. And etc...
|
| That's all the "undefined behavior allows the compiler to
| murder my cat!" really is - it's just the chain of
| consequences that result from the compiler following the
| rules it was told. It was given a rule that a parameter
| couldn't be null, and so it listened & did what it was
| told including deleting redundant null checks (since
| after all, the value was already specified to be not
| null!).
|
| To demonstrate the problem in a different language, let's
| say I have this code: float
| asFloat(@Nullable Integer i) { return i ==
| null ? Float.NAN : i.floatValue(); }
|
| You'd expect the null check, right? But let's say I call
| it from this function: void
| sayHi(@NonNull Integer i) { println("Hello "
| + asFloat(i)); }
|
| The optimizer comes along and inlines the two together
| and now sees: void sayHi(@NonNull
| Integer i) { float temp = i == null ?
| Float.NAN : i.floatValue(); println("Hello "
| + temp); }
|
| Does it still need to keep the null check? After all, I
| defined that 'i' isn't null, so surely the null check is
| just unreachable code & can be eliminated, right? If it
| doesn't, it's obviously wasting performance. If it does,
| the internet gets angry about the compiler "abusing
| undefined behavior." Even though as far as the compiler
| is concerned it _isn 't_ undefined behavior! 'i' is
| defined to be not-null!
| dooglius wrote:
| What "compiler"? We're talking about the x86 instruction
| specification. The microcode translator certainly isn't
| allowed make the sort of optimization you're describing,
| that would be a violation of the spec that GP linked.
| gpderetta wrote:
| What would be, for example, the expected behaviour of
| accessing an out of scope variable?
| ErikCorry wrote:
| You will get whatever bits have been written at that
| location
| gpderetta wrote:
| What location? The variable might only have been
| allocated in a register.
| ErikCorry wrote:
| There's no register allocator in X86 assembly
| WalterBright wrote:
| I had an online discussion some years back where I suggested that
| C nail the size of char to 8 bits. He responded that there was a
| CPU that had chars be 32 bits, and wasn't that great that a C
| compiler for it would be Standard compliant?
|
| I replied by pointing out that nearly every non-trivial C program
| would have to be recoded to work on that architecture. So what
| purpose did the Standard allowing that actually achieve?
|
| I also see no problem for a vendor of a C compiler for that
| architecture making a reasonable _dialect_ of C for it. After
| all, to accommodate the memory architecture of the x86, nearly
| all C compilers in the 80 's adopted near/far pointers, and while
| not Standard compliant, it really didn't matter, and was
| tremendously successful.
|
| D made some decisions early on that worked out very well:
|
| 1. 2's complement wraparound arithmetic
|
| 2. sizes of basic integer types are fixed at 1 byte for chars, 2
| for shorts, 4 for integers, 8 for longs. This is worked out very
| well
|
| 3. floating point is IEEE
|
| 4. char's are UTF-8 code points
|
| 5. chars are unsigned
|
| These 5 points make for tremendous simplicity gains for D
| programmers, and ironically _increase_ portability of D code.
|
| After reading the paper, I'm inclined to change the definition of
| UB in D to _not_ mean it can be assumed to not happen and not be
| unintended.
| abfan1127 wrote:
| what's the history of having undefined behavior? why would a
| language designer not specify?
| rocqua wrote:
| Because the code should be somewhat portable, and not all
| processors act the same. This is why left and right shift are
| weird with overflow. Not consistent between x86 and some other
| instruction set. Similar for accessing the null address.
|
| Besides that, how would you specify the behavior of reading
| from an address the user randomly generated himself? What is
| stored at an address not generated by the compiler is out of
| scope for the compiler. If you try to define something like
| 'always trap' or 'always return 0', you incur massive overhead.
|
| Besides that. The fact that signed integer over/underflow is
| undefined behavior actually unlocks a lot of reasonable
| optimizations that treat integers like actual integers. Things
| like (a+1 > a) always being true.
| [deleted]
| gpderetta wrote:
| Sanitizers attempt to do define the behaviour of undefined
| constructs, but expect an integer multiple slowdown compared
| even with unoptimized builds.
|
| It is very hard to specify the behaviour of, for example, use-
| after-free, or accessing an automatic variable after it goes
| out of scope, or data races, in a C-like language without
| significant runtime cost.
| speedcoder wrote:
| Can you still compile gcc code with -O0 (gcc option to turn
| optimization off) to get completely defined behavior? When doing
| so does it actually still turn off all optimizations? Also does
| -Os (optimization for size) still produce defined behavior?
| foxfluff wrote:
| > Can you still compile gcc code with -O0 (gcc option to turn
| optimization off) to get completely defined behavior?
|
| No. The standard specifies what's undefined, optimization
| levels don't change it (though there are compiler flags such as
| -fwrapv which make undefined things defined).
|
| However, turning off optimizations will make behavior easier to
| predict.
| lanstin wrote:
| it used to be passed around never to use -O0 because it was
| much less used/tested and would generate wrong code more
| often. Not sure id that was folklore or true.
| RcouF1uZ4gsC wrote:
| I think the undefined behavior in C and C++ are even less
| defensible today than when they started because of the
| convergence of architectures.
|
| Pretty much every non-legacy architecture does IEEE floating
| point. Pretty much all of them do a flat address space. The word
| size is some power of 2(32 bit, 64 bit, maybe 128 bit in the
| future). They are almost always little endian. The memory models
| are converging towards the C++ memory model.
|
| Given that, I think simplifying the language and getting rid of
| foot guns could be done without losing any significant
| performance or actual flexibility/portability.
| pjmorris wrote:
| It could be called 'C--' unless that's been taken.
| RcouF1uZ4gsC wrote:
| There is a language C-- created by Microsoft Research (Simon
| Peyton Jones of Haskel and GHC fame)
|
| https://www.microsoft.com/en-us/research/wp-
| content/uploads/...
| jandrese wrote:
| You could also add that they all do 2s complement math.
|
| This is what the OpenBSD team did to OpenSSL. If the code has
| some complexity that is only necessary because it might have
| been run on a VAX or AIX or early Cray architecture then it is
| time to excise that complexity. They deleted thousands and
| thousands of lines of support for architectures that are only
| seen in museums and landfills today.
| maxlybbert wrote:
| It may be unusable for "modern" operating systems (you could
| write an early UNIX clone with just ISO C because early UNIX
| didn't support much).
|
| But that's basically always been the case. I doubt you could stay
| within the first ISO C standard and write a modern operating
| system.
| phicoh wrote:
| Assuming that an early Unix clone would be written in C plus
| some amount of assembler. Then what constructs of modern Unix
| systems make it impossible to in write in the first ISO C
| standard plus some assembler?
|
| For example, 4.4BSD, early Solaris have just about everything
| you would expect in a modern operating system. And give the age
| of those systems, they were written in early versions of the C
| standard.
| immibis wrote:
| This is true for any language. An operating system can be
| written in Java plus some amount of assembler.
| maxlybbert wrote:
| (Duplicating an answer to a similar question):
|
| The C standard has notes about a freestanding implementation,
| mainly for developing kernels. The requirements C has for
| freestanding environments are very limited ( http://www.open-
| std.org/jtc1/sc22/wg14/www/docs/n1570.pdf , you get float.h,
| iso646.h, limits.h, stdalign.h, stdarg.h, stdbool.h,
| stddef.h, stdint.h, and stdnoreturn.h) and the starting point
| for the program is implementation defined; you might get
| _Atomic, but that's optional). For the record, C++ goes a
| little overboard and promises all sorts of things in its
| freestanding environment.
|
| https://groups.google.com/g/comp.std.c/c/zzyii-
| DlMiU/m/RnH5i...
| ErikCorry wrote:
| Before 1985 there was no C standard, and until 1989 it was
| only a draft. So I doubt anything was written in standard C
| in the 1970s or early 1980s.
| phicoh wrote:
| C89 is mostly a superset of K&R C. So if you can write an
| OS in K&R you can write it is C89.
|
| The issue was, do modern operating systems require newer
| versions of the C standard? Or can you write everything in
| C89.
| ErikCorry wrote:
| K&R wasn't standardized either though.
| pjmlp wrote:
| Inline assembly is not part of C89, so a pure C89
| compiler needs to rely on an external Assembler for
| systems programming for example.
|
| Same applies to using C in hardware that lacked memory
| mapped IO.
| andyjohnson0 wrote:
| > I doubt you could stay within the first ISO C standard and
| write a modern operating system.
|
| Do you take that view because of some intrinsic deficiency in
| the language, or the difficulty of memory safety, or something
| else?
| maxlybbert wrote:
| The C standard has notes about a freestanding implementation,
| mainly for developing kernels. The requirements C has for
| freestanding environments are very limited ( http://www.open-
| std.org/jtc1/sc22/wg14/www/docs/n1570.pdf , you get float.h,
| iso646.h, limits.h, stdalign.h, stdarg.h, stdbool.h,
| stddef.h, stdint.h, and stdnoreturn.h) and the starting point
| for the program is implementation defined; you might get
| _Atomic, but that's optional). For the record, C++ goes a
| little overboard and promises all sorts of things in its
| freestanding environment.
|
| https://groups.google.com/g/comp.std.c/c/zzyii-
| DlMiU/m/RnH5i...
| phicoh wrote:
| One thing that is not mentioned in the article, is that next to
| undefined behavior, there is also implementation defined
| behavior.
|
| For example, if signed integer overflow would be implementation
| defined behavior, then any weirdness would be limited to just the
| integer operation that overflows.
|
| Lots of other stuff can be expressed as implementation defined
| behavior. That would probably kill some optimizations.
|
| So the question is more, do we want a portable assembler? In that
| case as many C constructs as possible need have defined behavior.
| Either defined by the standard or as part of the compiler
| documentation.
|
| Another possibily is to have standards for C on x86, amd64, arm,
| etc. Then we can strictly define signed integer overflow, etc.
| And say that on x86, pointers don't have alignment, so a pointer
| that points to storage of suitable size can be used to stored an
| object of different type, etc.
|
| If the goal is to run SPEC as fast as possible, then making sure
| every program trigger undefined behavior is the way to go.
| wruza wrote:
| I have a dumb question. Why can "we" write pretty good apps in
| languages other than C, but can't write operating systems? Is
| talking to hardware so much different than talking to APIs?
|
| Another point of view on the same question: Looking at software
| and hardware, the latter evolved insanely, but the former
| didn't get seemingly faster, at least in userlands. Why bother
| with UB-related optimizations at all for a wide spectrum of
| software? Is there even software which benefits from -O3 _and_
| doesn't use vectorization intrinsics? Why can't "we" just
| hardcode jpeg, etc for few platforms? Is that really easier to
| maintain opposed to maintaining never ending sources of UB?
|
| Iow, why e.g. my serial port or ata or network driver has to be
| implemented in C, if data mostly ends up in stream.on('data',
| callback) anyway?
| _3u10 wrote:
| Yes, C provides a way to talk to ABIs, in addition to APIs.
| It's not just "talking to hardware" it's talking to other
| software in a reliable way, such that you can upgrade your C
| compiler and have code written in C89 talk to code written in
| C11 which is unheard of in most of the other languages that
| don't support an ABI. (Think Python2 being incompatible with
| Python3)
|
| Software has gotten much faster. Yes, almost all software
| benefits from -O3. What do you mean "hardcode"? as far as I
| know libjpeg can be linked statically...
|
| UB is easy to maintain, lets take integer addition &
| overflow, you just issue an ADD instruction and however that
| CPU executes the ADD instruction is how integer overflow
| works on that platform and then in the C standard you write
| "integer overflow is undefined behavior".
| gpderetta wrote:
| A lot of C++ codebases benefit greatly by O3 due to the more
| aggressive inlining and interprocedural optimizations.
|
| Also may UB exploiting things like strict aliasing are
| enabled by default at all optimization levels in GCC.
| eatbitseveryday wrote:
| > Why can "we" write pretty good apps in languages other than
| C, but can't write operating systems? Is talking to hardware
| so much different than talking to APIs?
|
| Operating systems are written in other languages, such as C++
| and Rust.
|
| One requirement is that a language must be compiled and thus
| cannot rely on a runtime. That excludes Go and Java.
|
| The language needs to support direct memory manipulation.
|
| The compiled binary cannot be emitting system calls since
| that binary IS the kernel. Thus the compiler must be told to
| not link or include standard libraries.
|
| You need to disable certain optimizations like advanced
| vectorization extensions and red zone use on the stack.
|
| There are others. Lots of specific control needed.
| ArbixMenix wrote:
| >One requirement is that a language must be compiled and
| thus cannot rely on a runtime. That excludes Go and Java.
|
| Maybe I'm wrong, but I know that there exist CPUs made
| specifically to natively execute Java bytecode, so in
| reality if the hardware has a baked-in language
| interpretation it would be actually possible to write an OS
| completely in Java
| gpderetta wrote:
| running java bytecode natively is neither necessary nor
| sufficient as you can compile java to any other native
| ISA, but you do still a relatively heavy runtime for GC.
|
| Having said that, there have been OSs written in
| languages with heavy runtimes, even GC.
| pjc50 wrote:
| ARM "Jazelle" was capable of this, but it required a C
| implementation of a JVM. Any GC-dependant language has
| this problem.
| pjmlp wrote:
| Please find the C code here,
|
| https://people.inf.ethz.ch/wirth/ProjectOberon/Sources/Ke
| rne...
| bluGill wrote:
| True, you can design a CPU for anything. However a OS
| that depends on such a CPU is not portable to anything
| else, and can't easily run most programs that people
| depend on (emulators are possible, but not easy). Also
| most CPU advances haven't gone into such a thing and it
| is tricky to apply those advances while also providing
| what the language needs. None of this is impossible, but
| it makes such CPUs in todays world of questionable value.
|
| Note that you can port any OS written in C to such a CPU
| with "just" a new compiler backend and a few drivers.
| Your OS won't take advantage of the features the CPU
| provides, but it will work.
| 3836293648 wrote:
| Eh, can you really properly implement a CPU without
| interrupts? I wouldn't categorise anything in that space
| as a driver
| bluGill wrote:
| Good point. I assumed there was some form of interrupt
| system, but not all CPUs need to have it, and lacking
| that your OS choices will be limited.
| pjmlp wrote:
| > That excludes Go and Java.
|
| Only for those that cargo cult against using them.
|
| https://www.f-secure.com/en/consulting/foundry/usb-armory
|
| https://developer.arm.com/solutions/internet-of-
| things/langu...
|
| https://www.ptc.com/en/products/developer-tools/perc
|
| https://www.aicas.com/wp/products-services/jamaicavm/
| eatbitseveryday wrote:
| I was answering the question in a general sense for the
| more prolific operating systems and on generic commonly
| available general-purpose processors.
|
| Yes one can implement a CPU that natively executes a
| runtime for a high-level language, make your own ASIC, or
| FPGA, etc. that does this. That is a more advanced
| response to the general question.
|
| Knowing the detailed points I mentioned will help
| understand why specialization of processors is needed to
| support other higher-level languages that do not meet the
| requirements I laid out.
| pjmlp wrote:
| Which just proves your lack of knowledge that those
| runtimes target generic commonly available general-
| purpose processors.
|
| None of those products use FPGAs or custom ASICs.
| eatbitseveryday wrote:
| > just proves your lack of knowledge
|
| Tone is not needed.
|
| For TamaGo, it seems to allow developers run their
| application, not build an OS on the hardware. But I have
| not played with it, you are right.
|
| > TamaGo is a framework that enables compilation and
| execution of unencumbered Go applications on bare metal
|
| The environment does not seem to allow building a generic
| operating system [1]. F-Secure ported the runtime itself
| to boot natively. But please correct me.
|
| > There is no thread support
|
| The environment you run in is specifically curated for Go
| applications, such as the memory layout. I'd call this an
| "appliance" rather than enabling Go to be used for full-
| fledged generic operating system implementations.
|
| [1] https://github.com/f-secure-
| foundry/tamago/wiki/Internals
| pjmlp wrote:
| Tone follows the last paragraph, returning ball.
|
| An OS is an OS, regardless of the userspace.
| MauranKilom wrote:
| https://xkcd.com/801/
| matthewaveryusa wrote:
| This is why rust is so exciting: it's the first new
| language that's graduated from toy-language space we've
| seen in a while without a runtime. (python, ruby, go and
| typescript-nodejs are the other graduates I'm thinking
| about.)
| phicoh wrote:
| Let's assume you want write most of the operating system in
| the high level language and as little as possible in
| assembler.
|
| For most languages, writing hardware trap handler becomes
| quite a bit of an issue. In trap handler you cannot rely on
| an extensive runtime system. Anything that does garbage
| collection is probably out. Anything that does dynamic memory
| allocation is out as well.
|
| Beyond that, how easy is it to create pointers, create
| datastructures that match a specific memory layout, etc. Low
| level device drivers need to talk to hardware in very
| specific ways. If it is hard to talk to the hardware, most
| people are probably not going to bother using that language
| for an operating system.
|
| In theory you could mix and match languages in a kernel. For
| example, a filesystem could be written in a language that has
| an extensive runtime system.
| nine_k wrote:
| I'd say that Rust (and, to a smaller extent, Zig, and,
| AFAICT, Ada) allow to write code that is guaranteed to not
| allocate, and define the exact memory layout of certain
| structures, all while offering much tighter protections
| than C.
|
| Of course, there are things that cannot be expressed in
| safe code in either language. But marking fragments of code
| as unsafe, where C-like unconstrained access is allowed,
| helps a lot to minimize such areas and make them explicit.
|
| There is definitely room for a more expressive and safe
| languages in the kernel-level space. We can look at Google
| Fuchsia or maybe at Redox OS, both are very real operating
| systems trying to use safer languages, with some success.
| int_19h wrote:
| Ada still has plenty of UB; it's just that the cases
| where it arises are usually more explicit.
| zdragnar wrote:
| I think stability plays a big role in C continuing to
| remain dominant. Rust and Zig arent there yet, and wrt
| Rust in particular the ownership model doesn't play
| nearly as nicely in non-deterministic environments
| (taking far more code to deal with hardware such as an
| external display or printer that might, for example, get
| randomly unplugged at any point in time works against the
| grain of a static memory ownership analysis).
| nine_k wrote:
| I'd say it's a good example why more static checks like
| lifetimes are useful.
|
| With them, you can at least tell apart data structures
| that are fleeting and can disappear when a cable is
| ejected, and those which should stay put. This likely
| might help avoid another double-free or another freed
| pointer dereference.
| bell-cot wrote:
| If anything in the kernel is written in a language that has
| an extensive runtime system... Well, extensive runtime
| systems are pretty reliably resource hungry. And _when_
| they might suddenly need _which sorts_ of resources tends
| to be unpredictable.
|
| Vs. the kernel must keep working reliably when resources
| are running low.
| wruza wrote:
| But, today linux simply kills any process to free memory.
| What could prevent a gc (which also serves allocations,
| not only collects them back) to just do that on an
| emergency cycle? Destroy a record in a process array and
| reclaim its pages (of course without doing any
| allocations on the way, or by using an emergency pool).
| Or even just reclaim its pages and wait for it to crash
| on a page fault if you feel lazy.
|
| _which sorts_
|
| Dynamic languages only do memory-related ops
| unpredictably, or is it more than that?
| tyingq wrote:
| I suppose because there aren't many languages that allow you
| to manipulate arbitrary memory locations and cast portions of
| that to arbitrary types, and also allow relatively easy
| inline ASM. Which maybe isn't 100% necessary, but seems to be
| helpful at an OS level.
| rowls66 wrote:
| I think that the statement should be refined to say that it
| is not possible to develop a monolithic kernel based OS in
| ISO standard C. A monolithic kernel generally relies on
| passing pointers to memory between components with few
| restrictions. This can be problematic for some
| languages/compilers. However a microkernel OS that provides
| more structure around how data is shared between OS
| components can support development of many OS components in
| multiple languages. Even languages requiring significant
| runtimes like Java or C# could be used for many OS components
| like file systems, network stacks or device drivers.
|
| Historically, it has been difficult to beat monolithic
| kernels for performance and efficiency, and through
| significant effort, monolithic kernel based OS's exist that
| are reliable enough to be useful. However, the monolithic
| kernel is not the only OS architecture.
| adamc wrote:
| While maybe theoretically of interest, it is far afield of
| the pragmatic considerations that underlie the paper:
| worked on operating systems in common use TODAY.
| user-the-name wrote:
| > I have a dumb question. Why can "we" write pretty good apps
| in languages other than C, but can't write operating systems?
| Is talking to hardware so much different than talking to
| APIs?
|
| To some small extent, yes. But I don't think that is the main
| issue here.
|
| The real issue is that the stakes are much, much higher when
| implementing an operating system than when writing, say, an
| image editor. You can live with an occasional crash in a
| userland app. But the same crash in an operating system may
| open the door to taking over the entire computer, possibly
| even remotely.
| pjc50 wrote:
| There are some other candidates, as expressed below, but
| really the main problem is how difficult it is to write _and
| deploy_ an operating system that 's of usable capability.
| Even just hitting enough of POSIX to get a GUI and a browser
| up is a pretty huge amount of work.
|
| How many operating systems do we use that are less than 20
| years old?
| immibis wrote:
| It boils down to abstractions papering over ABI details.
|
| How do you write "put 0x12345678 to register 0x04000001" in
| assembler? mov eax, 0x04000001 / mov [eax], 0x12345678
|
| How do you write it in C-as-portable-assembler? You write
| _(u32_ )0x04000001 = 0x12345678;
|
| How do you write it in Java? You can't, the language has no
| such ability and if you try it's a syntax error. You have to
| call into a routine written in a lower-level language.
|
| How do you write it in C-as-abstract-machine? You can't, the
| language has no such ability and if you try it's undefined
| behaviour. You have to call into a routine written in a
| lower-level language.
|
| By the way, you can't write an operating system in C-as-
| portable-assembler either. No access to I/O port space, no
| way to define the headers for the bootloader, no way to
| execute instructions like LGDT and LIDT, no way to get the
| right function prologues and epilogues for interrupt handlers
| and system calls, no way to _invoke_ system calls. All those
| things are usually written in assembler. Writing operating
| systems in C has always been a lie. Conversely, you can
| extend the compiler to add support for those things and then
| you can write an operating system in extended-C!
| wruza wrote:
| This addresses a part of my question, which I didn't make
| clear, thanks! I mean after all this assembler stuff one
| could just use BASIC or similar. Yes, Java has no concept
| of PEEK/POKE, IN/OUT, but it just wasn't designed for that.
| Meanwhile, 1980s small systems were all assembly +
| basic/fortran. Of course they had no kernel in a modern
| sense, but all the devices were there: a speaker (SOUND), a
| serial line to a streamer/recorder (BLOAD), a graphics
| controller, no dma though, but it's just a controller with
| the same "ports" as well, which can r/w memory and generate
| interrupts. I don't get it why we don't just skip C to
| something high-level after wrapping all this
| pio/dma/irq/gdt/cr3 stuff into __cdecl/__stdcall format and
| then use ffi of a language which would decide to support
| that. I also don't understand GC arguments down the thread,
| because GC over malloc seems to be just a synthetic detail.
| You definitely can implement GC over a linear address
| space, just bump alloc it until the limit, or page-table
| however you want for dma. Malloc is not hardware, it isn't
| even a kernel thing. Apps run on mmap and brk, which are
| similar to what kernels have hardware-wise. Mmap is
| basically a thin layer over paging and/or dma.
|
| It was so easy and then blasted into something unbelievably
| complex in just few years. Maybe 80386 wasn't a good place
| to run typescript-over-asm kernel, but do we still have
| this limitation today?
| tenebrisalietum wrote:
| > Is talking to hardware so much different than talking to
| APIs?
|
| It depends. If your hardware is behind a bus or controller
| device that's serviced by a separate driver, then you're
| using APIs of that bus/controller driver.
|
| But think of having to talk to the TCP/IP stack using system
| calls - you are using an API but you'll still need to have
| some structure just beyond moving data back and forth over a
| bus. A USB mass storage driver is going to need different
| data moving over the USB interface than a USB network
| interface driver.
|
| Different buses work differently as well - USB device
| addressing is different than SATA or PCI-E device addressing.
|
| If you are really talking directly to a device, you're
| manipulating registers, bits, ports, etc. You may have to
| involve IRQs, etc. Your serial port, for example, can hold 16
| bytes before it generates an IRQ to tell the CPU it has data
| if it's a 16550 I think. Your SATA interface doesn't work
| like that, it can actually DMA data directly to memory. But
| both of these could be streamable devices to an operating
| system.
| rocqua wrote:
| I would guess that the big difference between an app and an
| OS is that the OS needs to do more complicated things with
| memory addresses.
|
| An app that runs has its own nicely mapped address space. And
| it interfaces with devices through system calls. An operating
| system has to keep the actual addresses of everything in
| mind, and it usually has to talk to devices through virtual
| addresses.
|
| As an example of what I think might be the problem. If the OS
| wants to read data from a device, it might allocate a buffer,
| wait for the device to write into that buffer, and then later
| read it. For the compiler, that is essentially "reading
| uninitialized memory" and thus undefined behavior.
| nine_k wrote:
| The example works because the compiler has no way to know
| that the programmer _intends_ the memory to be filled by
| e.g. a DMA transfer from a device.
|
| If a programmer could communicate this idea to the
| compiler, it would be somehow safer to write such code.
| There is a big difference between _intentionally_ reading
| what looks like initialized memory, and doing so by an
| oversight.
| rocqua wrote:
| It's not so much about 'intent'. The spec simply says
| this operation is undefined behavior. You could have a
| compiler that you could somehow inform "please just
| define this behavior as reading whatever is in memory
| there". But that supports the original point of the
| article, that plain ISO C is not suitable for OS
| programming.
| GoblinSlayer wrote:
| There are operating systems written in other languages than
| C.
|
| A driver doesn't need to be implemented in C, but the kernel
| API is likely written in C, your code needs to talk to it
| somehow. If your driver is written in C, it's as simple as
| #include.
| wruza wrote:
| Isn't this sort of circular? If very low-level, I mean
| in/out, lgdt, etc, were exposed as v8-bare-metal modules,
| it would be as simple as require() then. t
| = require("awesome-8253") p = require("pio-node")
| // cast your usual 0x42, 0x43, 0x61 spells
| ErikCorry wrote:
| In theory the difference between undefined behaviour and
| implementation defined behaviour is that ID behaviour must be
| documented. In practice good luck finding that documentation
| for each CPU and compiler combination. In fact good luck just
| finding it for LLVM and x64.
| josephcsible wrote:
| No, that's the difference between _unspecified_ and
| implementation defined behavior.
| ErikCorry wrote:
| If you want to be pedantic about it then I guess Clang and
| GCC don't implement the standards since they treat
| implementation defined as unspecified.
| astrobe_ wrote:
| This _unspecifed behavior_ is perhaps a bit lesser-known
| than UB, here is a (hopefully non-null ;-) pointer:
|
| https://stackoverflow.com/questions/18420753/unspecified-
| und...
| palotasb wrote:
| Undefined behavior entails _" there are no restrictions on
| the behavior of the program",_ meaning anything can happen,
| including executing the opposite of the program text.
| Implementation defined behavior is saner in the sense that
| program behavior is still defined. Examples of the latter are
| the exact type of std::size_t or the number of bits in a
| byte: https://en.cppreference.com/w/cpp/language/ub
| immibis wrote:
| The linked reference page does not say that implementation-
| defined behaviour must be sensible, only that it must be
| defined. Contrast with _unspecified behaviour_ where "
| _Each unspecified behavior results in one of a set of valid
| results._ "
|
| I expect that most instances of implementation-defined
| behaviour come with additional rules which state that the
| implementation has to define something sensible.
| bluecalm wrote:
| I don't think making it defined would help much. Overflowing a
| signed integer is a bug in logic. It would be ideal to have a
| crash on that. Continuing is going to be bad one way or another
| unless you luck out with your buggy code so the way the
| implementation works saves you. It can't be relied upon in
| general case though.
|
| Imo the way is to develop more tools that detect (either by
| analysis or at runtime) those bugs and run the code with those
| attached as often as you can afford it (to take the performance
| penalty).
| GoblinSlayer wrote:
| "Can't be relied upon in general case" is implementation
| defined behavior, not undefined. UB is much worse than what
| you think, it's not merely can't be relied, but the program
| can launch nuclear rockets when it happens. Preventing such
| interpretations is very helpful.
| bluecalm wrote:
| I meant that you can't rely on platform/implementation
| specific behavior to save you. It's the worst of both
| worlds: you don't get performance benefits of UB and you
| introduce a disaster waiting to happen once your code runs
| on another platform or is compiled with another compiler.
|
| I know what UB is. I think the idea is brilliant and saves
| millions of dollars of burnt coal every day. Sometimes
| security matters more and then you compile your code with
| every flag/sanitizer you can find to exchange performance
| for security.
| cle wrote:
| Overflowing a signed integer is not always a bug in logic, if
| you know the underlying representation then purposefully
| overflowing can be pretty useful.
| gpderetta wrote:
| The problem is that the vast majority of overflows are
| indeed logic errors. If the behaviour were defined, I
| wouldn't be able to use ubsan to catch them.
| MaxBarraclough wrote:
| > the vast majority of overflows are indeed logic errors
|
| This topic has turned up before. [0]
|
| I think C# gets this right. Ordinarily it handles integer
| overflow by throwing an exception, but it has an
| _unchecked_ keyword which gives you wrapping. [1] If you
| 're writing code that is expected to wrap, you use the
| _unchecked_ keyword, and you carry on using the usual
| arithmetic operators. (I believe you can also instruct
| the C# compiler to default to unchecked behaviour, so
| there 's also a _checked_ keyword. This strikes me as a
| mistake.)
|
| Strictly speaking Java gives you the option of checked vs
| unchecked integer arithmetic, but with terrible
| ergonomics: the '+' operator always silently wraps, if
| you want throw-on-overflow behaviour you have to call a
| method. [2] This is of course so unsightly that Java
| programmers tend to stick with the infix arithmetic
| operators regardless of the wrapping behaviour.
|
| C++ has templates and operator overloading, so you can
| use a library to get signed arithmetic to wrap, or throw,
| without undefined behaviour. [3] Such libraries are very
| rarely used, though.
|
| See also this very good blog post by John Regehr, who
| specialises in this kind of thing. [4] To quote the post:
|
| > _Java-style wrapping integers should never be the
| default, this is arguably even worse than C and C++'s UB-
| on-overflow which at least permits an implementation to
| trap._
|
| [0] https://news.ycombinator.com/item?id=26538606
|
| [1] https://docs.microsoft.com/en-
| us/dotnet/csharp/language-refe...
|
| [2] https://docs.oracle.com/en/java/javase/17/docs/api/ja
| va.base...
|
| [3] https://www.boost.org/doc/libs/1_78_0/libs/safe_numer
| ics/doc...
|
| [4] https://blog.regehr.org/archives/1401
| int_19h wrote:
| "unchecked" is the default in C#, and "checked" is opt-
| in, except for compile-time expressions (and
| System.Decimal, which always throws on overflow).
|
| You can tell the compiler to use "checked" by default for
| a given project, but it's fairly rare to see that in
| practice.
|
| I wish it was the other way around, but I guess they
| didn't consider the overhead of "checked" acceptable as a
| default back in 1999; and now it's a back-compat issue.
| pjmlp wrote:
| It is being discussed to change the defaults on .NET 7.
| phicoh wrote:
| That's the thing. When C is portable assembler, you expect
| signed integer overflow to be the same as in assembler. On
| x86 you don't expect a trap.
|
| There are quite a few idioms where overflow is used
| intentionally. There is no reason to turn that into undefined
| behavior if it works fine on the underlying platform.
| bluecalm wrote:
| I don't expect C to be portable assembler. I expect it to
| be a simple fast language with clear rules. I am not sure
| what you mean by an idiom here. I suppose you mean
| assembler idiom as in C it was always a simple bug in
| logic. Obviously you can't just use idioms from one
| language in another without checking what the rules in the
| other language are.
|
| Platform specific behavior should be as rare as possible.
| It's a recipe for bugs. The rule is simple enough and major
| compilers have flags to prevent the overflow. Obviously you
| pay the performance penalty for using them. It's a choice
| you're free to make as it should be.
| rini17 wrote:
| So you're fine with throwing your hands up "its unreliable,
| its a bug that should never happen" when standard lacks a
| clear recipe how to check beforehand whether signed integer
| arithmetic will overflow? Everyone rolls their own,
| introducing even more bugs.
| bluecalm wrote:
| Well, I prefer to have standard tools to check or a way to
| compile so it traps on the overflow. Majors compilers
| provide that if you value security over performance and are
| not sure about correctness of your logic.
| ErikCorry wrote:
| Similarly, standard C++ is not usable to implement virtual
| machines like Hotspot, and yet all virtual machines are
| implemented using C++ compilers.
___________________________________________________________________
(page generated 2022-01-21 23:01 UTC)