Опубликован: 01.03.2016 | Доступ: свободный | Студентов: 468 / 20 | Длительность: 03:55:00
Лекция 5:

Команды ассемблера

Размер элементов цепочки, которые обрабатывают эти команды, зависит от использованного суффикса команды.

Команда movs выполняет копирование одного элемента из цепочки-источника в цепочку-приёмник.

Команда cmps выполняет сравнение элемента из цепочки-источника и цепочки-приёмника (фактически, как и cmp, выполняет вычитание, источник - приёмник, результат никуда не записывается, но флаги устанавливаются).

Команда scas предназначена для поиска определённого элемента в цепочке. Она сравнивает содержимое регистра %eax/%ax/%al и содержимое элемента цепочки (выполняется вычитание %eax/%ax/%al - элемент_цепочки, результат не записывается, но флаги устанавливаются). Адрес цепочки должен быть помещён в регистр %edi.

После того, как эти команды выполнили своё основное действие, они увеличивают/уменьшают индексные регистры на размер элемента цепочки. Подчеркну тот факт, что эти команды обрабатывают только один элемент цепочки. Таким образом, нужно организовать что-то вроде цикла для обработки всей цепочки. Для этих целей существуют префиксы команд:

 rep
 repe/repz
 repne/repnz

Эти префиксы ставятся перед командой, например: repe scas. Префикс организовывает как бы цикл из одной команды, при этом с каждым шагом цикла значение регистра %ecx автоматически уменьшается на 1.

  • rep повторяет команду, пока %ecx не равен нулю.
  • repe (или repz - то же самое) повторяет команду, пока %ecx не равен нулю и установлен флаг zf. Анализируя значение регистра %ecx, можно установить точную причину выхода из цикла: если %ecx равен нулю, значит, zf всегда был установлен, и вся цепочка пройдена до конца, если %ecx больше нуля - значит, флаг zf в какой-то момент был сброшен.
  • repne (или repnz - то же самое) повторяет команду, пока %ecx не равен нулю и не установлен флаг zf.

Также следует указать команды для управления флагом df:

 cld
 std

cld (CLear Direction flag) сбрасывает флаг df.

std (SeT Direction flag) устанавливает флаг df.

Пример: memcpy

Вооружившись новыми знаниями, попробуем заново изобрести функцию memcpy(3):

.data
printf_format:
        .string  "%s\n "

str_in:
        .string  "abc123()!@!777 "
        .set str_in_length, .-str_in

.bss
str_out:
        .space str_in_length

.text

/* void *my_memcpy(void *dest, const void *src, size_t n); */

my_memcpy:
        pushl %ebp
        movl  %esp, %ebp

        pushl %esi
        pushl %edi

        movl  8(%ebp), %edi     /* цепочка-назначение                */
        movl  12(%ebp), %esi    /* цепочка-источник                  */
        movl  16(%ebp), %ecx    /* длина                             */

        rep movsb

        movl  8(%ebp), %eax     /* вернуть dest                      */

        popl  %edi
        popl  %esi

        movl  %ebp, %esp
        popl  %ebp
        ret

.globl main
main:
        pushl %ebp
        movl  %esp, %ebp

        pushl $str_in_length
        pushl $str_in
        pushl $str_out
        call  my_memcpy

        pushl $str_out
        pushl $printf_format
        call  printf

        movl  $0, %eax

        movl  %ebp, %esp
        popl  %ebp
        ret

Вы, наверно, будете удивлены, если я вам скажу, что эта реализация memcpy всё равно не самая быстрая. "Что ещё можно сделать? " - спросите вы. Ведь мы можем копировать данные не по одному байту, а по целых 4 байта за раз при помощи movsl. Тогда у нас получается приблизительно такой алгоритм: копируем как можно больше данных блоками по 4 байта, после этого остаётся хвостик в 0, 1, 2 или 3 байта; этот остаток можно скопировать при помощи movsb. Поэтому нашу memcpy лучше переписать вот так:

/* void *my_memcpy(void *dest, const void *src, size_t n); */

my_memcpy:
        pushl %ebp
        movl  %esp, %ebp

        pushl %esi
        pushl %edi

        movl  8(%ebp), %edi     /* цепочка-назначение                */
        movl  12(%ebp), %esi    /* цепочка-источник                  */
        movl  16(%ebp), %edx    /* длина                             */

        movl  %edx, %ecx
        shrl  $2, %ecx          /* делить на 2^2 = 4; теперь в 
                                   находится %ecx количество 4-байтных 
                                   кусочков                          */
        rep movsl

        movl  %edx, %ecx
        andl  $3, %ecx          /* $3 == $0b11, оставить только два 
                                   младших бита, то есть остаток от 
                                   деления на 4                      */
        jz    1f                /* если результат 0, пропустить 
                                   цепочечную команду                */
        rep movsb
1:

        movl  8(%ebp), %eax     /* вернуть dest                      */

        popl  %edi
        popl  %esi

        movl  %ebp, %esp
        popl  %ebp
        ret

Пример: strlen

Теперь strlen: нам нужно сравнить каждый байт цепочки с 0, остановиться, когда найдём 0, и вернуть количество ненулевых байт. Как счетчик мы будем использовать регистр %ecx, который автоматически изменяют все префиксы. Но префиксы уменьшают счетчик и прекращают выполнение команды, когда %ecx равен 0. Поэтому перед цепочечной командой мы поместим в %ecx число 0xffffffff, и этот регистр будет уменьшатся в ходе выполнения цепочечной команды. Результат получится в обратном коде, поэтому мы используем команду not для инвертирования всех битов. И после этого ещё уменьшим результат на 1, так как нулевой байт тоже был посчитан.

.data
printf_format:
        .string  "%u\n "

str_in:
        .string  "abc123()!@!777 "

.text

/* size_t my_strlen(const char *s); */

my_strlen:
        pushl %ebp
        movl  %esp, %ebp

        pushl %edi

        movl  8(%ebp), %edi             /* цепочка */

        movl  $0xffffffff, %ecx
        xorl  %eax, %eax                /* %eax = 0 */

        repne scasb

        notl  %ecx
        decl  %ecx

        movl  %ecx, %eax

        popl  %edi

        movl  %ebp, %esp
        popl  %ebp
        ret

.globl main
main:
        pushl %ebp
        movl  %esp, %ebp

        pushl $str_in
        call  my_strlen

        pushl %eax
        pushl $printf_format
        call  printf

        movl  $0, %eax

        movl  %ebp, %esp
        popl  %ebp
        ret

Как реализованы другие стандартные цепочечные функции, можно посмотреть, например, в исходных кодах ядра Linux в файлах /usr/src/linux/arch/x86/include/asm/string_*.h,/usr/src/linux/arch/x86/lib/{mem*,str*}. Оттуда взяты все примеры для этого раздела.

В заключение обсуждения цепочечных команд нужно сказать следующее: не следует заново изобретать стандартные функции, как мы это только что сделали. Это всего лишь пример и объяснение принципов их работы. В реальных программах используйте цепочечные команды, только когда они реально смогут помочь при нестандартной обработке цепочек, а для стандартных операций лучше вызывать библиотечные функции.

Конструкция switch

Оператор switch языка Си можно переписать на ассемблере разными способами. Рассмотрим несколько вариантов того, какими могут быть значения у case:

  • значения из определённого маленького промежутка (все или почти все), например, 23, 24, 25, 27, 29, 30;
  • значения, между которыми большие "расстояния " на числовой прямой, например, 5, 15, 80, 3800;
  • комбинированный вариант: 35, 36, 37, 38, 39, 1200, 1600, 7000.

Рассмотрим решение для первого случая. Вспомним, что команда jmp принимает адрес не только в виде непосредственного значения (метки), но и как обращение к памяти. Значит, мы можем осуществлять переход на адрес, вычисленный в процессе выполнения. Теперь вопрос: как можно вычислить адрес? А нам не нужно ничего вычислять, мы просто поместим все адреса case-веток в массив. Пользуясь проверяемым значением как индексом массива, выбираем нужный адрес case-ветки. Таким образом, процессор всё вычислит за нас. Посмотрите на следующий код:

.data
printf_format:
        .string  "%u\n "

.text
.globl main

main:
        pushl %ebp
        movl  %esp, %ebp

        movl  $1, %eax          /* получить в %eax некоторое 
                                   интересующее нас значение         */

                                /* мы предусмотрели случаи только для 
                                   0, 1, 3, поэтому,                 */
        cmpl  $3, %eax          /* если %eax больше 3 
                                   (как беззнаковое),                */
        ja    case_default      /* перейти к default                 */

        jmp   *jump_table(,%eax,4) /* перейти по адресу, содержащемуся 
                                   в памяти jump_table + %eax*4      */

.section .rodata
        .p2align 4
jump_table:                     /* массив адресов                    */
        .long case_0            /* адрес этого элемента массива: 
                                                     jump_table + 0  */
        .long case_1            /*                   jump_table + 4  */
        .long case_default      /*                   jump_table + 8  */
        .long case_3            /*                   jump_table + 12 */
.text

case_0:
        movl  $5, %ecx          /* тело case-блока                   */
        jmp   switch_end        /* имитация break - переход в конец 
                                   switch                            */

case_1:
        movl  $15, %ecx
        jmp   switch_end

case_3:
        movl  $35, %ecx
        jmp   switch_end

case_default:
        movl  $100, %ecx

switch_end:

        pushl %ecx              /* вывести %ecx на экран, выйти      */
        pushl $printf_format
        call  printf

        movl  $0, %eax

        movl  %ebp, %esp
        popl  %ebp
        ret

Этот код эквивалентен следующему коду на Си:

#include  <stdio.h >

int main()
{
  unsigned int a, c;

  a = 1;
  switch(a)
  {
    case 0:
      c = 5;
      break;

    case 1:
      c = 15;
      break;

    case 3:
      c = 35;
      break;

    default:
      c = 100;
      break;
  }

  printf( "%u\n ", c);
  return 0;
}
Алексей Силенок
Алексей Силенок
Россия, С-Пб, Высшая инженерная школа СПбПУ Петра Великого, 2020