Почему компилятор генерирует сдвиг вправо на 31 бит при делении на 2?
Я разобрал код, созданный компилятором, и вижу, что он произвел следующую последовательность инструкций:
mov eax, edx
shr eax, 1Fh
add eax, edx
sar eax, 1
Какова цель этого кода?
я знаю это
sar eax, 1
делится на 2, но что делает
shr eax, 1Fh
делать? Значит ли это, что EAX
будет 0 или 1, если левый бит был 0 или 1?
Это выглядит странно для меня! Может кто-нибудь объяснить это?
1 ответ
Быстрый ответ на ваш вопрос - что shr eax, 1Fh
- это то, что он служит для изоляции самого верхнего бита eax
, Это может быть легче понять, если вы преобразуете шестнадцатеричное 1Fh
в десятичном виде 31
, Теперь вы видите, что вы перемещаетесь eax
прямо на 31. Так как eax
32-битное значение, сдвиг его битов вправо на 31 изолирует самый верхний бит, так что eax
будет содержать либо 0, либо 1, в зависимости от того, какое исходное значение было у бита 31 (при условии, что мы начинаем нумерацию битов с 0).
Это обычная уловка для выделения знакового бита. Когда значение интерпретируется как целое число со знаком на машине с двумя дополнительными символами, самый верхний бит является знаковым битом. Устанавливается (== 1), если значение отрицательное, или сбрасывается (== 0) в противном случае. Конечно, если значение интерпретируется как целое число без знака, самый верхний бит - это просто еще один бит, используемый для хранения его значения, поэтому самый верхний бит имеет произвольное значение.
Идя строка за строкой разборки, вот что делает код:
mov eax, edx
Очевидно, вклад был в EDX
, Эта инструкция копирует значение из EDX
в EAX
, Это позволяет последующему коду манипулировать значением в EAX
без потери оригинала (в EDX
).
shr eax, 1Fh
сдвиг EAX
прямо на 31 место, таким образом изолируя самый верхний бит. Предполагая, что входное значение является целым числом со знаком, это будет бит знака. EAX
теперь будет содержать 1, если исходное значение было отрицательным, или 0 в противном случае.
add eax, edx
Добавить исходное значение (EDX
) к нашему временному значению в EAX
, Если исходное значение было отрицательным, это добавит 1 к нему. В противном случае это добавит 0.
sar eax, 1
сдвиг EAX
прямо на 1 место. Разница здесь в том, что это арифметический сдвиг вправо, тогда как SHR
логический сдвиг вправо. Логический сдвиг заполняет вновь выставленные биты нулями. Арифметический сдвиг копирует самый верхний бит (бит знака) во вновь выставленный бит.
Собирая все вместе, это стандартная идиома для деления целочисленного значения со знаком на 2, чтобы обеспечить правильное округление отрицательных значений.
Когда вы делите значение без знака на 2, все, что требуется, это просто сдвиг битов. Таким образом:
unsigned Foo(unsigned value)
{
return (value / 2);
}
эквивалентно:
shr eax, 1
Но при делении значения со знаком вы должны иметь дело со знаковым битом. Вы могли бы использовать sar eax, 1
реализовать целочисленное деление со знаком на 2, но это приведет к тому, что результирующее значение будет округлено до отрицательной бесконечности. Обратите внимание, что это отличается от поведения DIV
/ IDIV
инструкция, которая всегда округляется до нуля. Если вы хотите эмулировать поведение с округлением до нуля, вам нужна особая обработка, которая в точности соответствует тому, что делает ваш код. Фактически, GCC, Clang, MSVC и, возможно, любой другой компилятор будут генерировать именно этот код при компиляции следующей функции:
int Foo(int value)
{
return (value / 2);
}
Это очень старый трюк. Майкл Абраш обсуждал это в своей книге " Дзен на ассемблере", опубликованной примерно в 1990 году. ( Вот соответствующий раздел в онлайн-копии его книги.) Это было наверняка общеизвестно среди гуру на ассемблере задолго до этого.