12

The situation:
I have a piece of code that works when compiled for 32-bits but fails when compiled for 64-bits with gcc 4.6. After identifying the problem and reading up on the standards, I cannot really understand why it is working for 32-bits. I hope someone can explain what is going on.

The code (somewhat simplified and cut down to the interesting parts):

// tbl: unsigned short *, can be indexed with positive and negative values
// v: unsigned int
// p: unsigned char *
tmp = tbl[(v >> 8) - p[0]]; // Gives segfault when not compiled with -m32

When compiled with -m32 the code works. When compiled without -m32 it gives a segfault. The reason for the segfault is that (v >> 8) - p[0] is interpreted as an unsigned int when compiled for 64-bit which for "negative" results will be way off.

According to this question, the C99 standard says the following:
6.2.5c9: A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.

From this it seems that unsigned minus unsigned will always result in unsigned output which is consistent with what happens in the 64-bit case. This does not seem to happen in the 32-bit case which is what I find extremely strange.

Can anyone explain what is happening in the 32-bit case?

Community
  • 1
  • 1
Leo
  • 2,328
  • 2
  • 21
  • 41
  • 2
    It takes an ISO committee to call that "never overflow". – Hans Passant Sep 05 '12 at 14:16
  • 2
    @HansPassant: Anyone, whether they are an ISO committee or not, should use technical terms precisely when writing standards documents. The technical definition of overflow is that the result of a computation has a magnitude too large to be represented in the chosen type. Integer overflow in C is undefined behavior, but unsigned integers can't overflow -- this is a very important clarification, without which some people might think that unsigned arithmetic could e.g. raise a signal. Signed overflow *does* raise signals (crash) on certain platforms (MIPS). – Dietrich Epp Sep 05 '12 at 14:27

1 Answers1

14

In both cases you get a very large number because the unsigned int wraps around, but in the 32-bit case, the pointer arithmetic also wraps around, and so it cancels out.

In order to do the pointer arithmetic, the compiler promotes the array index to the same width as the pointer. So for unsigned with 32-bit pointers, you get the same result as int with 32-bit pointers.

For example,

char *p = (char *) 0x1000;

// always points to 0x0c00
// in 32-bit, the index is 0xfffffc00
// in 64-bit, the index is 0xfffffffffffffc00
int r = p[(int) -0x400]; 

// depends on architecture
// in 32-bit, the index is 0xfffffc00 (same as int)
// in 64-bit, the index is 0x00000000fffffc00 (different from int)
int r = p[(unsigned) -0x400];
Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
  • Actually, the array index is (effectively) promoted to `ptrdiff_t`, which is *usually* of the same width as a data pointer. See 6.5.6:8, :9. – ecatmur Sep 05 '12 at 14:04
  • 1
    @ecatmur: I've read that section of n1256 and nowhere does it mention `ptrdiff_t`. Bonus challenge: can you name a 32-bit or 64-bit architecture where `sizeof(ptrdiff_t) != sizeof(void *)`? – Dietrich Epp Sep 05 '12 at 14:11
  • Good point; I assumed that was implied by the result type of pointer subtraction (6.5.6:9) but it actually isn't. Bonus challenge - no. – ecatmur Sep 05 '12 at 14:27
  • Great answer! Definitely didn't think about the pointer arithmetic wrapping around. – Leo Sep 05 '12 at 19:32