The difference between checked and unchecked here is actually a bit of a bug in the IL, or just some bad source code (I'm not a language expert so I will not comment on if the C# compiler is generating the correct IL for the ambigious source code). I compiled this test code using the 4.0.30319.1 version of the C# compiler (although the 2.0 verision seemed to do the same thing). The command line options I used were: /o+ /unsafe /debug:pdbonly.
For the unchecked block, we have this IL code:
//000008: unchecked
//000009: {
//000010: Console.WriteLine("{0:x}", (long)(testPtr + offset));
IL_000a: ldstr "{0:x}"
IL_000f: ldloc.0
IL_0010: ldloc.1
IL_0011: add
IL_0012: conv.u8
IL_0013: box [mscorlib]System.Int64
IL_0018: call void [mscorlib]System.Console::WriteLine(string,
object)
At IL offset 11, the add gets 2 operands, one of type byte* and the other of type uint32. Per the CLI spec these are really normalized into native int and int32, respectively. According to the CLI spec (partition III to be precise), the result will be native int. Thus the secodn operand must be promoted to be of type native int. According to the spec, this is accomplished via a sign extension. So the uint.MaxValue (which is 0xFFFFFFFF or -1 in signed notation) is sign extened to 0xFFFFFFFFFFFFFFFF. Then the 2 operands are added (0x0000000008000000L + (-1L) = 0x0000000007FFFFFFL). The conv opcode is only needed for verification purposes to convert the native int into an int64, which in the generated code is a nop.
Now for the checked block, we have this IL:
//000012: checked
//000013: {
//000014: Console.WriteLine("{0:x}", (long)(testPtr + offset));
IL_001d: ldstr "{0:x}"
IL_0022: ldloc.0
IL_0023: ldloc.1
IL_0024: add.ovf.un
IL_0025: conv.ovf.i8.un
IL_0026: box [mscorlib]System.Int64
IL_002b: call void [mscorlib]System.Console::WriteLine(string,
object)
It is virtually identical, except for the add and conv opcode. For the add opcode we've added 2 'suffixes'. The first one is the ".ovf" suffix which has an obvious meaning: check for overflow, but it is also required to 'enable the second suffix: ".un". (i.e. there is no "add.un", only "add.ovf.un"). The ".un" has 2 effects. The most obvious one is that the additiona nd overflow checking are done as if the operands were unsigned integers. From our CS classes way back when, hopefully we all remember that thanks to two's complement binary encoding, signed addition and unsigned addition are the same, so the ".un" really only impacts the overflow checking, right?
Wrong.
Remember that on the IL stack we don't have 2 64-bit numbers, we have an int32 and a native int (after normalization). Well the ".un" means that the conversion from int32 to native is treated like a "conv.u" rather than the default "conv.i" as above. Thus uint.MaxValue is zero extended to 0x00000000FFFFFFFFL. Then the add correctly produces 0x0000000107FFFFFFL. The conv opcode makes sure the unsigned operand can be represented as a signed int64 (which it can).
Your fix works just find for 64-bit. At the IL level a more correct fix would be to explicitly convert the uint32 operand to native int or unsigned native int, and then both the check and unchecked would bhave identically for both 32-bit and 64-bit.