Опубликован: 16.09.2005 | Уровень: для всех | Доступ: свободно | ВУЗ: Московский государственный университет имени М.В.Ломоносова
Лекция 7:

Машинно-независимый Ассемблер RTL и Ассемблер Intel 80x86. Внешние устройства и прерывания. Виртуальная память и поддержка параллельных задач

< Лекция 6 || Лекция 7: 1234 || Лекция 8 >

Примеры программ на 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). Эти команды, как правило, присутствуют в наборе инструкций любого процессора.

< Лекция 6 || Лекция 7: 1234 || Лекция 8 >
Кирилл Юлаев
Кирилл Юлаев
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?