Когда и почему мы подписываем расширение и используем cdq с mul/div?

Сегодня у меня был тест, и единственный вопрос, который я не понял, касался преобразования двойного слова в четверное слово.

Это заставило меня задуматься, почему/когда мы подписываем расширение для умножения или деления? Кроме того, когда мы используем такие инструкции, как cdq?


person Community    schedule 07.04.2016    source источник


Ответы (1)


Используйте cdq / idiv для знакового 32-битного / 32-битного => 32-битного деления,
xor edx,edx / div для беззнакового.

С делимым в EAX для начала и делителем, указанным в качестве операнда для DIV или IDIV.

   mov  eax, 1234
   mov  ecx, 17
   cdq                   ; EDX = signbit(EAX)
   idiv  ecx             ; EAX = 1234/17     EDX = 1234%17

Если вы обнулите EDX/RDX вместо расширения знака в EDX:EAX до idiv, можно получить большой положительный результат для -5/2, например.

Использование «полной мощности» 64/32-битного => 32-битного деления возможно, но небезопасно, если вы не знаете, что делитель достаточно велик, чтобы частное не переполнялось. (т. е. вы не можете вообще реализовать (a*b) / c только с mul / div и 64-битным временным файлом в EDX:EAX.)

Разделение вызывает исключение (#DE) при переполнении частного. В Unix/Linux ядро ​​поставляет SIGFPE для арифметических исключений, включая ошибки деления. При нормальном знаке или делении с нулевым расширением переполнение возможно только fpe/46378352">с idiv из INT_MIN / -1 (т. е. 2-й дополнительный частный случай самого отрицательного числа.)


Как видно из справочного руководства insn (ссылка в разделе x86 тег вики):

  • один операнд mul/imul: edx:eax = eax * src
  • два операнда imul: dst *= src. например imul ecx, esi не читает и не записывает eax или edx.

  • div / idiv: делит edx:eax на источник. частное в eax, остаток в edx. Не существует формы div/idiv, которая игнорирует edx во входных данных.
  • cdq sign расширяет eax до edx:eax, т. е. передает бит знака eax в каждый бит edx . Не путать с cdqe, 64-битной инструкцией, которая является более компактной формой movsxd rax, eax.

    Первоначально (8086) было только cbw (ax = sign_extend(al)) и cwd (dx:ax = sign_extend(ax)). Расширения x86 до 32-битных и 64-битных сделали мнемонику немного двусмысленной (но помните, кроме cbw, версии внутри eax всегда заканчиваются на e для расширения). Инструкции dl=sign_bit(al) нет, потому что 8-битные mul и div являются специальными и используют ax вместо dl:al.


Поскольку входные данные для [i]mul являются одиночными регистрами, вам не нужно ничего делать с edx перед умножением.

Если ваш ввод подписан, вы расширяете его, чтобы заполнить регистр, который вы используете в качестве ввода для умножения, например. с movsx или cwde (eax = sign_extend(ax)). Если ваш ввод не подписан, вы нулевое расширение. (За исключением того, что если вам нужны только младшие 16 бит результата умножения, например, не имеет значения, содержат ли старшие 16 бит одного или обоих входных данных мусор.)


Для деления вам всегда нужно обнулить или расширить знак eax в edx. Обнуление — это то же самое, что и безусловное обнуление edx, поэтому для этого нет специальной инструкции. Просто xor edx,edx.

cdq существует потому, что он намного короче, чем mov edx, eax / sar edx, 31, чтобы передать бит знака eax каждому биту в edx. Кроме того, сдвиги с немедленным счетчиком > 1 не существовали до 186 и по-прежнему составляли 1 цикл на счет, поэтому на 8086 вам пришлось бы делать что-то еще хуже (например, перейти или повернуть бит знака вниз и изолировать + neg Это). Таким образом, cwd в 8086 сэкономил много времени/пространства, когда это было необходимо.


В 64-битном режиме знак и ноль расширяют 32-битные значения до 64-битных. ABI допускает мусор в старших 32 битах 64-битного регистра, содержащего 32-битное значение, поэтому, если ваша функция должна просматривать только младшие 32 бита edi, вы не можете просто использовать [array + rdi] для индексации массива.

Таким образом, вы видите много movsx rdi, edi (расширение знака) или mov eax, edi (расширение нуля, и да, более эффективно использовать другой целевой регистр, потому что Intel mov-elimination не работает с mov same,same)

person Peter Cordes    schedule 07.04.2016
comment
Извините - я всегда путаюсь с делением в ассемблере, потому что путаюсь с регистрами. Я думал, что делимое всегда помещалось в eax/ax, а инструкция с одним операндом была просто div/idiv ebx (или любым другим регистром). Который будет эффективно выполнять eax/ebx с частным в eax и остатком в edx. Мой экзамен показал, что мы использовали cdq до того, как мы вызвали idiv для EAX, содержащего 71, и другого регистра, содержащего -4. Почему это? Мы использовали весь каждый регистр. Я не понимаю, зачем нам нужно было, чтобы один из них был четверным словом. - person ; 07.04.2016
comment
@Koronakesh: прочитайте первую строку моего ответа и/или справочное руководство Intel по insn. idiv ebx делает eax = (edx:eax)/ebx и eax = (edx:eax)%ebx. edx всегда является старшей половиной делимого, а явный операнд всегда является делителем. Не существует формы div / idiv, которая игнорирует edx так же, как 2- и 3-операндные формы imul дают только результат с одним регистром. - person Peter Cordes; 07.04.2016
comment
Хорошо - теперь это имеет смысл. Есть ли требования к размеру дивиденда по сравнению с делителем? Кроме того, существуют ли такие инструкции, как cdq, просто потому, что они на 1 байт дешевле, чем что-то вроде sub edx, edx? - person ; 07.04.2016
comment
@Koronakesh: cdq существует, потому что он намного короче, чем mov edx, eax / sar edx, 31 для передачи знакового бита eax каждому биту в edx. xor edx,edx ноль-расширяет, что отличается от расширения знака. Кроме того, до 286 не существовало сдвигов со значением count › 1, поэтому необходимость в цикле была бы действительно ужасной. Что касается ограничений по размеру, да, если вы прочитаете справочное руководство, вы увидите, что div выдает ошибку, если частное превышает размер операнда (например, 32 бита). - person Peter Cordes; 07.04.2016