Search code examples
delphilazarusfreepascalfpcbasm

FPC BASM32 MUL bug?


I come across a problem while porting Delphi BASM32 code to FPC:

program MulTest;

{$IFDEF FPC}
  {$mode delphi}
  {$asmmode intel}
{$ELSE}
  {$APPTYPE CONSOLE}
{$ENDIF}

function Mul(A, B: LongWord): LongWord;
asm
         MUL    EAX,EDX
end;

begin
  Writeln(Mul(10,20));
  Readln;
end.

The above code compiles in Delphi XE and works as expected; FPC outputs compile-time error on MUL EAX,EDX line:

Error: Asm: [mul reg32,reg32] invalid combination of opcode and operands

I am using Lazarus 1.4.4/FPC2.6.4 for Win32 (the current stable version)

Any workaround or fix for the problem?


Solution

  • FreePascal is correct. There are only 3 forms of MUL:

    MUL r/m8
    MUL r/m16
    MUL r/m32
    

    Performs an unsigned multiplication of the first operand (destination operand) and the second operand (source operand) and stores the result in the destination operand. The destination operand is an implied operand located in register AL, AX or EAX (depending on the size of the operand); the source operand is located in a general-purpose register or a memory location.

    In other words, the first operand (used for both input and output) is specified in AL/AX/EAX, and the second input operand is explicitly specified as either a general-purpose register or a memory address.

    So, MUL EAX,EDX is indeed an invalid assembly instruction.

    If you compile this code in Delphi and use the debugger to look at the generated assembly, you would see that the call to Mul(10,20) generates the following assembly code:

    // Mul(10,20)
    mov edx,$00000014
    mov eax,$0000000a
    call Mul
    

    //MUL    EAX,EDX
    mul edx
    

    So, as you can see, Delphi is actual parsing your source code, sees that the first operand is EAX and strips it off for you, thus producing the correct assembly. FreePascal is not doing that step for you.

    The workaround? Write proper assembly code to begin with. Don't rely on the compiler to re-interpret your code for you.

    function Mul(A, B: LongWord): LongWord;
    asm
             MUL    EDX
    end;
    

    Or, you could simply not write assembly code directly, let the compiler do the work for you. It knows how to multiple two LongWord values together:

    function Mul(A, B: LongWord): LongWord;
    begin
      Result := A * B;
    end;
    

    Though Delphi does use IMUL instead of MUL in this case. From Delphi's documentation:

    The value of x / y is of type Extended, regardless of the types of x and y. For other arithmetic operators, the result is of type Extended whenever at least one operand is a real; otherwise, the result is of type Int64 when at least one operand is of type Int64; otherwise, the result is of type Integer. If an operand's type is a subrange of an integer type, it is treated as if it were of the integer type.

    It also uses some unsightly bloated assembly unless stackframes are disabled and optimizations are enabled. By configuring those two options, it is possible to get Mul() to generate a single IMUL EDX instruction (plus the RET instruction, of course). If you don't want to change the options project-wide, you can isolate them to just Mul() by using the {$STACKFRAMES OFF}/{$W-} and {$OPTIMIZATION ON}/{$O+} compiler instructions.

    {$IFOPT W+}{$W-}{$DEFINE SF_Was_On}{$ENDIF}
    {$IFOPT O-}{$O+}{$DEFINE O_Was_Off}{$ENDIF}
    function Mul(A, B: LongWord): LongWord;
    begin
      Result := A * B;
    end;
    {$IFDEF SF_Was_On}{W+}{$UNDEF SF_Was_On}{$ENDIF}
    {$IFDEF O_Was_Off}{O-}{$UNDEF O_Was_Off}{$ENDIF}
    

    Generates:

    imul edx
    ret