Машинно-независимый Ассемблер RTL и Ассемблер Intel 80x86. Внешние устройства и прерывания. Виртуальная память и поддержка параллельных задач
Примеры программ на RTL и Ассемблере Intel 80x86
Рассмотрим несколько простых примеров программ на "виртуальном Ассемблере" RTL и на конкретном Ассемблере для процессора Intel 80x86.
Вычисление наибольшего общего делителя
Реализуем функцию, вычисляющую наибольший общий делитель двух целых чисел. Мы уже записывали алгоритм вычисления НОД на псевдокоде. На языке Си эта функция выглядит следующим образом:
int gcd(int x, int y) { // цел алг. gcd(цел x, цел y) int a, b, r; // | цел a, b, r; a = x; b = y; // | a := x; b := y; while (b != 0) { // | цикл пока b != 0 r = a % b; // | | r := a % b; a = b; // | | a := b; b = r; // | | b := r; } // | конец цикла return a; // | ответ := a; } // конец алгоритма
Вместо НОД мы назвали функцию " gcd " (от слов greatest common divisor), поскольку в языке Си русские буквы в именах функций и переменных использовать нельзя. Запишем эту программу на языке RTL. Переменные a, b, r мы будем хранить в регистрах R0, R1, R2.
// Вход в функцию: push FP; // сохраним значение FP в стеке; FP := SP; // определим новое значение FP; push R1; // сохраним значения регистров R1 push R2; // и R2 // R0 := m[FP+8]; // a := x; R1 := m[FP+12]; // b := y; L1: // метка начала цикла CC0 := R1 - 0; // сравнить b с нулем if (eq) goto L2; // если результат равен нулю, // то перейти на метку L2 R2 := R0 % R1; // r := a % b; R0 := R1; // a := b; R1 := R2; // b := r; goto L1 // перейти на метку L1 L2: // метка конца цикла // ответ уже содержится в R0 // выход из функции: pop R2; // восстановим значения R2 pop R1; // и R1 pop FP; // восстановим значение FP return; // вернемся в вызывающую программу
Эту программу можно переписать на конкретный Ассемблер, например, на Ассемблер "Masm" фирмы Microsoft для процессоров Intel 80x86. Первое, что надо сделать при переводе с RTL на Ассемблер — это распределить регистры, т.е. задать отображение виртуальных регистров R0, R1, ... на конкретные регистры данного процессора. У процессоров серии Intel 80x86 есть всего 8 общих регистров: это регистры
EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP.
Процессор Intel сконструирован таким образом, что каждый регистр выполняет в определенных командах свою особую роль (Intel 80x86 — это CISC-процессор; в RISC-процессорах все регистры равноправны). В частности, команда деления всегда использует в качестве делимого длинное восьмибайтовое целое число, содержащееся в паре регистров (EDX, EAX), где старшие байты в регистре EDX. В результате выполнения команды деления вычисляется как частное, так и остаток от деления: частное помещается в регистр EAX, остаток — в регистр EDX.
В данной программе на языке RTL остаток от деления помещается в регистр R2. Поэтому регистр R2 удобно отобразить на регистр EDX, это позволит избежать лишних пересылок результата из одного регистра в другой. Итак, зафиксируем следующее распределение регистров:
R0 — EAX R1 — EBX R2 — EDX FP — EBP SP — ESP
После того как распределены регистры, остается только переписать каждую строку RTL программы на конкретный Ассемблер. Для этого необходимо знать ограниченный набор команд, реализующих операции языка RTL в конкретном Ассемблере. Например, в нашем случае операция пересылки из одного регистра в другой или из памяти в регистр реализуется командой mov, операция деления реализуется командой div и т.д. Программа на языке Ассемблера Intel 80386 записывается следующим образом:
.386 .model flat, stdcall .code gcd: ; Вход в функцию: push EBP ; сохраним старое значение EBP mov EBP, ESP ; определим новое значение EBP push EBX ; сохраним значения EBX push EDX ; и EDX. ; mov EAX, [EBP+8] ; EAX := x mov EBX, [EBP+12] ; EBX := y L1: ; метка начала цикла cmp EBX, 0 ; сравнить EBX с нулем je L2 ; если результат равен нулю, ; то перейти на метку L2 mov EDX, 0 ; div EBX ; EDX := EAX % EBX mov EAX, EBX ; EAX := EBX mov EBX, EDX ; EBX := EDX jmp L1 ; перейти на метку L1 L2: ; метка конца цикла ; ответ уже содержится в EAX ; выход из функции: pop EDX ; восстановим значения EDX pop EBX ; и EBX pop EBP ; восстановим значение EBP ret ; возврат из функции public gcd end
Суммирование массива
Рассмотрим еще один простой пример программы на Ассемблере. Требуется вычислить сумму элементов целочисленного массива заданной длины. Прототип этой функции на языке Си выглядит следующим образом:
int sum(int a[], int n);
Функции передается (как обычно, через аппаратный стек) адрес начала целочисленного массива a и его длина n. На RTL функция sum записывается следующим образом:
// Вход в функцию: push FP; // сохраним старое значение FP; FP := SP; // определим новое значение FP; push R1; // сохраним значения регистров R1, push R2; // R2 push R3; // и R3. // R0 := 0; // обнулим сумму R1 := m[FP+8]; // R1 := a (адрес начала массива) R2 := m[FP+12]; // R2 := n (число эл-тов массива) L1: // метка начала цикла CC0 := R2 - 0; // сравнить R2 с нулем if (le) goto L2; // если результат < = 0, // то перейти на метку L2 R3 := m[R1]; // R3 := очередной элемент массива R0 := R0 + R3; // прибавим очередной эл-т к сумме R1 := R1 + 4; // увеличим адрес очер. эл-та на 4 R2 := R2 - 1; // уменьшим счетчик необр. эл-тов goto L1 // перейти на метку L1 L2: // метка конца цикла // ответ уже содержится в R0 // выход из функции: pop R3; // восстановим значения R3, pop R2; // R2 pop R1; // и R1 pop FP; // восстановим старое значение FP return; // вернемся в вызывающую программу
В этой программе адрес очередного элемента массива содержится в регистре R1. Сумма просмотренных элементов массива накапливается в регистре R0. Регистр R2 содержит число еще не обработанных элементов массива. В начале программы в регистр R1 записывается адрес начала массива, а в R2 —число элементов массива. В теле цикла очередной элемент массива читается из памяти и помещается в регистр R3, затем содержимое R3 прибавляется к сумме R0. После каждого выполнения тела цикла адрес очередного элемента увеличивается на 4 (т.к. целое число занимает 4 байта), а количество необработанных элементов уменьшается на единицу. Цикл продолжается, пока содержимое регистра R2 (т.е. число необработанных элементов) больше нуля.
Для переписывания программы на Ассемблер Intel 80386 зафиксируем следующее распределение виртуальных регистров:
R0 — EAX R1 — EBX R2 — ECX R3 — EDX FP — EBP SP — ESP
Программа переписывается таким образом:
.386 .model flat, stdcall .code sum: ; Вход в функцию: push EBP ; сохраним старое значение EBP mov EBP, ESP ; определим новое значение EBP push EBX ; сохраним значения регистров EBX, push ECX ; ECX push EDX ; и EDX. ; mov EAX, 0 ; EAX := 0 mov EBX, [EBP+8] ; EBX := a mov ECX, [EBP+12]; ECX := n L1: ; метка начала цикла cmp ECX, 0 ; сравнить ECX с нулем jle L2 ; если результат < = 0, ; то перейти на метку L2 mov EDX, [EBX] ; EDX := очередной эл-т массива add EAX, EDX ; EAX := EAX+EDX add EBX, 4 ; EBX := EBX+4 (адрес след. эл-та) dec ECX ; ECX := ECX-1 (счетчик) jmp L1 ; перейти на метку L1 L2: ; метка конца цикла ; ответ содержится в регистре EAX ; выход из функции: pop EDX ; восстановим значения EDX, pop ECX ; ECX pop EBX ; и EBX. pop EBP ; восстановим значение EBP ret ; вернемся в вызывающую программу public sum end
Отметим, что мы использовали команду уменьшения значения регистра на единицу dec (от слова decrement) для реализации следующей строки RTL:
R2 := R2 - 1; // уменьшим счетчик необр. эл-тов
В Ассемблере Intel 80386 она записывается как
dec ECX; ECX := ECX-1
Команда увеличения регистра на единицу обычно записывается как inc (от слова increment). Эти команды, как правило, присутствуют в наборе инструкций любого процессора.