§ Общая конструкция

Инструкции сдвига, как по мне, самые любопытные инструкции. Они всегда занимают пространство в однооперандных групповых инструкциях с операндом modrm и режимом работы, задаваемым reg-частью. Всего существуют 8 типов инструкции сдвига (основных, так как есть и дополнительные):
  • 0 ROL - Вращение влево, из старшего бита пишется в младший
  • 1 ROR - Вращение вправо, из младшего бита пишется в старший
  • 2 RCL - Сдвиг влево через C, в младший бит пишется CF
  • 3 RCR - Сдвиг вправо через C, в старший бит пишется CF
  • 4 SHL - Логический сдвиг влево, в младшит бит пишется 0
  • 5 SHR - Логический сдвиг вправо, в старший бит записывается 0
  • 6 ??? - Неизвестная инструкция, но должен быть SAL, который повторяет инструкцию SHL
  • 7 SAR - Арифметический сдвиг вправо, старший бит остается на месте и сдвигается тоже
Как и с операциями АЛУ, создадим такую же функцию для работы:
// Инструкции сдвига
uint16_t shiftlogic(char id, char i_w, uint16_t op1, uint16_t op2) {

    int cf = 0, i, msb;
    int bits = i_w ? 0x08000 : 0x080;
    int bitw = i_w ? 0x0FFFF : 0x0FF;
    int bitc = i_w ? 0x10000 : 0x100;

    op2 &= (i_w ? 15 : 7);

    switch (id) {
       case 0: break; // ROL
       case 1: break; // ROR
       case 2: break; // RCL
       case 3: break; // RCR
       case 4: case 6: break; // SHL
       case 5: break; // SHR
       case 7: break; // SAR
    }

    return op1 & bitw;
}
В этом коде снова вычисляются старший бит bits, маска результата bitw и бит переноса bitc. Возвращается & bitw - либо 8 битное, либо 16 битное число, это зависит от заданного i_w (0-8 бит, 1-16 бит). Режим работы выбирается через id. Операндом op2 выступает здесь количество сдвигов, которое не может быть более 15, поскольку сдвиг 16 раз либо возвращает тот же самый op1, либо обнуляет или устанавливает все биты 1. Поэтому ограничение в 15 бит и стоит здесь. Для 32 битных или 64 битных ограничение будет 31 или 63 бита соответственно. ID = 4 и 6 я совместил в одну, так как это по сути одна и та же операция.

§ ROL

Вращение влево. Заданный операнд (первый) сначала сдвигается влево на 1 бит, а старший бит отсылается в младший, получается, что операнд вращается.
for (i = 0; i < op2; i++) {

    cf  = !!(op1 & bits);
    op1 = ((op1 << 1) | cf) & bitw;
}

flags.c = cf;
flags.o = !!(op1 & bits) ^ cf;
В этом коде вращение повторяется op2 раза, при этом сначала проверяется старший бит, потом происходит сдвиг, и он отсылается в бит 0. При этом, старший бит этот сохраняется во временный cf. После выполнения всех сдвигов, во флаги записывается полученный cf. Во флаг OF записывается XOR над старшим битов результата и флагом CF. Причем в официальной документации Intel я посмотрел, при количестве вращений более 1 этот флаг на самом деле не определен. Но я ничего не теряю, раз он не определен, то пускай записывается туда что угодно, а то есть SF^CF.

§ ROR

Эта инструкция тоже вращает операнд, но уже вправо.
for (i = 0; i < op2; i++) {

    cf  = op1 & 1;
    op1 = ((op1 >> 1) | (cf ? bits : 0));
}

flags.o = !!((op1 ^ (op1 << 1)) & bits);
flags.c = cf;
Действие похоже на ROL. В CF записывается предыдущее значение младшего бита op1 перед сдвигом, после чего сдвигается, и записывает в старший разряд. Как обычно, значение CF записывается во флаг переноса, а вот флаг OF выставляется уже иначе: делается XOR между старшим битом и предшествующим ему битом результата (например между битом 7 и 6, или битом 15 и 14), и полученное значение пишется в OF. При сдвиге более 1, этот бит не определен, но у меня он определен.

§ RCL

Инструкция RCL сдвигает операнд влево, но в младшем бите будет предыдущее значение CF, в том числе и то, что получается во время самого сдвига, если сдвигов более 1.
cf = flags.c;
for (i = 0; i < op2; i++) {

    old_cf = cf;
    cf  = !!(op1 & bits);
    op1 = ((op1 << 1) | old_cf) & bitw;
}

flags.c = cf;
flags.o = cf ^ !!(op1 & bits);
Код по сути повторяет предыдущие операции сдвигов. Здесь такой момент есть, что при первом сдвиге, используется реально флаг C, который был до инструкции. Потом вычисляется новый флаг CF, сдвигается, записывается в бит 0 предыдущее значение CF. И если будет еще раз сдвиг, то записываться будет тот CF, который был получен после сдвига. Флаги CF, OF выставляются подобно операции ROL.

§ RCR

Инструкция RCR сдвигает операнд вправо, и в старшем бите будет предыдущее значение CF, аналогично RCL, тоже при сдвиге более 1 раза, будет записываться предыдущий флаг переноса после предыдущего сдвига.
op1 &= bitw;
cf   = flags.c;

for (i = 0; i < op2; i++) {

    old_cf = cf;
    cf  = op1 & 1;
    op1 = (op1 >> 1) | (old_cf ? bits : 0);
}

flags.c = cf;
flags.o = !!((op1 ^ (op1 << 1)) & bits);
Тут все так же, как и в RCL, но сдвиг вправо. Флаги CF, OF выставляются подобно операции ROR.

§ SHL

Логический сдвиг влево.
for (i = 0; i < op2; i++) {

    cf  = !!(op1 & bits);
    op1 = (op1 << 1) & bitw;
}

if (op2) {

    flags.s = !!(op1 & bits);
    flags.z = !op1;
    flags.p = parity(op1);
    flags.c = cf;
    flags.o = cf ^ flags.s;
}
Если сдвиг не указан (op2=0), то никакие флаги меняться не будут. Здесь, в отличии от предыдущих сдвиговых операции, устанавливаются флаги SF,ZF,PF в соответствии с результатом. В SF пишется старший бит, в ZF=1 при результате 0, и PF при четности младших 8 бит. Флаг OF устанавливается в случае если XOR между полученным CF и SF равен 1, проще говоря OF = CF XOR SF.

§ SHR

Логический сдвиг вправо.
op1 &= bitw;
if (op2) flags.o = !!(op1 & bits);

for (i = 0; i < op2; i++) {

    cf  = op1 & 1;
    op1 = (op1 >> 1);
}

if (op2) {

    flags.s = !!(op1 & bits);
    flags.z = !op1;
    flags.p = parity(op1);
    flags.c = cf;
}
В отличии от SHL, флаг OF всегда ставится в то значение, что было в старшем бите исходного операнда. Остальные флаги, собственно говоря, ставятся аналогично SHL, в том числе если количество сдвигов не равно 0, а если не так, то никакие флаги не меняются.

§ SAR

Арифметический сдвиг вправо. Почему это называется арифметическим? Дело в знаке. При сдвиге вправо сохраняется знак, который находится в старшем разряде (дополненный код), что позволяет делить на 2 знаковые числа. Например, если байт 0x80 (-128) сдвинуть вправо на 1 разряд, то получится байт 0xC0 (-64).
op1 &= bitw;

for (i = 0; i < op2; i++) {

    msb = op1 & bits;
    cf  = op1 & 1;
    op1 = ((op1 >> 1) | msb);
}

flags.o = 0;
flags.s = !!(op1 & bits);
flags.z = !op1;
flags.p = parity(op1);
flags.c = cf;
Здесь msb - это старший разряд, который остается на месте при сдвиге вправо. В SAR флаги устанавливаются точно так же, как и SHR, кроме OF, который всегда сброшен и равен 0.

§ Реализация сдвигов

Теперь пришло время реализовывать сдвиги. На самом деле, это очень просто.
case 0xC0: case 0xC1: // [op] rm, i8

    i_size = opcode_id & 1;
    put_rm(i_size, shiftlogic(i_reg, i_size, get_rm(i_size), fetch(1)));
    break;
Сначала выбирается i_size (8 или 16 бит), после чего, основываясь на значении i_reg из modrm, берется операнд из rm-части нужного размера, считывается следующий байт, который будет равен количеству сдвигов, и выполняется функция shiftlogic. Результат записывается обратно в rm.
case 0xD0: case 0xD1: // [op] rm, 1
case 0xD2: case 0xD3: // [op] rm, cl

    i_size = opcode_id & 1;
    put_rm(i_size, shiftlogic(i_reg, i_size, get_rm(i_size), opcode_id & 2 ? regs[REG_CL] : 1));
    break;
Второй набор сдвигов работает аналогично первому, но в качестве второго операнда выбирается либо 1 (групповые инструкции D0h, D1h), либо значение в регистре CL (D2h, D3h).
И на этом всё. В процессоре есть еще сдвиговые инструкции SHRD, SHLD, но они находятся уже в расширенных опкодах и реализованы точно не для x8086, они сейчас не требуются.
Исходный код, как обычно, находится по ссылке.
Следующий материал
2 окт, 2020
© 2007-2022 Все права весьма зарезервированы