Could somebody provide me with an example of truly undefinable behavior? (Perhaps data races?)
Memory corruption is pretty undefinable. Imagine writing data through a wild pointer. The program might segfault immediately, or compute a wrong result but not crash, or crash at an arbitrary later point, or delete all your files, or...
It makes no sense to try to enumerate all that might happen, so this cannot really be implementation defined (where the implementation must document what will happen).
Then a particular ABI could say "if your pointer overflows (by a our system specific meaning of overlow) then you have a bad pointer". Merely doing a pointer increment just gets you implementation-defined behaviour. The UB only happens if you plough on.
Such a setup wouldn't do much to save us from pointer bugs. But it would make optimizers more predictable.
I could be wrong though, after all compilers back then were a lot less clever than now so maybe they couldn't really make use of this information. I'd be curious to get a first hand testimony as to why certain UB exist in the first place.
No compiler vendor back then wanted to give up on their own specific deviations, given their existing customer base, proprietary OSes and extensions.
So many of those vendor specific features got tucked away into implementation specific and UB buckets.
So from the point of view of a C compiler that exploits undefined signed overflow, if your code could overflow, it was probably already buggy, and you should have fixed it.
(One problem with this line of reasoning is that it kind of also applies to unsigned numbers, but there C goes the other way.)
Then there's the matter if ((char )0) causes a trap or not; on systems without a MMU, it probably won't trap [1]. It's differences like overflow and memory references that cause most of the UB you see in C.
[1] On Linux, you can map a page to address 0. But that you can, doesn't mean you should.
C code defines run-time behavior (or fails to define it in some cases), not CPU instruction sequences. If you want assembly language, you know where to find it.
I believe the intention was whichever one matched the types. If you're adding 2 16-bit ints, it would use the 16-bit add instruction, if it existed or a larger one (then, in this case, cast it back down to 16-bits when appropriate). If it's a floating-point type, it would get a floating point add.
[Edit: The compilers usually wouldn't provide a 16-bit int type if they didn't have the instructions to deal with it.]
> What if the operands are of different types?
C89 has well defined promotion rules. You'd convert the operands appropriately, then use the equivalent add.
> What if the CPU doesn't have an "add" instruction?
Then it would probably end up as a function call to a system specific function. Either way, it wouldn't do anything particularly unexpected for the system.
> If you want assembly language, you know where to find it.
C was originally just used as a high-level assembly language. I suspect people found it easier to reason about, even if it basically used the system instruction's behaviour.
One somewhat unfortunate side effect of the pervasiveness of C and Unix-style machines is that it encouraged CPUs to optimize for C rather than the opposite way around. The use of GPUs for non-graphics programming is a welcome improvement.