In embedded environments you're constrained by toolchain and platform but it's still a bad idea to rely on any behaviour which your compiler doesn't provide a definition for (which might be more behaviour than what your standard provides a definition for) because changes to the version of the compiler or even changes to surrounding code can trigger issues caused by reliance on UB.
It's not actually that hard to write embedded code which does not invoke UB outside of register access and even there it's possible to limit yourself to invoking behaviours which the combination of hardware + compiler does provide documented behaviour for.
(source: I've written embedded code which did not knowingly/intentionally invoke UB outside of register access and in those cases the implementation did define behaviour.)
1. Masochism
2. Underspecification, in a vain attempt to make a language that can theoretically be used on PDP computers.
3. The fact that an inappropriate write through a pointer results in behavior that is so undefined that it can lead to remote code execution and hence do literally anything.
No amount of additional specification can fix #3, and masochism cannot explain it.
One could mitigate #3 to some extent with techniques like control flow integrity or running in a strongly sandboxed environment.
This is a strange way to look at it. You'd get remote code execution only if the result of writing through the pointer was exactly what you'd expect: that the value you tried to write was copied into the memory indexed by the pointer.
I'm talking more about the nonsense like "c++ + ++c". There's no reason but masochism to keep it undefined. Just pick one unambiguous option and codify it.
An example of #2 is stuff like signed overflow. There are only so many ways to handle it: wraparound, saturate, error out. So C should just document them and provide a way to detect which behavior is active (like it does with endianness).
> I'm talking more about the nonsense like "c++ + ++c". There's no reason but masochism to keep it undefined. Just pick one unambiguous option and codify it.
It's because there's an underlying variance in what the compilers (and the hardware [1]) translated for expressions like that, and codifying any option would have broken several of them, which was anathema in the days of ANSI C standardization. (It's still pretty frowned upon, but "get one person to change behavior so that everybody gets a consistent standard" is something the committees are more willing to countenance nowadays).
> An example of #2 is stuff like signed overflow. There are only so many ways to handle it: wraparound, saturate, error out.
Funnily enough, none of the ways you mention turn out to be the way it's actually implemented in the compiler nowadays.
As for why UB actually exists, there are several reasons. Sometimes, it's essential because the underlying behavior is impossible to rationally specify (e.g., errant pointer dereferences, traps). Sometimes, it's because you have optimization hints where you don't want to constrain violation of those hints (e.g., restrict, noreturn). Sometimes, it's erroneous behavior that's hard to consistently diagnose (e.g., signed overflow). Sometimes, it's for explicit implementation-defined behavior, but for various reasons, the standard authors didn't think it could be implemented as unspecified or implementation-defined behavior.
[1] Remember, this is the days of CISC, and not the x86 only-very-barely-not-RISC kind of CISC, the heady days of CISC where things like "*p++ = --q" is a single instruction.