Оглавление
§ Что такое стек
Стек (stack) – это одна из ключевых технологии для любого процессора. Стек позволяет сохранять временные данные для процедур и не только. Стек организован приблизительно говоря, как гора посуды – первую тарелку положишь, последней возьмешь, то есть если положить тарелку, потом на нее еще тарелку, и еще, то если тарелку надо будет забрать, то будет взята именно последняя положенная тарелка, потом предпоследняя, а потом первая. Такой принцип называется LIFO (Last In First Out – первым пришел, последним ушел).
В процессоре стек организован в виде указателя на память в каком-то регистре. В процессоре x86 это регистр SP (или расширенный для 32 бит ESP, 64 бит RSP). Он указывает на последний элемент, а конкретно 2/4/8 байта (машинное слово). При вставке значения в стек SP уменьшается, а при извлечении он увеличивается.
void push(uint16_t data) {
regs16[REG_SP] -= 2;
wr(SEGREG(REG_SS, regs16[REG_SP]), data, 2);
}
uint16_t pop() {
uint16_t tw = rd(SEGREG(REG_SS, regs16[REG_SP]), 2);
regs16[REG_SP] += 2;
return tw;
}
В процедуре push, перед тем, как данные будут записаны в память по адресу SS:SP, сам указатель стека уменьшается на 2 (word), и уже потом записывается. А pop же наоборот, сначала читаются данные из указателя SS:SP, а потом к SP добавляется 2.
§ Реализация PUSH и POP
В процессоре есть такие инструкции, как PUSH и POP. Они обычно работают с 16 разрядными регистрами. Рассмотрю сначала самые простые наборы инструкции.
case 0x50: case 0x51: case 0x52: case 0x53:
case 0x54: case 0x55: case 0x56: case 0x57:
push(regs16[opcode_id & 7]);
break;
case 0x58: case 0x59: case 0x5A: case 0x5B:
case 0x5C: case 0x5D: case 0x5E: case 0x5F:
regs16[opcode_id & 7] = pop();
break;
Этот код очень простой: здесь выбирается 16 битный регистр и либо из него пишется в стек, либо извлекается из стека в регистр. Аналогично для сегментных регистров:
case 0x06: case 0x0E: case 0x16: case 0x1E:
push(regs16[REG_ES + ((opcode_id >> 3) & 3)]);
break;
case 0x07: case 0x17: case 0x1F:
regs16[REG_ES + ((opcode_id >> 3) & 3)] = pop();
break;
Единственное отличие, что номер сегментного регистра находится в битах 4:3 (всего 4 регистра), поэтому выбор опкода (opcode>>3)&3 такой необычный.
Реализуем, пожалуй, еще 2 инструкции, не принадлежащих к 8086
case 0x68: push(fetch(2)); break;
case 0x6A: i_tmp = fetch(1); push(i_tmp & 0x80 ? i_tmp | 0xFF00 : i_tmp); break;
Второй PUSH i8 требуется пояснить. Читается байт из непосредственного операнда. Если старший бит равен 1, то этот байт расширяется – в старших 8 битах тоже устанавливается все единицы, иначе там остаются нули. Тем самым образом это может позволить сэкономить на занимаемом программой месте.
И еще есть возможность выполнить инструкцию POP через modrm:
case 0x8F: i_tmp = pop(); put_rm(1, i_tmp); break;
Извлекается число из стека, и записывается в регистр или память. А вот инструкция PUSH rm находится уже в групповой инструкции FFh под номером i_reg = 6:
case 6: push(get_rm(1)); break;
Она тоже очень простая. Просто извлекается из rm-части операнд и записывается в стек. Вот как по мне, сделано плохо. Лучше бы вместо POP rm использовали i_reg=7, который сейчас undefined instruction и освободили бы место для инструкции 0x8F, поскольку там modrm, у которого i_reg часть вообще никакой роли не играет. Это явно недоработка инженегрии Интола.
§ PUSHA, POPA
Эти инструкции не для 8086, но я все равно сделаю их реализацию, тем более это и несложно. Удивительный факт – они работают только для процессоров старше 186, но в 64-х битном режиме их уже нет.
Приведу код для PUSHA:
case 0x60:
i_tmp = regs16[REG_SP];
for (int i = 0; i < 8; i++)
push(i == REG_SP ? i_tmp : regs16[i]);
break;
Перед операцией сохраняется в i_tmp значение регистра SP, что обязательно, а потом последовательно вставляются в стек 8 регистров общего назначения. Если номер регистра REG_SP, то вставляется предыдущее значение SP.
Код POPA немного посложнее:
case 0x61:
for (int i = 7; i >= 0; i--) {
if (i == REG_SP) i_tmp = pop();
else regs16[i] = pop();
}
regs16[REG_SP] = i_tmp;
break;
Регистры извлекаются в обратном порядке. При извлечении регистра SP, он записывается в i_tmp, и потом, после операции, уже записывается в SP.
§ PUSHF, POPF
Сами по себе эти инструкции работают тривиально
case 0x9C: push(get_flags()); break;
case 0x9D: set_flags(pop()); break;
Но были написаны 2 процедуры для корректного получения флагов
uint16_t get_flags() {
return
(!!flags.c) |
0x02 |
(!!flags.p<<2) |
0 |
(!!flags.a<<4) |
0 |
(!!flags.z<<6) |
(!!flags.s<<7) |
(!!flags.t<<8) |
(!!flags.i<<9) |
(!!flags.d<<10) |
(!!flags.o<<11);
}
И корректного сохранения данных во флаги
void set_flags(uint16_t data) {
flags.c = !!(data & 0x01);
flags.p = !!(data & 0x04);
flags.a = !!(data & 0x10);
flags.z = !!(data & 0x40);
flags.s = !!(data & 0x80);
flags.t = !!(data & 0x100);
flags.i = !!(data & 0x200);
flags.d = !!(data & 0x400);
flags.o = !!(data & 0x800);
}