Опубликован: 01.03.2016 | Уровень: для всех | Доступ: платный
Лекция 5:

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

Программа: вычисление факториала

Теперь напишем рекурсивную функцию для вычисления факториала. Она основана на следующей формуле:

0!=1, n!=n /cdot ( b-1)!
.data
printf_format:
        .string  "%d\n "
 
.text
/* int factorial(int) */
factorial:
        pushl %ebp
        movl  %esp, %ebp
 
        /* извлечь аргумент в %eax */
        movl  8(%ebp), %eax
 
        /* факториал 0 равен 1 */
        cmpl  $0, %eax
        jne   not_zero
 
        movl  $1, %eax
        jmp   return
not_zero:
 
        /* следующие 4 строки вычисляют выражение
           %eax = factorial(%eax - 1) */
 
        decl  %eax
        pushl %eax
        call  factorial
        addl  $4, %esp
 
        /* извлечь в %ebx аргумент и вычислить %eax = %eax * %ebx */
 
        movl  8(%ebp), %ebx
        mull  %ebx
 
        /* результат в паре %edx:%eax, но старшие 32 бита нужно 
           отбросить, так как они не помещаются в int */
 
return:
        movl  %ebp, %esp
        popl  %ebp
        ret
 
.globl main
main:
        pushl %ebp
        movl  %esp, %ebp
 
        pushl $5
        call  factorial
 
        pushl %eax
        pushl $printf_format
        call  printf
 
        /* стек можно не выравнивать, это будет сделано
           во время выполнения эпилога */
 
        movl  $0, %eax                  /* завершить программу */
 
        movl  %ebp, %esp
        popl  %ebp
        ret

Любой программист знает, что если существует очевидное итеративное (реализуемое при помощи циклов) решение задачи, то именно ему следует отдавать предпочтение перед рекурсивным. Итеративный алгоритм нахождения факториала даже проще, чем рекурсивный; он следует из определения факториала: n!=1 /cdot 2 /cdot  /dots /cdot n

Говоря проще, нужно перемножить все числа от 1 до n.

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

factorial:
        movl  4(%esp), %ecx
 
        cmpl  $0, %ecx
        jne   not_zero
 
        movl  $1, %eax
        ret
 
not_zero:
 
        movl  $1, %eax
loop_start:
        mull  %ecx
        loop  loop_start
 
        ret

Что же здесь изменено? Рекурсия переписана в виде цикла. Кадр стека больше не нужен, так как в стек ничего не перемещается и другие функции не вызываются. Пролог и эпилог поэтому убраны, при этом регистр %ebp не используется вообще. Но если бы он использовался, сначала нужно было бы сохранить его значение, а перед возвратом восстановить.

Автор увлёкся процессом и написал 64-битную версию этой функции. Она возвращает результат в паре %eax:%edx и может вычислить 20!!=2432902008176640000.

.data
printf_format:
        .string  "%llu\n "
 
.text
.type   factorial, @function    /* long long int factorial(int)      */
factorial:
        movl  4(%esp), %ecx
 
        cmpl  $0, %ecx
        jne   not_zero
 
        movl  $1, %eax
        ret
 
not_zero:
 
        movl  $1, %esi          /* младшие 32 бита                   */
        movl  $0, %edi          /* старшие 32 бита                   */
loop_start:
        movl  %esi, %eax        /* загрузить младшие биты для 
                                   умножения                         */
        mull  %ecx              /* %eax:%edx = младшие биты * %ecx   */
        movl  %eax, %esi        /* записать младшие биты 
                                   обратно в %esi                    */
 
        movl  %edi, %eax        /* загрузить старшие биты            */
        movl  %edx, %edi        /* записать в %edi старшие биты 
                                   предыдущего умножения; теперь 
                                   результат умножения младших битов 
                                   находится в %esi:%edi, а старшие 
                                   биты - в %eax для следующего 
                                   умножения                         */
        mull  %ecx              /* %eax:%edx = старшие биты * %ecx   */
        addl  %eax, %edi        /* сложить полученный результат со 
                                   старшими битами предыдущего 
                                   умножения                         */
 
        loop  loop_start
 
        movl  %esi, %eax        /* результат вернуть в паре          */
        movl  %edi, %edx        /* %eax:%edx                         */
 
        ret
.size   factorial, .-factorial
 
.globl main
main:
        pushl %ebp
        movl  %esp, %ebp
 
        pushl $20
        call  factorial
 
        pushl %edx
        pushl %eax
        pushl $printf_format
        call  printf
 
        /* стек можно не выравнивать, это будет сделано во время 
           выполнения эпилога */
 
        movl  $0, %eax          /* завершить программу               */
 
        movl  %ebp, %esp
        popl  %ebp
        ret

Умножение 64-битного числа на 32-битное делается как при умножении "в столбик ":

     %edi       %esi
?               %ecx
---------------------
%edi?%ecx  %esi?%ecx
    A           |
   /|\          |
    +-- <-- <-- <--+
   старшие 32 бита

Но произведение %esi ? %ecx не поместится в 32 бита, останутся ещё старшие 32 бита. Их мы должны прибавить к старшим 32-м битам результата. Приблизительно так вы это делаете на бумаге в десятичной системе:

  2  5      25 ? 3 = 75
?    3
  ----
    15
+ 6
  ----
  7  5

Задание: напишите программу-считалочку. Есть числа от 0 до m, которые располагаются по кругу. Счёт начинается с элемента 0. Каждый n-й элемент удаляют. Счёт продолжается с элемента, следующего за удалённым. Напишите программу, выводящую список вычеркнутых элементов. Подсказка: используйте malloc(3) для получения m+1 байт памяти и занесите в каждый байт число 1 при помощи memset(3). Значение 1 означает, что элемент существует, значением 0 отмечайте удалённые элементы. При счете пропускайте удалённые элементы.

Системные вызовы

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

Теперь наша задача состоит в том, чтобы разобраться, как происходит системный вызов. Каждый системный вызов имеет свой номер. Все они перечислены в файле /usr/include/asm-i386/unistd.h.

Системные вызовы считывают свои параметры из регистров. Номер системного вызова нужно поместить в регистр %eax. Параметры помещаются в остальные регистры в таком порядке:

  1. первый - в %ebx;
  2. второй - в %ecx;
  3. третий - в %edx;
  4. четвертый - в %esi;
  5. пятый - в %edi;
  6. шестой - в %ebp.

Таким образом, используя все регистры общего назначения, можно передать максимум 6 параметров. Системный вызов производится вызовом прерывания 0x80. Такой способ вызова (с передачей параметров через регистры) называется fastcall. В других системах (например, *BSD) могут применяться другие способы вызова.

Следует отметить, что не следует использовать системные вызовы везде, где только можно, без особой необходимости. В разных версиях ядра порядок аргументов у некоторых системных вызовов может отличаться, и это приводит к ошибкам, которые довольно трудно найти. Поэтому стоит использовать функции стандартной библиотеки Си, ведь их сигнатуры не изменяются, что обеспечивает переносимость кода на Си. Почему бы нам не воспользоваться этим и не "заложить фундамент " переносимости наших ассемблерных программ? Только если вы пишете маленький участок самого нагруженного кода и для вас недопустимы накладные расходы, вносимые вызовом стандартной библиотеки Си, - только тогда стоит использовать системные вызовы напрямую.

В качестве примера можете посмотреть код программы Hello world.

Структуры

Объявляя структуры в Си, вы не задумывались о том, как располагаются в памяти её элементы. В ассемблере понятия "структура " нет, зато есть "блок памяти ", его адрес и смещение в этом блоке. Объясню на примере:

0x23 0x72 0x45 0x17

Пусть этот блок памяти размером 4 байта расположен по адресу 0x00010000. Это значит, что адрес байта 0x23 равен 0x00010000. Соответственно, адрес байта 0x72 равен 0x00010001. Говорят, что байт 0x72 расположен по смещению 1 от начала блока памяти. Тогда байт 0x45 расположен по смещению 2, а байт 0x17 - по смещению 3. Таким образом, адрес элемента = базовый адрес + смещение.

Приблизительно так в ассемблере организована работа со структурами: к базовому адресу структуры прибавляется смещение, по которому находится нужный элемент. Теперь вопрос: как определить смещение? В Си компилятор руководствуется следующими правилами:

  • Вся структура должна быть выровнена так, как выровнен её элемент с наибольшим выравниванием.
  • Каждый элемент находится по наименьшему следующему адресу с подходящим выравниванием. Если необходимо, для этого в структуру включается нужное число байт-заполнителей.
  • Размер структуры должен быть кратен её выравниванию. Если необходимо, для этого в конец структуры включается нужное число байт-заполнителей.

Примеры (внизу указано смещение элементов в байтах; заполнители обозначены XX):

struct     Выравнивание структуры: 1, размер: 1
{          +----+
  char c;  | c  |
};         +----+
           0

struct     Выравнивание структуры: 2, размер: 4
{          +----+----+----+----+
  char c;  | c  | XX |    s    |
  short s; +----+----+----+----+
};          0         2

struct     Выравнивание структуры: 4, размер: 8
{          +----+----+----+----+----+----+----+----+
  char c;  | с  | XX   XX   XX |         i         |
  int i;   +----+----+----+----+----+----+----+----+
};          0                   4

struct     Выравнивание структуры: 4, размер: 8
{          +----+----+----+----+----+----+----+----+
  int i;   |         i         | c  | XX   XX   XX |
  char c;  +----+----+----+----+----+----+----+----+
};          0                   4

struct     Выравнивание структуры: 4, размер: 12
{          +----+----+----+----+----+----+----+----+----+----+----+----+
  char c;  | c  | XX   XX   XX |         i         |    s    | XX   XX |
  int i;   +----+----+----+----+----+----+----+----+----+----+----+----+
  short s;  0                   4                   8
};

struct     Выравнивание структуры: 4, размер: 8
{          +----+----+----+----+----+----+----+----+
  int i;   |         i         | c  | XX |    s    |
  char c;  +----+----+----+----+----+----+----+----+
  short s;  0                   4         6
};

Обратите внимание на два последних примера: элементы структур одни и те же, только расположены в разном порядке. Но размер структур получился разный!

Программа: вывод размера файла

Напишем программу, которая выводит размер файла. Для этого потребуется вызвать функцию stat(2) и прочитать данные из структуры, которую она заполнит. man 2 stat:

 
STAT(2)                  Системные вызовы                  STAT(2)
 
ИМЯ
       stat, fstat, lstat - получить статус файла
 
КРАТКАЯ СВОДКА
       #include  <sys/types.h >
       #include  <sys/stat.h >
       #include  <unistd.h >
 
       int stat(const char *file_name, struct stat *buf);
 
ОПИСАНИЕ
       stat возвращает информацию о файле, заданном с помощью 
       file_name, и заполняет буфер buf.
 
       Все эти функции возвращают структуру stat, которая содержит 
       такие поля:
 
          struct stat {
              dev_t         st_dev;     /* устройство                */
              ino_t         st_ino;     /* индексный дескриптор      */
              mode_t        st_mode;    /* режим доступа             */
              nlink_t       st_nlink;   /* количество жестких ссылок */
              uid_t         st_uid;     /* идентификатор 
                                           пользователя-владельца    */
              gid_t         st_gid;     /* идентификатор 
                                           группы-владельца          */
              dev_t         st_rdev;    /* тип устройства (если это 
                                           устройство)               */
              off_t         st_size;    /* общий размер в байтах     */
              unsigned long st_blksize; /* размер блока ввода-вывода */
                                        /* в файловой системе        */
              unsigned long st_blocks;  /* количество выделенных 
                                           блоков                    */
              time_t        st_atime;   /* время последнего доступа  */
              time_t        st_mtime;   /* время последнего 
                                           изменения                 */
              time_t        st_ctime;   /* время последней смены 
                                           состояния                 */
          };

Так, теперь осталось только вычислить смещение поля st_size… Но что это за типы - dev_t, ino_t? Какого они размера? Следует заглянуть в заголовочный файл и узнать, что обозначено при помощи typedef. Я сделал так:

 
[user@host:~]$ cpp /usr/include/sys/types.h | less

Далее, ищу в выводе препроцессора определение dev_t, нахожу:

 typedef __dev_t dev_t; 

Ищу __dev_t:

 __extension__ typedef __u_quad_t __dev_t; 

Ищу __u_quad_t:

__extension__ typedef unsigned long long int __u_quad_t; 
Константин Белюстин
Константин Белюстин
Украина, г. Киев
Максим Барашков
Максим Барашков
Россия, Якутск