§ Что такое стек
Стек (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 разрядными регистрами. Рассмотрю сначала самые простые наборы инструкции.// PUSH r16 case 0x50: case 0x51: case 0x52: case 0x53: case 0x54: case 0x55: case 0x56: case 0x57: push(regs16[opcode_id & 7]); break; // POP r16 case 0x58: case 0x59: case 0x5A: case 0x5B: case 0x5C: case 0x5D: case 0x5E: case 0x5F: regs16[opcode_id & 7] = pop(); break;Этот код очень простой: здесь выбирается 16 битный регистр и либо из него пишется в стек, либо извлекается из стека в регистр. Аналогично для сегментных регистров:
// PUSH sreg case 0x06: case 0x0E: case 0x16: case 0x1E: push(regs16[REG_ES + ((opcode_id >> 3) & 3)]); break; // POP sreg case 0x07: case 0x17: case 0x1F: regs16[REG_ES + ((opcode_id >> 3) & 3)] = pop(); break;Единственное отличие, что номер сегментного регистра находится в битах 4:3 (всего 4 регистра), поэтому выбор опкода
(opcode>>3)&3
такой необычный.Реализуем, пожалуй, еще 2 инструкции, не принадлежащих к 8086
// PUSH imm16 case 0x68: push(fetch(2)); break; // PUSH imm8 case 0x6A: i_tmp = fetch(1); push(i_tmp & 0x80 ? i_tmp | 0xFF00 : i_tmp); break;Второй PUSH i8 требуется пояснить. Читается байт из непосредственного операнда. Если старший бит равен 1, то этот байт расширяется - в старших 8 битах тоже устанавливается все единицы, иначе там остаются нули. Тем самым образом это может позволить сэкономить на занимаемом программой месте.
И еще есть возможность выполнить инструкцию POP через modrm:
// POP rm case 0x8F: i_tmp = pop(); put_rm(1, i_tmp); break;Извлекается число из стека, и записывается в регистр или память. А вот инструкция PUSH rm находится уже в групповой инструкции FFh под номером i_reg = 6:
// Групповая инструкция FFh 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
Сами по себе эти инструкции работают тривиально// PUSHF case 0x9C: push(get_flags()); break; // POPF case 0x9D: set_flags(pop()); break;Но были написаны 2 процедуры для корректного получения флагов
uint16_t get_flags() { return /* 0 */ (!!flags.c) | /* 1 */ 0x02 | /* 2 */ (!!flags.p<<2) | /* 3 */ 0 | /* 4 */ (!!flags.a<<4) | /* 5 */ 0 | /* 6 */ (!!flags.z<<6) | /* 7 */ (!!flags.s<<7) | /* 8 */ (!!flags.t<<8) | /* 9 */ (!!flags.i<<9) | /* 10 */ (!!flags.d<<10) | /* 11 */ (!!flags.o<<11); }И корректного сохранения данных во флаги
void set_flags(uint16_t data) { // basic flags.c = !!(data & 0x01); flags.p = !!(data & 0x04); flags.a = !!(data & 0x10); flags.z = !!(data & 0x40); flags.s = !!(data & 0x80); // extend flags.t = !!(data & 0x100); flags.i = !!(data & 0x200); flags.d = !!(data & 0x400); flags.o = !!(data & 0x800); }Исходные коды прикреплены в архиве.
Следующий материал