Я пытаюсь разделить два числа 50 и 5. Это мой код:

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

Это дает мне исключение DivisionByZeroException в Delphi. Может кто-нибудь сказать мне, что я делаю не так?

4
nexno 22 Мар 2014 в 02:48
2
Zahl1 и Zahl2 не Num1 и Num2. Это правильно? Вы там ссылаетесь на глобальные переменные, или перевод (с немецкого?) неполный?
 – 
GolezTrol
22 Мар 2014 в 02:52
@GolezTrol: это перевод. «Zahl» по-немецки означает «число».
 – 
Mason Wheeler
22 Мар 2014 в 02:53
2
@MasonWheeler, но это также Паскаль для «необъявленного идентификатора».
 – 
Free Consulting
22 Мар 2014 в 02:55
Почему вы внедряете div? Это в образовательных целях?
 – 
David Heffernan
22 Мар 2014 в 14:09

2 ответа

Лучший ответ

Это инструкция CDQ. Из онлайн-справки:

Преобразует знаковый DWORD в EAX в четверное слово со знаком в EDX: EAX путем расширения старшего бита EAX по всему EDX.

Проблема в том, что 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;
9
David Heffernan 22 Мар 2014 в 03:33
Упс. Я забыл заменить Zahl1 на Num1 --- Глупый я. Вобщем спасибо :)
 – 
nexno
22 Мар 2014 в 02:59
@nexno: рад, что смог помочь. Если это решит вашу проблему, используйте виджет с галочкой, чтобы отметить ответ как принятый. Спасибо! :)
 – 
Mason Wheeler
22 Мар 2014 в 03:00
Я сделал это сейчас. Еще раз спасибо.
 – 
nexno
22 Мар 2014 в 03:04
@FrankKotler: это то, что делает последняя строка. Я собирался с минимальной конверсией его оригинальной рутины.
 – 
Mason Wheeler
22 Мар 2014 в 03:21
Спасибо за правки. Я вырезал все упоминания о Зале. Это была просто ошибка транскрипции при вопросе. Вопрос и ответы намного лучше без этой путаницы.
 – 
David Heffernan
22 Мар 2014 в 03:34

Ответ Мэйсона точен и ясно объясняет ошибку из-за расширения знака CDQ, перезаписывающего входной параметр в EDX. Мне не нужно больше говорить, Мейсон все понял. И обратите внимание на поправку, согласно которой IDIV возвращает частное в EAX, а не в ECX.

Я хотел бы попытаться дать несколько более общих советов по написанию asm. Я считаю, что ваша основная проблема здесь заключается в использовании имен параметров в вашем asm, а не имен регистров.

Поскольку вы используете соглашение о вызове регистров, действительно стоит четко указывать, что параметры поступают в регистры. Если бы вы сделали это, было бы яснее, что происходит. Попытка использовать имена переменных создает иллюзию абстракции. На самом деле этой абстракции нет. Скрывая передачу параметра регистра из поля зрения, вы затрудняете обнаружение таких ошибок, и, конечно же, вы наступили на свой ввод!

Прежде всего, давайте напишем код в ответе Мейсона в терминах регистров. Включите комментарии для большей ясности. Как это:

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

Очевидно, что DivideAsm выигрывает, если пропустить пролог / эпилог.

Но давайте посмотрим на основную часть кода:

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

Вы можете видеть, что компилятор имеет больше свободы в использовании регистров со встроенной версией. Компилятор не привязан к соглашению о вызовах 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             add ebx,ecx
SO22570866.dpr.34: inc(l, DividePasInline(i, j));
00406102 03D9             add ebx,ecx

Оптимизатор может распознать, что регистр ECX уже содержит результат DividePasInline, и полностью пропускает код!

8
David Heffernan 22 Мар 2014 в 14:09
+1 четкое объяснение преимуществ встраивания вместо asm для небольших подпрограмм. Используйте asm только при наличии узкого места! Преждевременная оптимизация - корень всех зол... Также хороший совет по поводу явного использования регистров asm. Еще один совет: сначала напишите версию на паскале, затем конвертируйте в ассемблер только в случае необходимости, но всегда сохраняйте версию на паскале для других процессоров (например, arm или x64).
 – 
Arnaud Bouchez
22 Мар 2014 в 14:42
Спасибо за ваше объяснение, это отлично работает для целочисленного деления.
 – 
nexno
22 Мар 2014 в 14:43
Сейчас я пытаюсь сделать то же самое для Real: puu.sh/7ESly/dc0eedbc99.png и я пытаюсь назвать это так: puu.sh/7ESpj/32bf211d30.png и Я получаю сообщение об ошибке прямо здесь: puu.sh/7ESrs/cefafe1b61.png
 – 
nexno
22 Мар 2014 в 14:47
Ну, вы должны понимать ABI. И операции. Я не очень хочу отвечать на этот вопрос в комментариях. Почему вы пишете функции asm для деления, а не используете оператор /. Ваш код будет хуже.
 – 
David Heffernan
22 Мар 2014 в 15:13
Просто чтобы получить больше опыта работы с ASM ^^
 – 
nexno
22 Мар 2014 в 15:21