我正在尝试将两个数字 50 和 5 相除。这是我的代码:
function Divide(Num1, Num2: Integer): Integer;
asm
MOV EAX, Num1
CDQ
MOV ECX, Num2
IDIV ECX
MOV @RESULT, ECX
end;
它给了我一个DivisionByZeroException
德尔福的例外。有人可以告诉我我做错了什么吗?
这是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;
梅森的回答是准确的,并且清楚地解释了由于 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 就是要理解寄存器的使用和重用。不要用变量名掩盖这一点。保持登记使用的正面和中心,一目了然。一旦开始这样做,您就不会很难发现问题中的错误,并且当值恰好落在正确的寄存器中时,您将能够删除虚假操作。
我的建议是永远不要使用参数名称或Result
asm 代码。
另一个非常明显的点是您正在重新实现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.
现在,DividePas
比DivideAsm
。前者经过优化编译为:
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
并完全跳过代码!