12

I have an assignment of expaining some seemingly strange behaviors of C code (running on x86). I can easily complete everything else but this one has really confused me.

Code snippet 1 outputs -2147483648

int a = 0x80000000;
int b = a / -1;
printf("%d\n", b);

Code snippet 2 outputs nothing, and gives a Floating point exception

int a = 0x80000000;
int b = -1;
int c = a / b;
printf("%d\n", c);

I well know the reason for the result of Code Snippet 1 (1 + ~INT_MIN == INT_MIN), but I can't quite understand how can integer division by -1 generate FPE, nor can I reproduce it on my Android phone (AArch64, GCC 7.2.0). Code 2 just output the same as Code 1 without any exceptions. Is it a hidden bug feature of x86 processor?

The assignment didn't tell anything else (including CPU architecture), but since the whole course is based on a desktop Linux distro, you can safely assume it's a modern x86.


Edit: I contacted my friend and he tested the code on Ubuntu 16.04 (Intel Kaby Lake, GCC 6.3.0). The result was consistent with whatever the assignment stated (Code 1 output the said thing and Code 2 crashed with FPE).

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
iBug
  • 35,554
  • 7
  • 89
  • 134
  • 7
    "I have an assignment of explaining some UBs of C" is a mistake. The only way to explain it is to say absolutely *anything* can happen. – paxdiablo Sep 23 '17 at 09:43
  • 1
    @paxdiablo Edited the question. It's not a UB but CPU architecture dependent behavior. – iBug Sep 23 '17 at 09:45
  • 1
    @iBug: The architecture should be mentioned in the question (which is then not about C, but about machine code obtained by some particular C compiler on some particular machine). On x86-64 integer overflows on Linux may give a `SIGFPE` – Basile Starynkevitch Sep 23 '17 at 09:46
  • 2
    I certainly won't be surprised if your first snippet is compiled to a negation rather than a division. It's a simple peephole optimization. – T.C. Sep 23 '17 at 09:59
  • @T.C. Your comment is the right answer. – iBug Sep 23 '17 at 10:09
  • You actually don't know the result in case 1, neither in case 2, as both lead you to **UNDEFINED BEHAVIOUR**, which means that you can know what your implementation does.... but you'll never know what **any** implementation which is standard compliant will give. – Luis Colorado Sep 25 '17 at 10:22
  • @ibug, that's not a architecture dependent behaviour. On signed integer overflow (that happens in both cases) the result of your expression is **UNDEFINED BEHAVIOUR** and not **implementation defined behaviour**. Read the standard, please. – Luis Colorado Sep 25 '17 at 10:24
  • Undefined behaviour is not a bug elsewhere, but in your code. If you commit to U.B. you are on your own risk. Don't bother about some bug elsewhere. Nobody is forced to implement some kind of U.B. in any coherent or definite way. That's what U.B. means. – Luis Colorado Sep 25 '17 at 12:00

5 Answers5

20

There are four things going on here:

  • gcc -O0 behaviour explains the difference between your two versions: idiv vs. neg. (While clang -O0 happens to compile them both with idiv). And why you get this even with compile-time-constant operands.

  • x86 idiv faulting behaviour vs. behaviour of the division instruction on ARM

  • If integer math results in a signal being delivered, POSIX require it to be SIGFPE: On which platforms does integer divide by zero trigger a floating point exception? But POSIX doesn't require trapping for any particular integer operation. (This is why it's allowed for x86 and ARM to be different).

    The Single Unix Specification defines SIGFPE as "Erroneous arithmetic operation". It's confusingly named after floating point, but in a normal system with the FPU in its default state, only integer math will raise it. On x86, only integer division. On MIPS, a compiler could use add instead of addu for signed math, so you could get traps on signed add overflow. (gcc uses addu even for signed, but an undefined-behaviour detector might use add.)

  • C Undefined Behaviour rules (signed overflow, and division specifically) which let gcc emit code which can trap in that case.


gcc with no options is the same as gcc -O0.

-O0 Reduce compilation time and make debugging produce the expected results. This is the default.

This explains the difference between your two versions:

Not only does gcc -O0 not try to optimize, it actively de-optimizes to make asm that independently implements each C statement within a function. This allows gdb's jump command to work safely, letting you jump to a different line within the function and act like you're really jumping around in the C source. Why does clang produce inefficient asm with -O0 (for this simple floating point sum)? explains more about how and why -O0 compiles the way it does.

It also can't assume anything about variable values between statements, because you can change variables with set b = 4. This is obviously catastrophically bad for performance, which is why -O0 code runs several times slower than normal code, and why optimizing for -O0 specifically is total nonsense. It also makes -O0 asm output really noisy and hard for a human to read, because of all the storing/reloading, and lack of even the most obvious optimizations.

int a = 0x80000000;
int b = -1;
  // debugger can stop here on a breakpoint and modify b.
int c = a / b;        // a and b have to be treated as runtime variables, not constants.
printf("%d\n", c);

I put your code inside functions on the Godbolt compiler explorer to get the asm for those statements.

To evaluate a/b, gcc -O0 has to emit code to reload a and b from memory, and not make any assumptions about their value.

But with int c = a / -1;, you can't change the -1 with a debugger, so gcc can and does implement that statement the same way it would implement int c = -a;, with an x86 neg eax or AArch64 neg w0, w0 instruction, surrounded by a load(a)/store(c). On ARM32, it's a rsb r3, r3, #0 (reverse-subtract: r3 = 0 - r3).

However, clang5.0 -O0 doesn't do that optimization. It still uses idiv for a / -1, so both versions will fault on x86 with clang. Why does gcc "optimize" at all? See Disable all optimization options in GCC. gcc always transforms through an internal representation, and -O0 is just the minimum amount of work needed to produce a binary. It doesn't have a "dumb and literal" mode that tries to make the asm as much like the source as possible.


x86 idiv vs. AArch64 sdiv:

x86-64:

    # int c = a / b  from x86_fault()
    mov     eax, DWORD PTR [rbp-4]
    cdq                                 # dividend sign-extended into edx:eax
    idiv    DWORD PTR [rbp-8]           # divisor from memory
    mov     DWORD PTR [rbp-12], eax     # store quotient

Unlike imul r32,r32, there's no 2-operand idiv that doesn't have a dividend upper-half input. Anyway, not that it matters; gcc is only using it with edx = copies of the sign bit in eax, so it's really doing a 32b / 32b => 32b quotient + remainder. As documented in Intel's manual, idiv raises #DE on:

  • divisor = 0
  • The signed result (quotient) is too large for the destination.

Overflow can easily happen if you use the full range of divisors, e.g. for int result = long long / int with a single 64b / 32b => 32b division. But gcc can't do that optimization because it's not allowed to make code that would fault instead of following the C integer promotion rules and doing a 64-bit division and then truncating to int. It also doesn't optimize even in cases where the divisor is known to be large enough that it couldn't #DE

When doing 32b / 32b division (with cdq), the only input that can overflow is INT_MIN / -1. The "correct" quotient is a 33-bit signed integer, i.e. positive 0x80000000 with a leading-zero sign bit to make it a positive 2's complement signed integer. Since this doesn't fit in eax, idiv raises a #DE exception. The kernel then delivers SIGFPE.

AArch64:

    # int c = a / b  from x86_fault()  (which doesn't fault on AArch64)
    ldr     w1, [sp, 12]
    ldr     w0, [sp, 8]          # 32-bit loads into 32-bit registers
    sdiv    w0, w1, w0           # 32 / 32 => 32 bit signed division
    str     w0, [sp, 4]

ARM hardware division instructions don't raise exceptions for divide by zero or for INT_MIN/-1 overflow. Nate Eldredge commented:

The full ARM architecture reference manual states that UDIV or SDIV, when dividing by zero, simply return zero as the result, "without any indication that the division by zero occurred" (C3.4.8 in the Armv8-A version). No exceptions and no flags - if you want to catch divide by zero, you have to write an explicit test. Likewise, signed divide of INT_MIN by -1 returns INT_MIN with no indication of the overflow.

AArch64 sdiv documentation doesn't mention any exceptions.

However, software implementations of integer division may raise: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka4061.html. (gcc uses a library call for division on ARM32 by default, unless you set a -mcpu that has HW division.)


C Undefined Behaviour.

As PSkocik explains, INT_MIN / -1 is undefined behaviour in C, like all signed integer overflow. This allows compilers to use hardware division instructions on machines like x86 without checking for that special case. If it had to not fault, unknown inputs would require run-time compare-and branch checks, and nobody wants C to require that.


More about the consequences of UB:

With optimization enabled, the compiler can assume that a and b still have their set values when a/b runs. It can then see the program has undefined behaviour, and thus can do whatever it wants. gcc chooses to produce INT_MIN like it would from -INT_MIN.

On a 2's complement system, the most-negative number is its own negative. This is a nasty corner-case for 2's complement, because it means abs(x) can still be negative. https://en.wikipedia.org/wiki/Two%27s_complement#Most_negative_number

int x86_fault() {
    int a = 0x80000000;
    int b = -1;
    int c = a / b;
    return c;
}

compile to this with gcc6.3 -O3 for x86-64

x86_fault:
    mov     eax, -2147483648
    ret

but clang5.0 -O3 compiles to (with no warning even with -Wall -Wextra`):

x86_fault:
    ret

Undefined Behaviour really is totally undefined. Compilers can do whatever they feel like, including returning whatever garbage was in eax on function entry, or loading a NULL pointer and an illegal instruction. e.g. with gcc6.3 -O3 for x86-64:

int *local_address(int a) {
    return &a;
}

local_address:
    xor     eax, eax     # return 0
    ret

void foo() {
    int *p = local_address(4);
    *p = 2;
}

 foo:
   mov     DWORD PTR ds:0, 0     # store immediate 0 into absolute address 0
   ud2                           # illegal instruction

Your case with -O0 didn't let the compilers see the UB at compile time, so you got the "expected" asm output.

See also What Every C Programmer Should Know About Undefined Behavior (the same LLVM blog post that Basile linked).

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 5
    Why was this downvoted? Did I make it too long and rambling? I didn't want to assume very much starting knowledge, but that made it into a long answer :/ – Peter Cordes Sep 23 '17 at 17:39
  • 2
    I checked the assembly output and immediately knew what was happening when I saw `a/-1;` was compiled into `negl %eax` while `a/b` was compiled to `cltd; idivl %ebx`. Thank you for your long explanation and patience! – iBug Sep 24 '17 at 01:09
  • 2
    The full ARM architecture reference manual states that UDIV or SDIV, when dividing by zero, simply return zero as the result, "without any indication that the division by zero occurred" (C3.4.8 in the Armv8-A version). No exceptions and no flags - if you want to catch divide by zero, you have to write an explicit test. Likewise, signed divide of `INT_MIN` by `-1` returns `INT_MIN` with no indication of the overflow. – Nate Eldredge Sep 21 '20 at 04:08
6

Signed int division in two's complement is undefined if:

  1. the divisor is zero, OR
  2. the dividend is INT_MIN (==0x80000000 if int is int32_t) and the divisor is -1 (in two's complement, -INT_MIN > INT_MAX, which causes integer overflow, which is undefined behavior in C)

(https://www.securecoding.cert.org recommends wrapping integer operations in functions that check for such edge cases)

Since you're invoking undefined behavior by breaking rule 2, anything can happen, and as it happens, this particular anything on your platform happens to be an FPE signal being generated by your processor.

Petr Skocik
  • 58,047
  • 6
  • 95
  • 142
2

With undefined behavior very bad things could happen, and sometimes they do happen.

Your question has no sense in C (read Lattner on UB). But you could get the assembler code (e.g. produced by gcc -O -fverbose-asm -S) and care about machine code behavior.

On x86-64 with Linux integer overflow (and also integer division by zero, IIRC) gives a SIGFPE signal. See signal(7)

BTW, on PowerPC integer division by zero is rumored to give -1 at the machine level (but some C compilers generate extra code to test that case).

The code in your question is undefined behavior in C. The generated assembler code has some defined behavior (depends upon the ISA and processor).

(the assignment is done to make you read more about UB, notably Lattner 's blog, which you should absolutely read)

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • 1
    Only `gcc -O0` explains the observed behaviour. Anything other than `-O0` will do constant propagation at compile time. But `-O0` has to make code that still implements the C source if you change the values of variables at runtime with a debugger. e.g. it can't optimize away the reload and x86 `idiv` in the 2nd version, because `int b = -1;` might not still be `-1` in the next statement. – Peter Cordes Sep 23 '17 at 11:43
  • 1
    Only integer overflow *from division* raises SIGFPE on x86-64 Linux. (Actually IDK what happens if you scatter [`into`](http://felixcloutier.com/x86/INT%20n:INTO:INT%203.html) instructions after signed addition / subtraction / whatever; the `#OF` exception might be handled the same as a `#DE` exception by the kernel.) But the compiler doesn't do that, so division is the only integer operation that ever traps in C on x86-64. – Peter Cordes Sep 23 '17 at 11:46
  • Wrote that up into an answer. – Peter Cordes Sep 23 '17 at 13:24
2

On x86 if you divide by actually using the idiv operation (which is not really necessary for constant arguments, not even for variables-known-to-be-constant, but it happened anyway), INT_MIN / -1 is one of the cases that results in #DE (divide error). It's really a special case of the quotient being out of range, in general that is possible because idiv divides an extra-wide dividend by the divisor, so many combinations cause overflow - but INT_MIN / -1 is the only case that isn't a div-by-0 that you can normally access from higher level languages since they typically do not expose the extra-wide-dividend capabilities.

Linux annoyingly maps the #DE to SIGFPE, which has probably confused everyone who dealt with it the first time.

harold
  • 61,398
  • 6
  • 86
  • 164
  • 2
    POSIX requires `SIGFPE` if there's a signal at all. :/ But on Linux / GNU systems, apparently you can distinguish between `FPE_INTDIV_TRAP` and actual FP causes: http://www.gnu.org/software/libc/manual/html_node/Program-Error-Signals.html. See my answer for an explanation of why `idiv` is necessary with `gcc -O0` for one but not both cases. – Peter Cordes Sep 23 '17 at 13:24
-2

Both cases are weird, as the first consists in dividing -2147483648 by -1 and should give 2147483648, and not the result you are receiving. The division by -1 (as the multiplication) should change the sign of the dividend to become a positive number. But there's no such positive number in int (this is what raises U.B.)

0x80000000 is not a valid int number in a 32 bit architecture (as stated in the standard) that represents numbers in two's complement. If you calculate its negative value, you'll get again to it, as it has no opposite number around zero. When you do arithmetic with signed integers, it works well for integer addition and substraction (always with care, as you are quite easy to overflow, when you add the largest value to some int) but you cannot safely use it to multiply or divide. So in this case, you are invoking Undefined Behaviour. You always invoke undefined behaviour (or implementation defined behaviour, which is similar, but not the same) on overflow with signed integers, as implementations vary widely in implementing that.

I'll try to explain what can be happening (with no trustness), as the compiler is free to do anything, or nothing at all.

Concretely, 0x80000000 as represented in two's complement is

1000_0000_0000_0000_0000_0000_0000

if we complement this number, we get (first complement all bits, then add one)

0111_1111_1111_1111_1111_1111_1111 + 1 =>
1000_0000_0000_0000_0000_0000_0000  !!!  the same original number.

suprisingly the same number.... You had an overflow (there's no counterpart positive value to this number, as we overflown when changing sign) then you take out the sign bit, masking with

1000_0000_0000_0000_0000_0000_0000 &
0111_1111_1111_1111_1111_1111_1111 =>
0000_0000_0000_0000_0000_0000_0000

which is the number you use as dividend.

But as I said before, this is what can be happening on your system, but not sure, as the standard says this is Undefined behaviour and, as so, you can get whatever different behaviour from your computer/compiler.

The different results you are obtaining are probably the result of the first operation being done by the compiler, while the second one is done by the program itself. In the first case you are assigning 0x8000_0000 to the variable, while in the second you are calculating the value in the program. Both cases are undefined behaviour and you are seeing it happening in front your eyes.

#NOTE 1

As the compiler is concerned, and the standard doesn't say anything about the valid ranges of int that must be implemented (the standard doesn't include normally 0x8000...000 in two's complement architectures) the correct behaviour of 0x800...000 in two's complement architectures should be, as it has the largest absolute value for an integer of that type, to give a result of 0 when dividing a number by it. But hardware implementations normally don't allow to divide by such a number (as many of them doesn't even implement signed integer division, but simulate it from unsigned division, so many simply extract the signs and do an unsigned division) That requires a check before division, and as the standard says Undefined behaviour, implementations are allowed to freely avoid such a check, and disallow dividing by that number. They simply select the integer range to go from 0x8000...001 to 0xffff...fff, and then from 0x000..0000 to 0x7fff...ffff, disallowing the value 0x8000...0000 as invalid.

Luis Colorado
  • 10,974
  • 1
  • 16
  • 31
  • *which is the number you use as divisor* - No, `0x80000000` was the dividend. Of course, in hardware the real dividend is 64-bit: `FFFFFFFF:80000000` in EDX:EAX, after the compiler sign-extends the dividend, because x86's only division instructions take a dividend twice as wide as the other input and output operands. So the full quotient is `00000001:00000000` which doesn't fit in a 32-bit register, thus #DE divide exception. But *not* divide by *zero*. At some point in your answer, you apparently got the dividend and divisor mixed up, and/or made up something re: "taking off the sign bit". – Peter Cordes Sep 21 '20 at 05:16
  • @PeterCordes, in 64bit machines, the size of `int` is 32bit, at least in linux and FreeBSD. You don't need to do a 64bit division to get that. As I stated in my answer, the standard normally doesn't recognize asymetrical ranges around zero, stating that `int` goes from -2^31-1 to 2^31-1, considering the use of -2^31 as Undefined Behaviour. What can be happening here is that in the first case it is the compiler that evaluates the expresion, while in the second case it is the runtime. – Luis Colorado Sep 21 '20 at 10:29
  • @PeterCordes, My apologies for exchanging the names for divisor and dividend, I always mistake them, but you are right, `a` is the dividend, the divisor is `-1` all the time. – Luis Colorado Sep 21 '20 at 10:30
  • `int` division in C produces an `int` result, therefore the compiler needs to use 32-bit operand-size for [x86 `idiv`](https://www.felixcloutier.com/x86/idiv). That size of `idiv` takes a 64-bit dividend in EDX:EAX. Yes, as I explained in my answer, the difference is runtime `idiv` vs. compile-time optimization to a simple `neg` (which is still the same signed-integer overflow but doesn't fault). You incorrectly explained the real reason for the fault, saying it had something to do with "then you take out the sign bit" and showing that the dividend becomes zero. But `0 / - 1` doesn't fault. – Peter Cordes Sep 21 '20 at 14:55
  • TL:DR: all the versions have signed-overflow UB in C. Any explanation hinges on looking at the asm to see exactly how it compiled with a specific compiler, and the ISA rules for those instructions. Remember that UB doesn't mean "required to fault", it means "anything can happen, including simple wrapping". The OP knows its UB and wanted an explanation of the symptoms on specific C implementations. – Peter Cordes Sep 21 '20 at 14:58
  • @PeterCordes, I'm afraid not. In the first example is mostly common that the compiler makes the calculation of the value (as it is a variable initializer in a constant expresion) so the assembler code already receives the calculated value. In the second example you can look whatever you like, but as U.B. has been triggered, each implementation can generate completely different and unrelated code. – Luis Colorado Sep 22 '20 at 06:05
  • That's exactly the point. This question is asking about the implementation details that resulted in different behaviour on each implementation for this UB. But note that the way the OP compiled (optimization disabled), the UB is *not* visible at compile time. With optimization disabled, constant propagation across statements is not done. This creates asm that gives consistent results even if you change `a` to some other value before the statement containing division. Read my answer, it explains all this. With optimization, both versions would be equivalent. – Peter Cordes Sep 22 '20 at 06:16
  • If you want to leave up this non-answer that goes into a lot of detail about things that barely relevant, that's up to you; after this last comment I'm done trying to convince you to delete it. The `-x` = `~x + 1` identity has nothing to do with anything. Fortunately the downvotes on this answer should prevent it from misleading future readers into thinking that `INT_MIN` as a dividend might be a problem in the general case (for other divisors). It's not; ISO C requires that `INT_MIN / -4` is well-defined, for example. Hypothetical C on HW with only unsigned division has to handler it. – Peter Cordes Sep 22 '20 at 06:23
  • I agree completely with you... there's no sense in this answer.... you are the only reference and the absolute truth. I didn't ask you for your comments, nor your last comment about your hint about erasing the answer... I think you are the only reference to be heard in this subject. Total authority to you. Thanks for your useful hints, and see on SO again! – Luis Colorado Sep 22 '20 at 16:56
  • @PeterCordes That's the most correct and straightforward answer. Thank you. – Skynight Mar 08 '21 at 01:12