Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASM/Delphi - Divide

I am trying to divide two numbers 50 and 5. This is my code:

function Divide(Num1, Num2: Integer): Integer;
asm
  MOV   EAX, Num1
  CDQ
  MOV   ECX, Num2
  IDIV  ECX
  MOV   @RESULT, ECX
end;

It gives me a DivisionByZeroException exception in Delphi. Can someone tell me what am I doing wrong ?

like image 589
nexno Avatar asked Dec 03 '22 20:12

nexno


2 Answers

It's the CDQ instruction. From an online reference:

Converts signed DWORD in EAX to a signed quad word in EDX:EAX by extending the high order bit of EAX throughout EDX

The problem is, Num2 being the second parameter, is stored in EDX, and since you're running CDQ before loading EDX to ECX, what ends up in ECX is 0. Rewrite it like this, and your routine works as expected:

function Divide(Num1, Num2: integer): integer;
asm
  MOV EAX, Num1
  MOV ECX, Num2
  CDQ
  IDIV ECX
  MOV @Result, EAX
end;
like image 177
Mason Wheeler Avatar answered Dec 18 '22 23:12

Mason Wheeler


Mason's answer is accurate and clearly explains the error due to CDQ sign extending overwriting the input parameter in EDX. No need for me to say more, Mason got it spot on. And note the correction that IDIV returns the quotient in EAX rather than ECX.

I would like to try to offer some more general advice on writing asm. I believe that your fundamental problem here is the use of the parameter names in your asm, rather than register names.

Since you use a register calling convention, it really pays to be explicit about the fact that parameters arrive in registers. Had you done that it might have been clearer what was happening. Trying to use variable names gives you an illusion of abstraction. In reality that abstraction is not there. By hiding the register parameter passing from view you make it hard to spot such errors, and sure enough you stomped on your input!

First of all let's write the code in Mason's answer in terms of registers. Include comments for added clarity. Like this:

function Divide(Num1, Num2: integer): integer;
// Input: EAX: Num1, EDX: Num2
// Output: EAX: Result
asm
  MOV EAX, EAX
  MOV ECX, EDX
  CDQ
  IDIV ECX
  MOV EAX, EAX
end;

Right away we get an immediate benefit that the first and last lines are blatantly pointless. You could not see that in your version because of the use of variable names.

So we can write it like this:

function Divide(Num1, Num2: integer): integer;
// Input: EAX: Num1, EDX: Num2
// Output: EAX: Result
asm
  MOV ECX, EDX
  CDQ
  IDIV ECX
end;

Of course it is no coincidence that most arithmetic operations return the result in EAX, and that same register is used for function return values.


The point is that writing asm is all about understanding register use, and re-use. Don't obscure that with variable names. Keep register use front and centre, in plain sight. Once you start doing so, you'll not make hard to spot errors like in the question, and you'll be able to remove spurious operations when values happen to land in the right registers.

My advice is never to use parameter names, or Result in asm code.


The other very obvious point is that you are re-implementing the div operator. By placing this in an asm function you are inevitably making the code less efficient, and less readable.

For what it is worth, this particular function can actually be written more efficiently as Pascal. Consider the following program:

{$APPTYPE CONSOLE}

function DivideAsm(Num1, Num2: integer): integer;
// Input: EAX: Num1, EDX: Num2
// Output: EAX: Result
asm
  MOV ECX, EDX
  CDQ
  IDIV ECX
end;

function DividePas(Num1, Num2: integer): integer;
begin
  Result := Num1 div Num2;
end;

function DividePasInline(Num1, Num2: integer): integer; inline;
begin
  Result := Num1 div Num2;
end;

var
  i, j, k, l: Integer;

begin
  i := 666;
  j := 42;
  l := 0;
  inc(l, i div j);
  inc(l, DivideAsm(i, j));
  inc(l, DividePas(i, j));
  inc(l, DividePasInline(i, j));
  Writeln(l);
end.

Now, DividePas is worse than DivideAsm. The former is compiled, with optimisation, to:

0040524C 53               push ebx
0040524D 8BDA             mov ebx,edx
0040524F 8BC8             mov ecx,eax
00405251 8BC1             mov eax,ecx
00405253 99               cdq 
00405254 F7FB             idiv ebx
00405256 5B               pop ebx
00405257 C3               ret 

Clearly DivideAsm wins by dint of skipping the prolog/epilog.

But let's look at the main body of the code:

SO22570866.dpr.28: i := 666;
004060D7 BE9A020000       mov esi,$0000029a
SO22570866.dpr.29: j := 42;
004060DC BF2A000000       mov edi,$0000002a
SO22570866.dpr.30: l := 0;
004060E1 33DB             xor ebx,ebx
SO22570866.dpr.31: inc(l, i div j);
004060E3 8BC6             mov eax,esi
004060E5 99               cdq 
004060E6 F7FF             idiv edi
004060E8 03D8             add ebx,eax
SO22570866.dpr.32: inc(l, DivideAsm(i, j));
004060EA 8BD7             mov edx,edi
004060EC 8BC6             mov eax,esi
004060EE E851F1FFFF       call DivideAsm
004060F3 03D8             add ebx,eax
SO22570866.dpr.33: inc(l, DividePas(i, j));
004060F5 8BD7             mov edx,edi
004060F7 8BC6             mov eax,esi
004060F9 E84EF1FFFF       call DividePas
004060FE 03D8             add ebx,eax
SO22570866.dpr.34: inc(l, DividePasInline(i, j));
00406100 8BC6             mov eax,esi
00406102 99               cdq 
00406103 F7FF             idiv edi
00406105 03D8             add ebx,eax

You can see that the compiler has more freedom of register use with the inline version. The compiler is not tied to the calling convention ABI. That allows it to emit fewer MOV operations. In fact, the interaction between the inline engine and the optimiser is very good. Here's the first version of the code that I wrote:

inc(l, DivideAsm(i, j));
inc(l, DividePas(i, j));
inc(l, i div j);
inc(l, DividePasInline(i, j));

But the optimiser defeats me on the last two statements:

SO22570866.dpr.33: inc(l, i div j);
004060F9 8BC6             mov eax,esi
004060FB 99               cdq 
004060FC F7FF             idiv edi
004060FE 8BC8             mov ecx,eax
00406100 03D9             add ebx,ecx
SO22570866.dpr.34: inc(l, DividePasInline(i, j));
00406102 03D9             add ebx,ecx

The optimiser is able to recognize that the ECX register already contains the result of DividePasInline and skips the code altogether!

like image 22
David Heffernan Avatar answered Dec 18 '22 23:12

David Heffernan