Команды ассемблера
Программа: вычисление факториала
Теперь напишем рекурсивную функцию для вычисления факториала. Она основана на следующей формуле:
.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
Любой программист знает, что если существует очевидное итеративное (реализуемое при помощи циклов) решение задачи, то именно ему следует отдавать предпочтение перед рекурсивным. Итеративный алгоритм нахождения факториала даже проще, чем рекурсивный; он следует из определения факториала:
Говоря проще, нужно перемножить все числа от 1 до .
Функция - на то и функция, что её можно заменить, при этом не изменяя вызывающий код. Для запуска следующего кода просто замените функцию из предыдущей программы вот этой новой версией:
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 и может вычислить .
.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 до , которые располагаются по кругу. Счёт начинается с элемента 0. Каждый -й элемент удаляют. Счёт продолжается с элемента, следующего за удалённым. Напишите программу, выводящую список вычеркнутых элементов. Подсказка: используйте malloc(3) для получения байт памяти и занесите в каждый байт число 1 при помощи memset(3). Значение 1 означает, что элемент существует, значением 0 отмечайте удалённые элементы. При счете пропускайте удалённые элементы.
Системные вызовы
Программа, которая не взаимодействует с внешним миром, вряд ли может сделать что-то полезное. Вывести сообщение на экран, прочитать данные из файла, установить сетевое соединение - это всё примеры действий, которые программа не может совершить без помощи операционной системы. В Linux пользовательский интерфейс ядра организован через системные вызовы. Системный вызов можно рассматривать как функцию, которую для вас выполняет операционная система.
Теперь наша задача состоит в том, чтобы разобраться, как происходит системный вызов. Каждый системный вызов имеет свой номер. Все они перечислены в файле /usr/include/asm-i386/unistd.h.
Системные вызовы считывают свои параметры из регистров. Номер системного вызова нужно поместить в регистр %eax. Параметры помещаются в остальные регистры в таком порядке:
- первый - в %ebx;
- второй - в %ecx;
- третий - в %edx;
- четвертый - в %esi;
- пятый - в %edi;
- шестой - в %ebp.
Таким образом, используя все регистры общего назначения, можно передать максимум 6 параметров. Системный вызов производится вызовом прерывания 0x80. Такой способ вызова (с передачей параметров через регистры) называется fastcall. В других системах (например, *BSD) могут применяться другие способы вызова.
Следует отметить, что не следует использовать системные вызовы везде, где только можно, без особой необходимости. В разных версиях ядра порядок аргументов у некоторых системных вызовов может отличаться, и это приводит к ошибкам, которые довольно трудно найти. Поэтому стоит использовать функции стандартной библиотеки Си, ведь их сигнатуры не изменяются, что обеспечивает переносимость кода на Си. Почему бы нам не воспользоваться этим и не "заложить фундамент " переносимости наших ассемблерных программ? Только если вы пишете маленький участок самого нагруженного кода и для вас недопустимы накладные расходы, вносимые вызовом стандартной библиотеки Си, - только тогда стоит использовать системные вызовы напрямую.
В качестве примера можете посмотреть код программы Hello world.
Структуры
Объявляя структуры в Си, вы не задумывались о том, как располагаются в памяти её элементы. В ассемблере понятия "структура " нет, зато есть "блок памяти ", его адрес и смещение в этом блоке. Объясню на примере:
Пусть этот блок памяти размером 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;