Monday, January 25, 2010

Divided and Confused

Odd discovery of the day. Execute the following on a system running a 32-bit version of Windows (NOT a Win64 system!):

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils;

begin
  try
    MSecsToTimeStamp(-1);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
It should print out the following:

EIntOverflow: Integer overflow

Now run the exact same (32bit) binary on a Win64 system, this is what you’ll get:

EDivByZero: Division by zero

Give it a try. Weird, no? So I set out to figure out what is going on. MSecsToTimeStamp() is implemented down in SysUtils and is implemented in assembler code:

function MSecsToTimeStamp(MSecs: Comp): TTimeStamp;
asm
        PUSH    EBX
        XOR     EBX,EBX
        MOV     ECX,EAX
        MOV     EAX,MSecs.Integer[0]
        MOV     EDX,MSecs.Integer[4]
        DIV     [EBX].IMSecsPerDay
        MOV     [ECX].TTimeStamp.Time,EDX
        MOV     [ECX].TTimeStamp.Date,EAX
        POP     EBX
end;

Notice the DIV instruction. That’s where all this funny business takes place. In this particular test case, the divisor is most certainly not 0 and according to the Intel documentation, if the quotient is too large to fit in EAX, then a #DE (Divide Error) exception is raised. If the divisor is 0, then the same exception is raised. Clearly, Windows 32bit is some how figuring out the difference while Windows 64bit doesn't bother. It is probably by disassembling the instruction and inspecting the machine state, which seems a little dangerous in the context of multicore CPUs. For instance, in the above case, the divisor is in a memory location. If another CPU were to modify that memory location before the exception was processed, the OS may report a different status from what actually happened. Yet, from what I've been able to find out, there is no sure way to know whether or not #DE happened from a real divide by zero or an overflow condition.

This took me a while to track down. It wasn’t until one engineer tried it and go the “expected” EIntOverflow and another tried it and got the EDivByZero. The only difference was that of the “bitness” of the OS (32bit vs. 64bit).