Почему компилятор генерирует сдвиг вправо на 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 году. ( Вот соответствующий раздел в онлайн-копии его книги.) Это было наверняка общеизвестно среди гуру на ассемблере задолго до этого.

Другие вопросы по тегам