UBSAN reports: -875 << 7 as undefined behaviour - c

Simple code snippet:
#define FOO 7
int bar = -875;
bar <<= FOO;
This is being reported by UBSAN as UB.
My understanding is that -875 << 7 is just -(875<<7)
and there is no overflow.
So, is there a real problem here?

Your understanding is incorrect.
Firstly you used bar <<= FOO syntax. This explicitly shifts bar and bar is negative. Left-shifting of negative values produces undefined behavior in C. There's no way bar <<= FOO can be interpreted as -(875<<7).
Secondly, concerning -875 << 7 in terms of operator priority: unary operators always have higher priority than binary ones, which means that -875 << 7 is (-875) << 7 and not -(875 << 7). And, again, left-shifting of negative values produces undefined behavior in C.

On a sign-magnitude machine, it is unclear what the effect of left-shifting a negative number should be, and it would not be unreasonable for such a machine to trap if such an operation was attempted. On such a machine, imposing any requirements on the behavior of a negative integer left-shift would likely have required compilers for such machines to generate extra code even in cases where the values to be shifted would always be positive. To avoid imposing such costs, the authors of the Standard declined to mandate any particular behavior.
Ones'-complement and two's-complement platforms would have no logical reason to trap when shifting a negative value (though whether -1<<1 should yield -2 or -3 on a ones'-complement machine would be unclear), but the authors of the Standard saw no reason to say that left-shifting of negative values has Undefined Behavior on platforms which use sign-magnitude integers, Implementation-Defined Behavior on platforms that use ones'-complement, and Standard-defined behavior on platforms which use two's-complement, since any two's-complement implementation would regard -1<<1 as yielding -2, whether or not the Standard mandated it, unless the author was deliberately obtuse.
Until probably 2005 or so, there would have been nothing even imaginably unsafe about code which will only be called upon to run on commonplace two's-complement machines using the left-shift operator on a negative value. Unfortunately, around that time an idea started to take hold which suggested that a compiler which avoids doing anything the Standard doesn't mandate can be more "efficient" than one which behaves usefully in situations not mandated by the Standard, and that such "efficiency" is desirable. I am not yet aware of compilers regarding the statement y=x<<1; as retroactively making the value of x non-negative, but I do not believe there is any reason to believe that they won't do so in future so unless or until some agency officially codifies the behavioral guarantees which mainstream microcomputer C compilers unanimously upheld for 25+ years such code cannot be considered "safe".

Related

What will be the output of the following C language statement -1<<1? [duplicate]

In C bitwise left shift operation invokes Undefined Behaviour when the left side operand has negative value.
Relevant quote from ISO C99 (6.5.7/4)
The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 × 2E2, reduced modulo
one more than the maximum value representable in the result type. If E1 has a signed
type and nonnegative value, and E1 × 2E2 is representable in the result type, then that is
the resulting value; otherwise, the behavior is undefined.
But in C++ the behaviour is well defined.
ISO C++-03 (5.8/2)
The value of E1 << E2 is E1 (interpreted as a bit pattern) left-shifted E2 bit positions; vacated bits are zero-filled. If E1 has an unsigned type, the value of the result is E1 multiplied by the quantity 2 raised to the power E2, reduced modulo ULONG_MAX+1 if E1 has type unsigned long, UINT_MAX+1 otherwise.
[Note: the constants ULONG_MAXand UINT_MAXare defined in the header ). ]
That means
int a = -1, b=2, c;
c= a << b ;
invokes Undefined Behaviour in C but the behaviour is well defined in C++.
What forced the ISO C++ committee to consider that behaviour well defined as opposed to the behaviour in C?
On the other hand the behaviour is implementation defined for bitwise right shift operation when the left operand is negative, right?
My question is why does left shift operation invoke Undefined Behaviour in C and why does right shift operator invoke just Implementation defined behaviour?
P.S : Please don't give answers like "It is undefined behaviour because the Standard says so". :P
The paragraph you copied is talking about unsigned types. The behavior is undefined in C++. From the last C++0x draft:
The value of E1 << E2 is E1
left-shifted E2 bit positions; vacated
bits are zero-filled. If E1 has an
unsigned type, the value of the result
is E1 × 2E2, reduced modulo one more
than the maximum value representable
in the result type. Otherwise, if E1
has a signed type and non-negative
value, and E1×2E2 is representable in
the result type, then that is the
resulting value; otherwise, the
behavior is undefined.
EDIT: got a look at C++98 paper. It just doesn't mention signed types at all. So it's still undefined behavior.
Right-shift negative is implementation defined, right. Why? In my opinion: It's easy to implementation-define because there is no truncation from the left issues. When you shift left you must say not only what's shifted from the right but also what happens with the rest of the bits e.g. with two's complement representation, which is another story.
In C bitwise left shift operation invokes Undefined Behaviour when the
left side operand has negative value.
[...]
But in C++ the behaviour is well defined.
[...] why [...]
The easy answer is: Becuase the standards say so.
A longer answer is: It has probably something to do with the fact that C and C++ both allow other representations for negative numbers besides 2's complement. Giving fewer guarantees on what's going to happen makes it possible to use the languages on other hardware including obscure and/or old machines.
For some reason, the C++ standardization committee felt like adding a little guarantee about how the bit representation changes. But since negative numbers still may be represented via 1's complement or sign+magnitude the resulting value possibilities still vary.
Assuming 16 bit ints, we'll have
-1 = 1111111111111111 // 2's complement
-1 = 1111111111111110 // 1's complement
-1 = 1000000000000001 // sign+magnitude
Shifted to the left by 3, we'll get
-8 = 1111111111111000 // 2's complement
-15 = 1111111111110000 // 1's complement
8 = 0000000000001000 // sign+magnitude
What forced the ISO C++ committee to consider that behaviour well
defined as opposed to the behaviour in C?
I guess they made this guarantee so that you can use << appropriately when you know what you're doing (ie when you're sure your machine uses 2's complement).
On the other hand the behaviour is implementation defined for bitwise
right shift operation when the left operand is negative, right?
I'd have to check the standard. But you may be right. A right shift without sign extension on a 2's complement machine isn't particularly useful. So, the current state is definitely better than requiring vacated bits to be zero-filled because it leaves room for machines that do a sign extensions -- even though it is not guaranteed.
To answer your real question as stated in the title: as for any operation on a signed type, this has undefined behavior if the result of the mathematical operation doesn't fit in the target type (under- or overflow). Signed integer types are designed like that.
For the left shift operation if the value is positive or 0, the definition of the operator as a multiplication with a power of 2 makes sense, so everything is ok, unless the result overflows, nothing surprising.
If the value is negative, you could have the same interpretation of multiplication with a power of 2, but if you just think in terms of bit shift, this would be perhaps surprising. Obviously the standards committee wanted to avoid such ambiguity.
My conclusion:
if you want to do real bit pattern
operations use unsigned types
if you want to multiply a
value (signed or not) by a power of two, do just
that, something like
i * (1u << k)
your compiler will transform this into decent assembler in any case.
A lot of these kind of things are a balance between what common CPUs can actually support in a single instruction and what's useful enough to expect compiler-writers to guarantee even if it takes extra instructions. Generally, a programmer using bit-shifting operators expects them to map to single instructions on CPUs with such instructions, so that's why there's undefined or implementation behaviour where CPUs had various handling of "edge" conditions, rather than mandating a behaviour and having the operation be unexpectedly slow. Keep in mind that the additional pre/post or handling instructions may be made even for the simpler use cases. undefined behaviour may have been necessary where some CPUs generated traps/exceptions/interrupts (as distinct from C++ try/catch type exceptions) or generally useless/inexplicable results, while if the set of CPUs considered by the Standards Committee at the time all provided at least some defined behaviour, then they could make the behaviour implementation defined.
My question is why does left shift operation invoke Undefined Behaviour in C and why does right shift operator invoke just Implementation defined behaviour?
The folks at LLVM speculate the shift operator has constraints because of the way the instruction is implemented on various platforms. From What Every C Programmer Should Know About Undefined Behavior #1/3:
... My guess is that this originated because the underlying shift operations on various CPUs do different things with this: for example, X86 truncates 32-bit shift amount to 5 bits (so a shift by 32-bits is the same as a shift by 0-bits), but PowerPC truncates 32-bit shift amounts to 6 bits (so a shift by 32 produces zero). Because of these hardware differences, the behavior is completely undefined by C...
Nate that the discussion was about shifting an amount greater than the register size. But its the closest I've found to explaining the shift constraints from an authority.
I think a second reason is the potential sign change on a 2's compliment machine. But I've never read it anywhere (no offense to #sellibitze (and I happen to agree with him)).
In C89, the behavior of left-shifting negative values was unambiguously defined on two's-complement platforms which did not use padding bits on signed and unsigned integer types. The value bits that signed and unsigned types had in common to be in the same places, and the only place the sign bit for a signed type could go was in the same place as the upper value bit for unsigned types, which in turn had to be to the left of everything else.
The C89 mandated behaviors were useful and sensible for two's-complement platforms without padding, at least in cases where treating them as multiplication would not cause overflow. The behavior may not have been optimal on other platforms, or on implementations that seek to reliably trap signed integer overflow. The authors of C99 probably wanted to allow implementations flexibility in cases where the C89 mandated behavior would have been less than ideal, but nothing in the rationale suggests an intention that quality implementations shouldn't continue to behave in the old fashion in cases where there was no compelling reason to do otherwise.
Unfortunately, even though there have never been any implementations of C99 that don't use two's-complement math, the authors of C11 declined to define the common-case (non-overflow) behavior; IIRC, the claim was that doing so would impede "optimization". Having the left-shift operator invoke Undefined Behavior when the left-hand operand is negative allows compilers to assume that the shift will only be reachable when the left-hand operand is non-negative.
I'm dubious as to how often such optimizations are genuinely useful, but the rarity of such usefulness actually weighs in favor of leaving the behavior undefined. If the only situations where two's-complement implementations wouldn't behave in commonplace fashion are those where the optimization would actually be useful, and if no such situations actually exist, then implementations would behave in commonplace fashion with or without a mandate, and there's no need to mandate the behavior.
The behavior in C++03 is the same as in C++11 and C99, you just need to look beyond the rule for left-shift.
Section 5p5 of the Standard says that:
If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined
The left-shift expressions which are specifically called out in C99 and C++11 as being undefined behavior, are the same ones that evaluate to a result outside the range of representable values.
In fact, the sentence about unsigned types using modular arithmetic is there specifically to avoid generating values outside the representable range, which would automatically be undefined behavior.
The result of shifting depends upon the numeric representation. Shifting behaves like multiplication only when numbers are represented as two's complement. But the problem is not exclusive to negative numbers. Consider a 4-bit signed number represented in excess-8 (aka offset binary). The number 1 is represented as 1+8 or
1001
If we left shift this as bits, we get
0010
which is the representation for -6. Similarly, -1 is represented as -1+8
0111
which becomes
1110
when left-shifted, the representation for +6. The bitwise behavior is well-defined, but the numeric behavior is highly dependent on the system of representation.

Implementation-defined behavior in C

Please, can you report some example for implementation-defined behavior in C?
For example, I know from standard that "An implementation-defined behavior is the propagation of the high-order bit
when a signed integer is shifted right."
Can you explain to me the significant of the example and report an example?
I understand that int i; i >> 3. But why this is implementation-defined?
The definition of implementation-defined behavior in C is when something is left for the compiler to decide, and the compiler documents which choice it made.
There are hundreds of such cases in the language. The standard contains a summary of most of them in Annex J.3, which is ~15 pages long.
The specific example int i; i >> 3 is undefined behavior since the variable isn't initialized.
The specific example int i=0; i >> 3 is implementation-defined because the standard says so. C17 6.5.7/5:
The result of E1 >> E2 is E1 right-shifted E2 bit positions. /--/ If E1 has a signed type and a negative value, the resulting value is implementation-defined.
In this particular case, it depends on whether the compiler picks an arithmetic shift or a logical shift instruction from the CPU instruction set. Meaning that the standard doesn't disfavour architectures that lack an arithmetic shift. Though in practice, the vast majority of CPUs are capable of doing arithmetic shift, even RISC ones.
It's implementation defined because the C standards committee declined to define what should happen. The reason why they did that is because different CPUs do different things in such circumstances, and the ethos of C (in the interests of fast execution) is to not to put too much abstraction between the C source code and the op codes running on the CPU.
They could have chosen to define a standard behaviour, but then on some CPUs that'd require compilers to generate a load of code to make up for the fact that the CPU's own op codes don't provide that standardised behaviour. Whilst perfectly possible, it inevitably wouldn't be very efficient.
Others will no doubt be able to point to more authoritative versions of that explanation!
Any code that relies on implementation defined behaviour is only guaranteed to work under a specific platform and/or compiler. Portable programs should try to avoid such behaviour.
According to: https://clc-wiki.net/wiki/C_language:Terms:Implementation-defined_behaviour
That also gives another example:
int *o = malloc(0 * sizeof *o);
may result in o either being NULL or a unique pointer (as specified in 7.20.3 of the C99 Standard).

Get warning for left shifting a negative number

I am trying to generate a warning for undefined behavior on left shifting a negative number. According to this answer, left shift of a negative number in C is undefined.
The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1×2E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and non-negative value, and E1×2E2 is representable in the result type, then that is the resulting value; otherwise, the behaviour is undefined.
I am trying to understand why I don't get a warning with this code:
x << 3
gcc -Wall (version 9.1.0)
int main ()
{
int x= -4, y, z=5;
y = z << x;
y = x << 3;
return y;
}
On a separate note, I also don't get warned about left shifting by a negative number
z << x
In 5 << -4, both GCC 9.1.0 and Apple LLVM 10.0.1 with clang-1001.0.46.4, targeting x86-64, issue a warning message (“left shift count is negative” for GCC and “shift count is negative” for LLVM-clang). In -4 << 3, GCC does not issue a warning, but LLVM-clang does (“shifting a negative value is undefined”).
The C standard does not require a diagnostic in either of these cases, so whether a compiler does or not is a quality of implementation issue (or, possibly, that the compiler extends C by defining left shifts of negative values and therefore does not consider it an error).
When an operand is not a constant, as in z << x and x << 3, we may surmise the compiler fails to see the operand is negative, so it does not issue a warning. In general, these expressions may have defined behavior: If x is not negative and is within suitable bounds (is not larger than the width of z in the former case and is not so large that x << 3 overflows in the latter case), the behavior is defined (excepting the possibility that z << x may overflow). The C standard does not say that left shifts with types that may have negative values, i.e., signed types, are undefined, just that left shifts with negative values are undefined. Therefore, it would be an error for the compiler to issue a warning message whenever x were a signed type in an expression such as z << x or x << 3.
Of course, given int x = -4 and the lack of any change to x between that initialization and the expressions z << x and x << 3, a compiler could deduce that the shifts are undefined in this specific case and issue a warning. This is not required by the standard, and the failure of the compiler to do so is simply a matter of the quality of the implementation of the compiler.
The short explanation for the compiler not issuing a warning in such cases is because it isn't required to.
The definition of "undefined" in the standard also explicitly states "no diagnostic required". Which means the standard does not require an implementation to issue a diagnostic in such cases. The reason for that is that, technically, compilers may not be able to detect all instances of undefined behaviour within a reasonable time or (in some cases) at all. Some cases can only be detected at run time. Compilers are complicated pieces of code so - even if a human can easily recognise a problem - a compiler may not (on the other hand, compilers also detect problems that humans cannot find easily).
When no diagnostics are required, there are several things - all discretionary - that must happen before a compiler will issue a warning.
The first things that happen amount to "quality of implementation" concerns - a vendor or compiler developer elects to write code that detects particular cases and other code that issues a warning
The two steps (detecting a case, and warning about it) are separate and completely discretionary - the standard does not require a diagnostic so, even if code is written that detects a particular case, the compiler is still not required to issue a warning about it. Practically, even if problems are detected, the compiler developers may elect not to issue a warning for various reasons. Some warnings are about rare edge cases, so it is not worth the effort to emit them. Some warnings have an inherent "false positive" rate which tends to result in developers whinging in bug reports to the compiler vendor about unnecessary warnings - so the compiler is configured by default to not emit them.
The next requirement for a warning to be issued is that the user of the compiler must elect to have the warnings shown. Thanks to vehement lobbying of vendors by developers - most modern compilers are configured BY DEFAULT so they only issue a relatively small number of warnings. So developers who want as much help as possible from the compiler need to explicitly enable it. For example, by using -Wall options with gcc and clang.
The behavior of left-shifting any value by an amount smaller than the word size was specified in C89 for all integer values of the left operand, even though the specified behavior for negative left operands would often have been less than ideal on non-two's-complement complement platforms, or on error-trapping platforms in cases where iterated multiplication by two would overflow.
To avoid requiring implementations to behave in less-than-ideal fashion, the authors of C99 decided to allow implementations to process such cases however they saw fit. Rather than try to guess at all the places where implementations might have a good reason to deviate from the C89 behavior, the authors of the C99 Standard presumably expected that implementations would follow the C89 behavior, absent a good reason to otherwise, whether the Standard compelled them to do so or not.
If the authors of the Standard had intended any change to the processing of left-shift operations in cases where the C89 behavior made sense, there should be some mention of the change in the published Rationale document. There is none whatsoever. The only inference I can see from such omission is that going from "Behave in a particular fashion that happens to almost always make sense" to "Behave in that fashion if the implementer judges that it makes sense, and otherwise process the code in whatever other fashion the implementer sees fit" wasn't seen as a sufficient change as to justify comment. The fact that the authors of the Standard failed to mandate a behavior in no way implies that they didn't expect that general-purpose two's-complement implementations wouldn't continue to process such shifts as they always had, nor that programmers shouldn't be entitled to similar expectations.

What replacements are available for formerly-widely-supported behaviors not defined by C standard

In the early days of C prior to standardization, implementations had a variety of ways of handling exceptional and semi-exceptional cases of various actions. Some of them would trigger traps which could cause random code execution if not configured first. Because the behavior of such traps was outside the scope of the C standard (and may in some cases be controlled by an operating system outside the control of the running program), and to avoid requiring that compilers not allow code which had been relying upon such traps to keep on doing so, the behavior of actions that could cause such traps was left entirely up to the discretion of the compiler/platform.
By the end of the 1990s, although not required to do so by the C standard, every mainstream compiler had adopted common behaviors for many of these situations; using such behaviors would allow improvements with respect to code speed, size, and readability.
Since the "obvious" ways of requesting the following operations are no longer supported, how should one go about replacing them in such a way as to not impede readability nor adversely affect code generation when using older compilers? For purposes of descriptions, assume int is 32-bit, ui is a unsigned int, si is signed int, and b is unsigned char.
Given ui and b, compute ui << b for b==0..31, or a value which may arbitrarily behave as ui << (b & 31) or zero for values 32..255. Note that if the left-hand operand is zero whenever the right-hand operand exceeds 31, both behaviors will be identical.
For code that only needs to run on a processor that yields zero when right-shifting or left-shifting by an amount from 32 to 255, compute ui << b for b==0..31 and 0 for b==32..255. While a compiler might be able to optimize out conditional logic designed to skip the shift for values 32..255 (so code would simply perform the shift that will yield the correct behavior), I don't know any way to formulate such conditional logic that would guarantee the compiler won't generate needless code for it.
As with 1 and 2, but for right shifts.
Given si and b such that b0..30 and si*(1<<b) would not overflow, compute si*(1<<b). Note that use of the multiplication operator would grossly impair performance on many older compilers, but if the purpose of the shift is to scale a signed value, casting to unsigned in cases where the operand would remain negative throughout shifting feels wrong.
Given various integer values, perform additions, subtractions, multiplications, and shifts, such fashion that if there are no overflows the results will be correct, and if there are overflows the code will either produce values whose upper bits behave in non-trapping and non-UB but otherwise indeterminate fashion or will trap in recognizable platform-defined fashion (and on platforms which don't support traps, would simply yield indeterminate value).
Given a pointer to an allocated region and some pointers to things within it, use realloc to change the allocation size and adjust the aforementioned pointers to match, while avoiding extra work in cases where realloc returns the original block. Not necessarily possible on all platforms, but 1990s mainstream platforms would all allow code to determine if realloc caused things to move, and determine the what the offset of a pointer into a dead object used to be by subtracting the former base address of that object (note that the adjustment would need to be done by computing the offset associated with each dead pointer, and then adding it the new pointer, rather than by trying to compute the "difference" between old and new pointers--something that would legitimately fail on many segmented architectures).
Do "hyper-modern" compilers provide any good replacements for the above which would not degrade at least one of code size, speed, or readability, while offering no improvements in any of the others? From what I can tell, not only could 99% of compilers throughout the 1990s do all of the above, but for each example one would have been able to write the code the same way on nearly all of them. A few compilers might have tried to optimize left-shifts and right-shifts with an unguarded jump table, but that's the only case I can think of where a 1990s compiler for a 1990s platform would have any problem with the "obvious" way of coding any of the above. If that hyper-modern compilers have ceased to support the classic forms, what do they offer as replacements?
Modern Standard C is specified in such a way that it can be guaranteed to be portable if and only if you write your code with no more expectations about the underlying hardware it will run on than are given by the C abstract machine the standard implicitly and explicitly describes.
You can still write for a specific compiler that has specific behaviour at a given optimization level for a given target CPU and architecture, but then do not expect any other compiler (modern or otherwise, or even a minor revision of the one you wrote for) to go out of its way to try to intuit your expectations if your code violates conditions where the Standard says that it is unreasonable to expect any well defined implementation agnostic behaviour.
Two general principles apply to standard C and standard C++:
Behavior with unsigned numbers is usually better defined than behavior with signed numbers.
Treat optimization as the quality-of-implementation issue that it is. This means that if you're worried about micro-optimization of a particular inner loop, you should read the assembly output of your compiler (such as with gcc -S), and if you discover that it fails to optimize well-defined behavior to appropriate machine instructions, file a defect report with the publisher of your compiler. (This doesn't work, however, when the publisher of the only practical compiler targeting a particular platform isn't very interested in optimization, such as cc65 targeting MOS 6502.)
From these principles, you can usually derive a well-defined way to achieve the same result, and then apply the trust-but-verify principle to the quality of generated code. For example, make shifting functions with well-defined behavior, and let the optimizer remove any unneeded checks that the architecture itself guarantees.
// Performs 2 for unsigned numbers. Also works for signed
// numbers due to rule for casting between signed and unsigned
// integer types.
inline uint32_t lsl32(uint32_t ui, unsigned int b) {
if (b >= 32) return 0;
return ui << b;
}
// Performs 3 for unsigned numbers.
inline uint32_t lsr32(uint32_t ui, unsigned int b) {
if (b >= 32) return 0;
return ui >> b;
}
// Performs 3 for signed numbers.
inline int32_t asr32(int32_t si, unsigned int b) {
if (si >= 0) return lsr32(si, b);
if (b >= 31) return -1;
return ~(~(uint32)si >> b);
}
For 4 and 5, cast to unsigned, do the math, and cast back to signed. This produces non-trapping well-defined behavior.

Integer overflow and undefined behavior

There are many questions about detection of the integer overflow BEFORE the actual addition/substraction because of possible undefined behavior. So, my question is
Why it will produce this undefined behavior in the first place?
I can think of 2 causes:
1) A processor that generates exception in this case. Sure, it can be toggled off, and most probably a well written CRT will do that.
2) A processor that uses other binary representations of numbers (1's complement? base 10?). In that case the undefined behavior will manifest itself as different result (but will not crash!). Well, we could live with that.
So, why should someone avoid causing it? Am I missing something?
While the historical reason signed overflow was specified as undefined behavior was probably these bogus legacy representations (ones complement/sign-magnitude) and overflow interrupts, the modern reason for it to remain undefined behavior is optimization. As J-16 SDiZ hinted at, the fact that signed overflow is undefined behavior allows the compiler to optimize out some conditionals whose algebraic truth (but not necessarily representation-level truth) are already established by a previous branch. It may also allow the compiler to algebraically simplify some expressions (especially those involving multiplication or division) in ways that could give different results than the originally-written order of evaluation if a subexpression contains an overflow, since the compiler is allowed to assume that overflow does not happen with the operands you've given it.
The other huge example of undefined behavior for the purpose of permitting optimization is the aliasing rules.
Although most modern CPUs use 2's complement, and integer overflow results in predictable modulo wraparound, this is by no means universal - to keep the language sufficiently general that it can be used on the widest range of architectures it's better to specify that integer overflow is UB.
The undefined behavior bits in the specification involve some compiler optimization. For example:
if (a > 0 && b > 0) {
if ( a + b <= 0 ) {
// this branch may be optimized out by compiler
} else {
// this branch will always run
}
}
Modern C compilers are not that simple, it do lots of guessing and optimization.
I think your assumption 1) that this can be switched off for any given processor has been false on at least one important historical architecture, the CDC, if my memory is correct.

Resources