4

我正在尝试将两个数字 50 和 5 相除。这是我的代码:

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

它给了我一个DivisionByZeroException德尔福的例外。有人可以告诉我我做错了什么吗?

4

2 回答 2

9

这是CDQ指示。来自在线参考

通过在整个 EDX 中扩展 EAX 的高位位,将 EAX 中的有符号 DWORD 转换为 EDX:EAX 中的有符号四字

问题是,Num2作为第二个参数,存储在 EDX 中,并且由于您CDQ在将 EDX 加载到 ECX 之前运行,因此 ECX 中的结果为 0。像这样重写它,您的例程按预期工作:

function Divide(Num1, Num2: integer): integer;
asm
  MOV EAX, Num1
  MOV ECX, Num2
  CDQ
  IDIV ECX
  MOV @Result, EAX
end;
于 2014-03-21T22:57:07.577 回答
8

梅森的回答是准确的,并且清楚地解释了由于 CDQ 符号扩展覆盖 EDX 中的输入参数而导致的错误。不用我多说,梅森说得对。并注意 IDIV 返回 EAX 而不是 ECX 中的商的更正。

我想尝试提供一些关于编写 asm 的更一般的建议。我相信您的基本问题是在您的 asm 中使用参数名称,而不是注册名称。

由于您使用寄存器调用约定,因此明确说明参数到达寄存器的事实确实值得。如果你这样做了,可能会更清楚发生了什么。尝试使用变量名会给您一种抽象的错觉。实际上,抽象并不存在。通过隐藏从视图中传递的寄存器参数,您很难发现此类错误,并且确实您踩到了您的输入!

首先,让我们根据寄存器编写 Mason 答案中的代码。包括注释以增加清晰度。像这样:

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;

我们立即得到一个直接的好处,即第一行和最后一行毫无意义。由于使用了变量名,您无法在您的版本中看到这一点。

所以我们可以这样写:

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

当然,大多数算术运算在 EAX 中返回结果并不是巧合,并且相同的寄存器用于函数返回值。


关键是编写 asm 就是要理解寄存器的使用和重用。不要用变量名掩盖这一点。保持登记使用的正面和中心,一目了然。一旦开始这样做,您就不会很难发现问题中的错误,并且当值恰好落在正确的寄存器中时,您将能够删除虚假操作。

我的建议是永远不要使用参数名称或Resultasm 代码。


另一个非常明显的点是您正在重新实现div运算符。通过将它放在 asm 函数中,您不可避免地会降低代码的效率和可读性。

对于它的价值,这个特定的函数实际上可以更有效地编写为 Pascal。考虑以下程序:

{$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.

现在,DividePasDivideAsm。前者经过优化编译为:

0040524C 53 推 ebx
0040524D 8BDA mov ebx,edx
0040524F 8BC8 mov ecx,eax
00405251 8BC1 移动 eax,ecx
00405253 99 cdq
00405254 F7FB idiv ebx
00405256 5B 流行 ebx
00405257 C3 RET

DivideAsm通过跳过序言/结语显然获胜。

但是让我们看一下代码的主体:

SO22570866.dpr.28:我:= 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 添加 ebx,eax
SO22570866.dpr.32: inc(l, DivideAsm(i, j));
004060EA 8BD7 mov edx,edi
004060EC 8BC6 mov eax,esi
004060EE E851F1FFFF 调用 DivideAsm
004060F3 03D8 添加 ebx,eax
SO22570866.dpr.33: inc(l, DividePas(i, j));
004060F5 8BD7 mov edx,edi
004060F7 8BC6 mov eax,esi
004060F9 E84EF1FFFF 调用 DividePas
004060FE 03D8 添加 ebx,eax
SO22570866.dpr.34: inc(l, DividePasInline(i, j));
00406100 8BC6 mov eax,esi
00406102 99 cdq
00406103 F7FF idiv edi
00406105 03D8 添加 ebx,eax

您可以看到编译器在使用内联版本时拥有更多的寄存器使用自由度。编译器不依赖于调用约定 ABI。这允许它发出更少的MOV操作。事实上,内联引擎和优化器之间的交互非常好。这是我编写的代码的第一个版本:

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

但是优化器在最后两个陈述中击败了我:

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 添加 ebx,ecx
SO22570866.dpr.34: inc(l, DividePasInline(i, j));
00406102 03D9 添加 ebx,ecx

优化器能够识别出ECX寄存器已经包含结果DividePasInline并完全跳过代码!

于 2014-03-21T23:15:32.253 回答