§ Введение

Почему-то эти инструкции имеют какое-то странное мистическое значение для меня. Мне они кажутся очень сложными, хотя это не так. Пора развеять свои мифы. Работа со строками - это такие инструкции, которые перемещают, проверяют, загружают строки из одной области памяти в другую. Всего существует несколько разновидностей: перемещение, загрузка, запись, сравнение строк. Для этого обычно используются регистры SI (Source Index) и DI (Destination Index). Начнем с самой простой, с MOVS. Эта инструкция берет значение из памяти по адресу DS:SI и записывает его по адресу ES:DI, при этом увеличивая на +1 или уменьшая на -1 значения регистров SI и DI одновременно, это зависит от значения флага D. Если D=0, то будет +1, иначе -1.
Еще одной особенностью строк является то, что перед ними может стоять префикс REP, REPNZ, REPZ. Когда он там находится, то после того, как произошла обработка одного символа, уменьшается регистр CX на 1, и если CX=0 то происходит переход к следующей инструкции, а иначе инструкция повторяется снова. Но, есть такой нюанс - если REP есть, но CX=0, то такая инструкция вообще не выполнится.

§ Код автоинкремента

Напишем общую процедуру автоинкремента для строковых инструкции
1// Выполнить инкремент или декремент
2void incsi(int i_w) { regs16[REG_SI] += (flags.d ? -1 : 1) * (i_w + 1); }
3void incdi(int i_w) { regs16[REG_DI] += (flags.d ? -1 : 1) * (i_w + 1); }
4
5// Автоинкремент для строковых инструкции
6void autorep(int i_w, int flag_test) {
7
8    // Есть префикс REP, REPNZ, REPZ
9    if (i_rep) {
10
11        // Уменьшаем CX на 1
12        regs16[REG_CX]--;
13
14        // Проверка на REPNZ, REPZ
15        if (flag_test) {
16
17            // Если REPZ, но не ZERO, переход к следующей инструкции
18            if ((i_rep == REPZ)  && !flags.z)
19                return;
20
21            // Если REPNZ, но ZERO, переход к следующей инструкции
22            if ((i_rep == REPNZ) &&  flags.z)
23                return;
24        }
25
26        // Повтор инструкции
27        if (regs16[REG_CX]) reg_ip = start_ip;
28    }
29}
Думаю, именно из-за вот этой конструкции мне и казались эти вещи сложными. Итак, после инструкции MOVS к примеру, происходит увеличение SI,DI на +1 или на +2 байта (зависит от того, сколько байт переслали), или уменьшение (флаг D это и решает).
Потом, если у нас был префикс REP, то в любом случае уменьшается CX на 1 (если он ранее был более 0). Далее, если передан параметр что REPNZ и REPZ имеют значение, то выполняется такая проверка. Для SCAS, CMPS префикс REPZ и REPNZ имеет роль. После выполнения одной операции проверяется флаг ZF. Если ZF=0 и перед инструкцией префикс REPNZ, то инструкция может повториться, а иначе если ZF=1 а префикс REPNZ, то тогда инструкция завершается и переходит к следующей, не дожидаясь пока CX не станет равным 0.
В конце есть выражение reg_ip = start_ip, где start_ip - это адрес инструкции перед ее считыванием. Причем адрес указывает не на сам опкод, а именно на префикс, то есть перед сканированием опкода надо добавить строку start_ip = reg_ip.

§ MOVSx

Инструкция перемещения данных
1case 0xA4: case 0xA5:
2
3    if (i_rep && regs16[REG_CX] == 0) break;
4
5    i_size = opcode_id & 1;
6    i_tmp  = rd(SEGREG(segment_id, regs16[REG_SI]), 1 + i_size);
7    wr(SEGREG(REG_ES, regs16[REG_DI]), i_tmp, 1 + i_size);
8
9    incsi(i_size);
10    incdi(i_size);
11    autorep(i_size, 0);
12    break;
Перед инструкцией проверяется CX на 0, если есть REP префикс. Если CX=0, то инструкция не выполняется. Потом читается байт или слово из DS:SI (DS может быть заменен сегментным префиксом) и записывается по ES:DI. Далее вызывается процедура автоинкремента (декремента) SI, DI и уменьшения CX на 1.

§ STOSx

Инструкция записи регистра AL или AX в память.
1case 0xAA: case 0xAB:
2
3    if (i_rep && regs16[REG_CX] == 0) break;
4
5    i_size = opcode_id & 1;
6    wr(SEGREG(REG_ES, regs16[REG_DI]), regs16[REG_AX], 1 + i_size);
7
8    incdi(i_size);
9    autorep(i_size, 0);
10    break;
Здесь изменяется только регистр DI, в отличии от MOVSx. Все остальное работает так же как и в MOVSx.

§ LODSx

Загрузка значения из DS:SI в AL или AX.
1case 0xAC: case 0xAD:
2
3    if (i_rep && regs16[REG_CX] == 0) break;
4
5    i_size = opcode_id & 1;
6    i_tmp  = rd(SEGREG(segment_id, regs16[REG_SI]), 2);
7    if (i_size) regs16[REG_AX] = i_tmp; else regs[REG_AL] = i_tmp;
8
9    incsi(i_size);
10    autorep(i_size, 0);
11    break;
На самом деле префикс REP тут не требуется, поскольку все равно в AL/AX будет загружено последнее значение, но того требует инструкция, они тут все одинаково работают. Сегмент DS может быть перегружен. Читаются 2 байта из памяти, после чего значение помещается либо в AL, либо в AX. Далее все как обычно, инкремент или декремент SI, обработка REP.

§ CMPSx

Сравнение двух строк DS:SI и ES:DI. Эта инструкция опирается на операцию вычитания. Как и обычная строковая инструкция, она не работает при REP и CX=0, увеличивает и уменьшает SI/DI.
1case 0xA6: case 0xA7:
2
3    if (i_rep && regs16[REG_CX] == 0) break;
4
5    i_size = opcode_id & 1;
6    i_op1  = rd(SEGREG(segment_id, regs16[REG_SI]), 1 + i_size);
7    i_op2  = rd(SEGREG(REG_ES,     regs16[REG_DI]), 1 + i_size);
8    arithlogic(ALU_SUB, i_size, i_op1, i_op2);
9
10    incsi(i_size);
11    incdi(i_size);
12    autorep(i_size, 1);
13    break;
Первым делом читаются операнды 1 или 2 байта из памяти, потом производится над ними операция вычитания, устанавливаются флаги (результат в память не пишется). Все остальное как обычно, кроме одного - проверяется ZF. Перед инструкцией CMPSB или CMPSW нельзя поставить REP, тут либо REPNZ, либо REPZ. Если после выполнения CMPSx ZF=1 и перед инструкцией установлен REPZ, то такая инструкция снова повторяется (если конечно CX больше 0), и наоборот, если ZF=0 и префикс REPNZ, тоже повторяется - в противном случае последовательность завершается и процессор переходит на следующую инструкцию. При это CX может остаться не нулевым.
Эта инструкция очень полезна при сравнении строк в памяти. С ее помощью можно узнать, идентичны ли строки и если да, то насколько. Допустим, надо сравнить строки "BK0010" и "BK0011". Устанавливается, допустим CX=10.
1mov si, str1 ; BK0010
2mov di, str2 ; BK0011
3mov cx, 10
4repz cmpsb
Теперь что будет происходить:
  • Проверяются "B"="B", CX=9 (после декремента), флаг ZF=1, повторяем
  • "K" = "K", CX=8, ZF=1, повтор
  • "0" = "0", CX=7
  • "0" = "0", CX=6
  • "1" = "1", CX=5
  • "1" != "0", CX=4, ZF=0, завершается инструкция
Как видно, CX в конце все равно уменьшился, поэтому мы можем сказать, что длина первых совпадающих символов будет 10 (CX в начале) - 4(CX в конце) и вычитаем -1, то есть 10-4-1=5. Это значит, что первые 5 символов совпали, и SI и DI будут находится после этих не совпавших символов, получается, надо будет отодвинуть SI и DI назад, чтобы найти последний не совпавший символ. Такие вот дела.

§ SCASx

Инструкция SCASx это инструкция CMPSx, но под другим соусом. В качестве первого операнда выступает регистр AL или AX, в качестве второго - указатель на память ES:DI. Также, в отличии от CMPSx, увеличивается или уменьшается DI, а SI остается неизменным. Все остальное работает идентично CMPSx.
1case 0xAE: case 0xAF:
2
3    if (i_rep && regs16[REG_CX] == 0) break;
4
5    i_size = opcode_id & 1;
6    i_op1  = regs16[REG_AX];
7    i_op2  = rd(SEGREG(REG_ES, regs16[REG_DI]), 1 + i_size);
8    arithlogic(ALU_SUB, i_size, i_op1, i_op2);
9
10    incdi(i_size);
11    autorep(i_size, 1);
12    break;
Как видим, i_op1 принимает AX на вход, но это не имеет значения, так как в arithlogic будет передан i_size, над которым он будет работать.
Как и обычно, доступ к кодам можно получить здесь.
Следующий материал