Сдвиг и вращение

Последнее обновление: 13.10.2023

Инструкции сдвига и вращения также манипулируют отдельными битами числа.

Сдвиг влево

Для сдвига влево применяется инструкция shl, которая имеет следующий синтаксис:

shl dest, count

Первый операнд - dest представляет число, в котором сдвигаются биты. Это может быть либо регистр, либо адрес памяти/переменная. Второй операнд - count указывается на сколько разрядов сдвигаем влево число из первого операнда. Пример программы на Linux, которая применяет сдвиг влево:

global _start

section .text
_start:
    mov rdi, 5       ; в RDI число 5 или 00000101
    shl rdi, 1       ; сдвигаем число в RDI на 1 разряд влево = 00000101 << 1 = 00001010 = 10
 
    mov rax, 60
    syscall 

Здесь в регистр RDI помещаем число 5. Поскольку регистр 64-разрядный, то его старшие 7 байт будут содержать нули. А значение из младшего байта будет равно 00000101 (число 5 в двоичной системе). С помощью инструкции shl сдвигаем число из регистра RDI влево на 1 разряд:

shl rdi, 1

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

00000101 << 1 = 00001010 = 10

С помощью символов << обычно обозначается сдвиг влево. В регистре RDI в итоге будет число 00001010, которое в десятичной системе равно 10.

Аналогичный пример для Windows:

global _start

section .text
_start:
    mov rax, 5       ; в RAX число 5 или 00000101
    shl rax, 1       ; сдвигаем число в RAX на 1 разряд влево = 00000101 << 1 = 00001010 = 10
    ret

Можно заметить, что при сдвиг на n разрядов влево фактически равнозначен умножению на n. Поэтому операцию сдвига влево можно использовать вместо операции умножения.

Стоит учитывать, что при сдвиге влево последний отбрасываемый бит копируется в регистр флагов во флаг переноса CF. И при необходимости мы можем инструкциями условного перехода, которые производят переход, если флаг переноса установлен:

global _start

section .text
_start:
    mov al, 69      ; в AL число 69 или 01000101
    shl al, 2       ; сдвигаем число в AL на 2 разряда влево = 01000101 << 2 = 00010100 = 20
    jc carry_set    ; при сдвиге отброшены 2 левых бита - 01 - флаг CF установлен
    mov rdi, 2
    jmp exit
carry_set:
    mov rdi, 4
exit:
    mov rax, 60
    syscall 

Здесь также сдвигаем число 69 или 01000101 влево на 2 разряда. Это ведет к тому, что левые два бита - 01 отбрасываются. Последний из этих битов копируется во флаг переноса CF. Поскольку последний отбрасываемый бит здесь равен 1, то соответственно флаг CF будет установлен. А с помощью инструкции jc мы можем проверить установку этого флага и выполнить переход к метке carry_set, если флаг установлен.

Кроме флага CF инструкция сдвига также устанавливает и другие флаги состояния. Флаг нуля ZF устанавливается, если в результате сдвига получилось число 0. Флаг знака устанавливается, если старший бит результата равен 1. Флаг переполнения OF устанавливается, если при сдвиге на 1 разряд меняется старший бит (с 0 на 1 или с 1 на 0).

Логический сдвиг вправо

Для сдвига вправо предназначена инструкция shr, которая работает во многом аналогично инструкции shl:

shr dest, count

В качестве первого операнда также передается сдвигаемое число, а в качестве второго операнда - на сколько разрядов надо сдвинуть число вправо. Например:

global _start

section .text
_start:
    mov rdi, 69     ; в RDI число 69 или 01000101
    shr rdi, 2      ; сдвигаем число в RDI на 2 разряда вправо = 01000101 >> 2 = 00010001 = 17
    mov rax, 60
    syscall 

Здесь помещаем в регистр RDI число 69 или 01000101 в двоичной системе и сдвигаем его на два разряда вправо. Сдвиг вправо на n разрядов приводит к тому что n правых разрядов числа отбрасываются, а с слева добавляются n нулевых битов. Поэтому при сдвиге числа 01000101 вправо отбрасываем справа 2 бита и слева добавляем 2 нуля:

01000101 >> 2 = 00010001 = 17

Фактически сдвиг вправо беззнаковых чисел на N разрядов эквивалентно делению числа на 2*N.

При сдвиге вправо также устанавливаются флаги состояния. Последний отрасываемый бит копируется во флаг переноса CF. Если результат сдвига равен 0, то также устанавливается флаг нуля ZF. Если при сдвиге вправо на 1 разряд меняется старший бит (с 1 на 0), то также устанавливается флаг переполнения OF. При этом флаг знака SF всегда сбрасывается, так как слева в любом случае добавляется 0.

Арифметический сдвиг вправо

Кроме логического сдвига вправо в ассемблере есть арифметически сдвиг вправо, для которого применяется инструкция sar:

sar dest, count

Она работает во многом аналогично за тем исключением, что сдвигаемое число слево дополняется не нулями, а значением из старшего бита. Это позволяет сохранить значение знакового бита. Например, возьмем сначала стандартный логический сдвиг вправо:

global _start

section .text
_start:
    mov rdi, -32    ; в RDI число -32 или FFFFFFE0h
    shr rdi, 4      ; сдвигаем число в RDI на 4 разряда вправо = FFFFFFE0 >> 4 = 0FFFFFFE = 268435454

    mov rax, 60
    syscall 

Здесь в регистр RDI помещается отрицательное число -32 (FFFFFFE0 в 16-чной системе). Когда логического сдвига сдивает на 4 байта вправо, в итоге получаем 0FFFFFFE или 268435454 в десятичной системе. С точки зрения логического сдвига это нормальный результат. Но например выше мы видели, что логический сдвиг вправо беззнакового числа на N разрядов вправо эквивалентен делению на 2*N. С числами со знаком такого не проходит. И в этом случае мы можем использовать арифметический сдвиг вправо:

global _start

section .text
_start:
    mov rdi, -32    ; в RDI число -32 или FFFFFFE0h
    sar rdi, 4      ; сдвигаем число в RDI на 4 разряда вправо = FFFFFFE0 >> 4 = FFFFFFFE = -2

    mov rax, 60
    syscall 

При арифметическим сдвиге вправо также устанавливаются флаги состояния. Последний отрасываемый бит копируется во флаг переноса CF. Если результат сдвига равен 0 (например, сдвигается положительное число), то также устанавливается флаг нуля ZF. Но в отличие от операции логического сдвига флаг переполнения OF всегда сброшен (так как знак числа не меняется), а флаг знака SF получает значение старшего бита числа.

Вращение

Операции вращения влево и вправо ведут себя так же, как операции сдвига влево и вправо, за тем исключением, что бит, смещенный с одного конца, помещается обратно в другой конец. Для вращения предусмотрены две инструкции: rol (поворот влево) и ror (поворот вправо). Синтаксис этих инструкций аналогичен инструкциям сдвига:

rol dest, count
ror dest, count

Первый операнд представляет сдвигаемое число (в виде регистра или переменной), а второй операнд - количество сдвигаемых битов.

Эти инструкции также изменяются флаги состояния. Но в отличие от инструкций сдвига, инструкции поворота не влияют на установку флагов знака или нуля. Последний сдвинутый бит копируется во флаг переноса CF. При 1-битном повороте также изменяется флаг OF: при повороте влево флаг OF получает результат операции XOR (исключающее ИЛИ) двух первых старших битов, а при повороте вправо флаг OF получает результат операции XOR двух старших битов после поворота.

Пример вращения влево:

global _start

section .text
_start:
    mov rax, 131    ; в AL число 131 или 10000011
    rol al, 2      ; вращаем число в AL на 2 разряда влево = 10000011 << 2 = 00001110 = 14
    mov rdi, rax
    mov rax, 60
    syscall 

Здесь в регистр AL помещается число 131 или 100000112. Инструкция rol поворачивает это число на разряда влево, поэтому два старших бита помещаются в начало

10000011 << 2 = 00001110

Что в десятичной системе будет равно 14.

Пример операции вращения вправо:

global _start

section .text
_start:
    mov rax, 131    ; в AL число 131 или 10000011
    ror al, 2      ; вращаем число в AL на 2 разряда вправо = 10000011 >> 2 = 11100000 = 224

    mov rdi, rax
    mov rax, 60
    syscall 

Здесь вращаем тоже самое число также на 2 разряда только вправо, поэтому два последних бита перейдут в начало, а результат будет равен 224:

10000011 >> 2 = 11100000

Дополнительно ассемблер предоставляет инструкции для вращения с использованием флага переноса - rcl (вращение влево с переносом) и rcr (вращение вправо с переносом):

rcl dest, count
rcr dest count

При вращении влево смещаемый старший бит числа переходит во флаг переноса, а бит из флага переноса переходит в конец числа. При вращении вправо смещаемый младший бит переходит во флаг переноса, а бит из флага переноса переходит в начало числа.

Поворот и сдвиг числа в ассемблере

Пример вращения влево с переносом

global _start

section .text
_start:
    mov rax, 131    ; в AL число 131 или 10000011
    rcl al, 2       ; вращаем число в AL на 2 разряда влево = 10000011 << 2 = 00001101 = 13

    mov rdi, rax
    mov rax, 60
    syscall 

Здесь число 131 или 100000112 вращается влево на 2 разряда с переносом. Вращение на 2 разряда мы можем разбить на два этапа - по каждому разряду. Поскольку вначале программы флаг переноса не установлен, равен 0, то при вращении первого бита числа нулевой бит из флага переноса перейдет в конец числа, а старщий бит числа - во флаг переноса:

Перед операцией
CF = 0
AL = 10000011 
После операции
CF = 1
AL =00000110

При вращении второго бита повторяются действия:

Перед операцией
CF = 1
AL = 00000110 
После операции
CF = 0
AL =00001101
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850