§ Что такое стек

Стек (stack) - это одна из ключевых технологии для любого процессора. Стек позволяет сохранять временные данные для процедур и не только. Стек организован приблизительно говоря, как гора посуды - первую тарелку положишь, последней возьмешь, то есть если положить тарелку, потом на нее еще тарелку, и еще, то если тарелку надо будет забрать, то будет взята именно последняя положенная тарелка, потом предпоследняя, а потом первая. Такой принцип называется LIFO (Last In First Out - первым пришел, последним ушел).
В процессоре стек организован в виде указателя на память в каком-то регистре. В процессоре x86 это регистр SP (или расширенный для 32 бит ESP, 64 бит RSP). Он указывает на последний элемент, а конкретно 2/4/8 байта (машинное слово). При вставке значения в стек SP уменьшается, а при извлечении он увеличивается.
1// Вставить в стек
2void push(uint16_t data) {
3
4    regs16[REG_SP] -= 2;
5    wr(SEGREG(REG_SS, regs16[REG_SP]), data, 2);
6}
7
8// Извлечь из стека
9uint16_t pop() {
10
11    uint16_t tw = rd(SEGREG(REG_SS, regs16[REG_SP]), 2);
12    regs16[REG_SP] += 2;
13    return tw;
14}
В процедуре push, перед тем, как данные будут записаны в память по адресу SS:SP, сам указатель стека уменьшается на 2 (word), и уже потом записывается. А pop же наоборот, сначала читаются данные из указателя SS:SP, а потом к SP добавляется 2.

§ Реализация PUSH и POP

В процессоре есть такие инструкции, как PUSH и POP. Они обычно работают с 16 разрядными регистрами. Рассмотрю сначала самые простые наборы инструкции.
1// PUSH r16
2case 0x50: case 0x51: case 0x52: case 0x53:
3case 0x54: case 0x55: case 0x56: case 0x57:
4
5    push(regs16[opcode_id & 7]);
6    break;
7
8// POP r16
9case 0x58: case 0x59: case 0x5A: case 0x5B:
10case 0x5C: case 0x5D: case 0x5E: case 0x5F:
11
12    regs16[opcode_id & 7] = pop();
13    break;
Этот код очень простой: здесь выбирается 16 битный регистр и либо из него пишется в стек, либо извлекается из стека в регистр. Аналогично для сегментных регистров:
1// PUSH sreg
2case 0x06: case 0x0E: case 0x16: case 0x1E:
3
4    push(regs16[REG_ES + ((opcode_id >> 3) & 3)]);
5    break;
6
7// POP sreg
8case 0x07: case 0x17: case 0x1F:
9
10    regs16[REG_ES + ((opcode_id >> 3) & 3)] = pop();
11    break;
Единственное отличие, что номер сегментного регистра находится в битах 4:3 (всего 4 регистра), поэтому выбор опкода (opcode>>3)&3 такой необычный.
Реализуем, пожалуй, еще 2 инструкции, не принадлежащих к 8086
1// PUSH imm16
2case 0x68: push(fetch(2)); break;
3
4// PUSH imm8
5case 0x6A: i_tmp = fetch(1); push(i_tmp & 0x80 ? i_tmp | 0xFF00 : i_tmp); break;
Второй PUSH i8 требуется пояснить. Читается байт из непосредственного операнда. Если старший бит равен 1, то этот байт расширяется - в старших 8 битах тоже устанавливается все единицы, иначе там остаются нули. Тем самым образом это может позволить сэкономить на занимаемом программой месте.
И еще есть возможность выполнить инструкцию POP через modrm:
1// POP rm
2case 0x8F: i_tmp = pop(); put_rm(1, i_tmp); break;
Извлекается число из стека, и записывается в регистр или память. А вот инструкция PUSH rm находится уже в групповой инструкции FFh под номером i_reg = 6:
1// Групповая инструкция FFh
2case 6: push(get_rm(1)); break;
Она тоже очень простая. Просто извлекается из rm-части операнд и записывается в стек. Вот как по мне, сделано плохо. Лучше бы вместо POP rm использовали i_reg=7, который сейчас undefined instruction и освободили бы место для инструкции 0x8F, поскольку там modrm, у которого i_reg часть вообще никакой роли не играет. Это явно недоработка инженегрии Интола.

§ PUSHA, POPA

Эти инструкции не для 8086, но я все равно сделаю их реализацию, тем более это и несложно. Удивительный факт - они работают только для процессоров старше 186, но в 64-х битном режиме их уже нет.
Приведу код для PUSHA:
1case 0x60:
2
3    i_tmp = regs16[REG_SP];
4    for (int i = 0; i < 8; i++)
5        push(i == REG_SP ? i_tmp : regs16[i]);
6
7    break;
Перед операцией сохраняется в i_tmp значение регистра SP, что обязательно, а потом последовательно вставляются в стек 8 регистров общего назначения. Если номер регистра REG_SP, то вставляется предыдущее значение SP.
Код POPA немного посложнее:
1case 0x61:
2
3    for (int i = 7; i >= 0; i--) {
4        if (i == REG_SP) i_tmp = pop();
5        else regs16[i] = pop();
6    }
7    regs16[REG_SP] = i_tmp;
8    break;
Регистры извлекаются в обратном порядке. При извлечении регистра SP, он записывается в i_tmp, и потом, после операции, уже записывается в SP.

§ PUSHF, POPF

Сами по себе эти инструкции работают тривиально
1// PUSHF
2case 0x9C: push(get_flags()); break;
3
4// POPF
5case 0x9D: set_flags(pop()); break;
Но были написаны 2 процедуры для корректного получения флагов
1uint16_t get_flags() {
2
3    return
4    /* 0 */ (!!flags.c) |
5    /* 1 */ 0x02 |
6    /* 2 */ (!!flags.p<<2) |
7    /* 3 */ 0 |
8    /* 4 */ (!!flags.a<<4) |
9    /* 5 */ 0 |
10    /* 6 */ (!!flags.z<<6) |
11    /* 7 */ (!!flags.s<<7) |
12    /* 8 */ (!!flags.t<<8) |
13    /* 9 */ (!!flags.i<<9) |
14    /* 10 */ (!!flags.d<<10) |
15    /* 11 */ (!!flags.o<<11);
16}
И корректного сохранения данных во флаги
1void set_flags(uint16_t data) {
2
3    // basic
4    flags.c = !!(data & 0x01);
5    flags.p = !!(data & 0x04);
6    flags.a = !!(data & 0x10);
7    flags.z = !!(data & 0x40);
8    flags.s = !!(data & 0x80);
9    // extend
10    flags.t = !!(data & 0x100);
11    flags.i = !!(data & 0x200);
12    flags.d = !!(data & 0x400);
13    flags.o = !!(data & 0x800);
14}
Исходные коды прикреплены в архиве.
Следующий материал