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).

5 comments:

  1. May be extend Delphi for support protected mode supervisor programming with Delphi driver injecting with some magic class TIDTCallgateDelegates and make this analyse of instruction opcodes in it. Why not? :)))

    ReplyDelete
  2. Pure Assembler is always dangerous, when platforms or compilers change. I would do the job with Pascal code. A good compiler produces almost the same code quality as pure asm.

    ReplyDelete
  3. Peter,

    Of course, however in this case it isn't about the assembly code. What if a compiler generated that code? Also, the same binary behaves differently depending between the two OSs.

    ReplyDelete
  4. is not possible, that the CPU in 64bit have a little difference between the 32bit?
    Specially in a DIV operation, XOR EBX,EBX = 0 but all 64bit register is 0?? , or there is a bug in the CPU when you use 32bit register in 64bit processor mode?

    ReplyDelete
  5. only to try in asm code tring to do the same operation usign RBX like XOR RBX,RBX is possible ?

    ReplyDelete

Please keep your comments related to the post on which you are commenting. No spam, personal attacks, or general nastiness. I will be watching and will delete comments I find irrelevant, offensive and unnecessary.