Файл: 2005 Рудольф Марек. Ассемблер на примерах. Базовый курс. .pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 26.10.2023

Просмотров: 145

Скачиваний: 12

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Глава 12. Программирование в Linux Теперь осталось запустить программу
. / h e l l o
Hello, World!
12.8. Облегчим себе работу утилиты Asmutils
Asmutils — это коллекция системных программ, написанных на языке ассемблера. Эти программы работают непосредственно с ядром операционной системы и не требуют библиотеки LIBC.
Asmutils — идеальное решение для небольших встроенных систем или для спасательного диска. Программы написаны на ассемблере NASM и совместимы с х86-процессорами. Благодаря набору макросов, представляющих системные вызовы, Asmutils легко портируется на другие операционные системы (нужно модифицировать только конфигурационный файл. Поддерживаются следующие операционные системы BSD (FreeBSD, OpenBSD, NetBSD), UnixWare, Solaris и AtheOS. Для каждой операционной системы Asmutils определяет свой набор символических констант и макросов, побочным эффектом которых оказывается улучшение читабельности ассемблерных программ. В листинге 12.2 можно увидеть, как будет выглядеть наша программа hello с использованием Asmutils. Листинг 12.2. Программа «Hello World!» под Linux с использованием Asmutils
%include «system.inc»
CODESEG начало секции кода
START: начало программы для компоновщика sys_write STDOUT,hello,len этот макрос записывает в регистры аргументы для системного вызова write и вызывает write sys_exit 0 тоже самое для системного вызова exit
DATASEG начало секции данных hello db «Hello, World!»,Oxa len equ $-hello
END
199
Ассемблер на примерах. Базовый курс Теперь код нашей программы выглядит намного проще. После подстановки макросов этот код превратится в текст из предыдущего примера и работать будет точно также. Если вы хотите запустить эту же программу на FreeBSD, модифицируйте только файл MCONFIG. Домашняя страница Asmutils — http://asm.sourceforge.net/asmutils.html, там же вы сможете скачать последнюю версию Asmutils. Пакет Asmutils распространяется по Стандартной Общественной лицензии GNU, то есть бесплатно. Рекомендуем вам скачать Asmutils прямо сейчас, поскольку в последующих программах мы будем использовать именно этот пакет. Теперь давайте откомпилируем нашу программу. Убедитесь, что у вас установлен NASM (он входит в состав разных дистрибутивов — как совместимых, таки совместимых. Распакуйте файл asmutils-0.17.tar.gz. После распаковки получим обычную структуру каталогов — /doc, /src и /inc. Целевая платформа задается в файле MCONFIG. Он самодокументирован, так что если вам придется его немного подправить, то вы легко разберетесь, как именно. В каталоге /src есть файл Makefile. В первых строках этого файла указываются имена программ, подлежащих сборке (компиляции и компоновке. Пропишите в нем имя выходного файла — hello (без расширения .asm). После этого выполните команду make. В результате получите готовый исполняемый файл компоновщик запускать уже ненужно. Макросы Asmutils Аргументы системного вызова передаются как часть макроса. В зависимости от операционной системы генерируются соответствующие команды ассемблера, а аргументы заносятся в соответствующие регистры. Макросы для системных вызовов начинаются с префикса sys_, после этого следует имя системного вызова так, как оно пишется в странице. В Linux параметры передаются в том же порядке, в котором они описаны в страницах. Системный вызов возвращает значение в регистре ЕАХ. Макрос sys_exit 0 после подстановки превратится в следующие команды mov еах,1 номер системного вызова 1 — e x i t mov ebx,0 код возврата 0 i n t 0x80 вызываем ядро и завершаем программу Аргумент макроса необязательно должен быть непосредственным значением. Если вы храните код возврата в переменной rtn, то можете написать макрос sys_exit [rtn], и после подстановки он превратится в mov еах,1 номер системного вызова 1 — e x i t mov ebx,[rtn] код возврата — содержится в переменной r t n i n t 0x80 вызываем ядро и завершаем программу
200

Глава 12. Программирование в Linux Если регистр уже содержит требуемое значение, вы можете пропустить установку аргумента, используя ключевое слово EMPTY — тогда значение данного регистра останется как есть. Пример использования аргумента
EMPTY будет приведен в программе для копирования файлов.
1   ...   12   13   14   15   16   17   18   19   20

12.10. Операции файлового
ввода/вывода (I/O) При работе с файлами в Linux помните, что Linux — это подобная операционная система, и просто так доступ к файлам вы не получите у каждого файла есть права доступа, ау вашего процесса должны быть соответствующие полномочия. Открытие файла С помощью системного вызова ореп() мы можем не только открыть, но и создать файл, если это необходимо. Во второй секции справочной системы
Linux вы найдете полное описание этого вызова (man 2 open). Мы же рассмотрим его в общих чертах i n t openfconst char *pathname, i n t f l a g s ) ; i n t open(const char *pathname, i n t flags, mode_t mode); Первый аргумент системного вызова содержит указатель на имя файла (нуль- завершенную строку, второй аргумент — это массив битов, определяющий режим открытия файла (чтение/запись и др. Третий аргумент необязательный — он определяет права доступа для вновь создаваемого файла. Вызов возвращает дескриптор файла или отрицательное значение в случае ошибки. Символические названия битовых флагов представлены в таблице 12.2 (информация из страницы. Символические названия битовых флагов Таблица 12.2 Флаг

0_RDONLY
0_WRONLY
0_RDWR
0_CREAT
O_TRUNC
0_APPEND
0_LARGEFILE Режим открытия Открывает файл только для чтения Открывает файл только для записи Открывает файл для чтения и для записи Создает файл, если его не существует Обрезает файл до нулевой длины Открывает файл для дозаписи, то есть новые данные будут записаны вконец файла (опасно на NFS) Используется для обработки файлов размером более 4 Гб
201
Ассемблер на примерах. Базовый курс Если второй аргумент содержит флаг 0_CREAT, то третий аргумент обязателен. Он указывает, какие права доступа будут присвоены новому файлу. Для указания прав доступа можно комбинировать символические имена, из которых чаще всего используются следующие Флаг
S_RWXU
S_RGRP
S_ROTH Описание Владелец может читать, записывать и запускать файл Члены группы владельца могут читать файл Все остальные могут читать файл Откроем файл, имя которого хранится в переменной name, для чтения и записи sys_open name, 0_RDWR, EMPTY t e s t e a x , e a x проверяем ЕАХ на отрицательное значение js . e r r o r _ o p e n более подробно этот пример описывается в главе 7 Имя файла можно определить в секции данных с помощью псевдокоманды DB: name DB «my_file_which_has_a_very_long_name.txt»,0 Отдельные флаги могут комбинироваться с помощью оператора | (побитовое
OR). Откроем файл для чтения и записи, а если файла не существует, то создадим его, присвоив ему права доступа 700 (чтение/запись/выполнение для владельца, ничего для остальных sys_open name, 0_RDWR I 0_CREAT, S_IRWXU t e s t e a x , e a x js . e r r o r _ o p e n переходим к e r r o r _ o p e n , если произошла ошибка
. . . в случае успеха в ЕАХ будет дескриптор файла Закрытие файла Как ив, файл нужно закрыть с помощью системного вызова. Этот вызов называется close(), а соответствующий макрос — sys_close. Этот макрос требует одного аргумента — дескриптора закрываемого файла. Если дескриптор файла находится в ЕАХ, то для закрытия файла воспользуемся макросом sys_close eax Чтение из файла Данные из файла читаются блоками различной длины. Если явно не указано иное, то чтение данных начинается стой позиции, на которой закончилась предыдущая операция чтения или записи, то есть последовательно. Для чтения данных из файла используется системный вызов read:
202
Глава 12. Программирование в Linux ssize_t read(int fd, void *buf, size_t count); Первый аргумент — дескриптор файла, второй содержит указатель на область памяти, в которую будут записаны прочитанные данные, а третий — максимальное количество байтов, которые нужно прочитать. Вызов возвращает либо количество прочитанных байтов, либо отрицательное число — код ошибки. Макрос sys_read можно также использовать для чтения данных с клавиатуры, если в качестве файлового дескриптора передать STDIN — дескриптор потока стандартного ввода. Запись в файл Функция записи требует таких же аргументов, как и функция чтения дескриптора файла, буфера сданными, которые нужно записать в файл, и количество байтов, которые нужно записать. Вызов возвращает либо количество записанных байтов, либо отрицательное число — код ошибки ssize_t write(int fd, const void *buf, size_t count); Давайте напишем простую программу, читающую последовательность символов с клавиатуры до нажатия клавиши «Enter» и преобразующую их в верхний регистр. Мы ограничимся только латинским алфавитом, то есть первой половиной таблицы ASCII. Работать программа будет в бесконечном цикле — прочитали, преобразовали, вывели, опять прочитали — а прервать ее работу можно будет нажатием комбинации Ctrl + С. По нажатии клавиши «Enter» системный вызов read завершит работу и вернет управление нашей программе, заполнив буфер прочитанными символами. Каждый символ из диапазона 'а' — 'z' будет преобразован в 'А' — 'Z'. После преобразования мы выведем получившийся результат на STDOUT. При использовании Asmutils секция кода должна начинаться идентификатором CODESEG, секция статических данных — DATASEG, а секция динамических данных — UDATASEG. Мы обязательно должны подключить заголовочный файл system.inc (листинг 12.3). Листинг 12.3. Программа, читающая последовательность символов с клавиатуры до нажатия клавиши «Enter» и преобразующая их в верхний регистр
%include «system.inc»
%define MAX_DATA 10
CODESEG
START: again: читаем следующую строку sys_read STDIN,read_data,MAX_DATA test eax.eax ошибка (отрицательное значение ЕАХ)
203
Ассемблер на примерах. Базовый курс j s endprog add ecx,eax compare_next: dec ecx cmp byte [ecx],'a' jb no_conversion cmp byte [ecx],'z' ja no_conversion sub byte [ecx],0x20 да выходим нет ЕАХ содержит количество прочитанных символов в ЕСХ был указатель на первый символ в буфере, теперь это позиция последнего элемента + 1 уменьшаем указатель строки если символа, то он вне нашего диапазона, поэтому его преобразовывать ненужно если > 'z' снова не преобразуем преобразуем в верхний регистр вычитанием 0x2 0 no_conversion: cmp ecx,read_data jz printit jmp short compare_next printit: указатель указывает на начало буфера да Завершаем преобразование, выводим строку иначе переходим к следующему символу количество прочитанных байтов сейчас в
;ЕАХ. Сохранять это значение в промежуточном регистре ненужно, потому что макросы подставляются справа налево, и значение ЕАХ будет сначала занесено в EDX, а потом заменено на код вызова w r i t e s y s _ w r i t e STDOUT,read_data, eax jmp s h o r t a g a i n после вывода читаем след. строку e n d p r o g : s y s _ e x i t 255 выходим с кодом 25 5
UDATASEG секция неинициализированных переменных r e a d _ d a t a r e s b MAX_DATA
END конец программы Программа ведет себя правильно, даже если ввести строку длиннее, чем предусматривает константа MAX_DATA. Ядро операционной системы сохранит лишние символы и передает их программе при следующем вызове sys_read. Рассмотрим еще один пример. На этот раз мы будем копировать файл А в файл В. Имена файлов мы будем передавать программе как аргументы командной строки. Сначала нашей программе нужно прочитать число аргументов из стека если оно меньше 3, программа должна завершить работу с ошибкой (первый аргу-
204
Глава 12. Программирование в Linux мент — это имя нашей программы, остальные два — имена файлов. Затем по команде POP программа прочитает имена файлов. Файл А будет открыт на чтение, а файл В будет создан или обрезан (перезаписан. В случае ошибки, программа завершит работу. Если ошибки нет, программа будет читать блоками файл А, пока не упрется вконец файла, и записывать прочитанные блоки в файл В. По окончании работы программа закроет оба файла. Код программы приведен в листинге
12.4. Листинг 12.4. Программа копирования файла А в файл В
%include «system.inc»
%define BUFF_LEN 409 6
CODESEG
START: pop eax извлекаем в ЕАХ количество аргументов командной строки cmp еах,3 их должно быть 3 jae enough_params если не меньше, пытаемся открыть файлы mov eax,255 если меньше, выходим с кодом ошибки 255 endprog: sys_exit eax системный вызов для завершения работы enough_jpara:ms: pop ebx извлекаем из стека первый аргумент. он нам ненужен, поэтому сразу извлекаем pop ebx второй — указатель на имя файла А. sys_open EMPTY,0_RDONLYI0_LARGEFILE открываем только для чтения test eax,eax ошибка Выходим. j s endprog mov ebp,eax дескриптор файла записываем в EBP pop ebx ; извлекаем в EBX имя файла В sys_open EMPTY,0_WRONLYI0_LARGEFILEI0_CREAT|OJTRUNC,S_IRWXU открываем и перезаписываем, или создаем заново с правами 700 test eax,eax js endprog ошибка выходим mov ebx,eax дескриптор файла В записываем вменяем местами EBX ив дескриптор файла А sys_read EMPTY,buff,BUFF_LEN читаем й блок данных из файла А test eax,eax проверка на ошибку
205
Ассемблер на примерах. Базовый курс js e n d _ c l o s e ошибка закрываем файлы и выходим jz e n d _ c l o s e конец файла закрываем файлы и выходим xchg ebp,ebx снова меняем местами ЕВР и ЕВХ, в ЕВХ - дескриптор файла В s y s _ w r i t e EMPTY,EMPTY,еах выводим столько байтов, сколько прочитано из файла А t e s t е ах, е ах ошибка jmp s h o r t copy_next копируем новый блок e n d _ c l o s e : s y s _ c l o s e EMPTY перед завершением закрываем оба файла xchg ebp,ebx теперь закрываем второй файл s y s _ c l o s e EMPTY jmp s h o r t endprog ; все
UDATASEG buff r e s b BUFF_LEN зарезервировано 4 Кб для буфера
END Поиск позиции в файле Иногда нужно изменить позицию чтения/записи файла. Например, если нам нужно прочитать файл дважды, то после первого прохода намного быстрее будет перемотать его, установив указатель позиции на начало файла, чем закрыть файл и открыть его заново. Изменить позицию следующей операции чтения/записи можно с помощью системного вызова lseek:
off_t lseek(int fildes, off_t offset, int whence); Первый аргумент — это, как обычно, дескриптор файла, второй — смещение — задает новую позицию в байтах, а третий определяет место в файле, откуда будет отсчитано смещение
• SEEK_SET — смещение будет отсчитываться сначала файла
• SEEK_CUR — с текущей позиции
• SEEK_END — с конца файла. Системный вызов lseek возвращает новую позицию — расстояние от начала файла в байтах — или код ошибки. Небольшой пример используя lseek, можно легко вычислить длину файла sys_lseek [fd], О, SEEK_END Вызов перематывает файл вконец, поэтому возвращаемое значение — это номер последнего байта, то есть размер файла.
206
Глава 12. Программирование в Linux А что случится, если создать файл, переместить указатель за пределы его конца, записать данные и закрыть файл Вместо от начала файла до позиции записи окажется заполнено случайными байтами. А в подобных ОС вместо этого получится разреженный файл, логический размер которого больше его физического размера физическое место под незаписанные данные отведено не будет. Другие функции для работы с файлами Файловая система подобных операционных систем позволяет создавать файлы типа ссылки — жесткой или символической. Жесткая ссылка — это нечто иное, как альтернативное имя файла одно и тоже содержимое, с одними тем же владельцем и правами доступа, может быть доступно под разными именами. Все эти имена (жесткие ссылки) равноправны, ивы не можете удалить файл до тех пор, пока существует хотя бы одна жесткая ссылка на него. Механизм жестких ссылок не позволяет организовать ссылку на файл, находящийся в другой файловой системе (на другом логическом диске. Для создания жесткой ссылки используется системный вызов link:
i n t l i n k ( c o n s t char *oldpath, const char *newpath); Первый аргумент — оригинальное имя файла, второй — новое имя файла имя жесткой ссылки. Более гибким решением являются символические ссылки (symlinks). Такая ссылка указывает не непосредственно на данные файла, а только на его имя. Поэтому она может вести на файл на любой файловой системе и даже на несуществующий файл. Когда операционная система обращается к символической ссылке, та возвращает имя оригинального файла. Исключением из этого правила являются операции удаления и переименования файла — эти операции работают со ссылкой, а нес оригинальным файлом. Для создания символической ссылки используется вызов symlink:
i n t symlink(const char *oldpath, const char *newpath); Аргументы этого системного вызова такие же, как у системного вызова link. Теперь давайте рассмотрим, как можно удалить и переместить файл. Для удаления файла используется системный вызов unlink, который удаляет файл или одно из его имен. Напомним, что файл удаляется только после удаления последней жесткой ссылки на него. i n t u n l i n k ( c o n s t char *pathname); Системному вызову нужно передать только имя файла. В случае успеха вызов возвращает 0, а если произошла ошибка — отрицательное значение. Для переименования файла используется вызов rename: i n t rename(const char *oldpath, const char *newpath);
207
Ассемблер на примерах. Базовый курс Оба аргумента полностью идентичны аргументам вызова link: это исходное и новое имя (точнее, путь) файла. Если новый путь отличается от старого только родительским каталогом, то результатом переименования окажется перемещение файла в другой каталог. Напишем небольшую программу symhard. asm, которая создает одну жесткую и одну символическую ссылку на указанный файл (листинг 12.5). Созданные ссылки будут называться 1 и 2 соответственно. Аргументы программы будут прочитаны из стека. Как обычно, первый элемент стека — количество аргументов, второй — имя программы, остальные — переданные аргументы. Листинг 12.5. Пример программы создания жестких и символических ссылок
%include «system.inc»
CODESEG
START: pop ebx cmp ebx,2 jz ok endprog: sys_exit 0 ok: pop ebx pop ebx sys_link EMPTY,one sys_symlink EMPTY,two jmp short endprog
DATASEG one DB «1»,0 two DB «2»,0
END начало кода читаем количество аргументов первый — имя программы, второй — имя файла, для которого будут созданы ссылки да, переданы все параметры выходим из программы это имя нашей программы нужно загружаем в ЕВХ адрес имени создаем жесткую ссылку и символическую ссылку коды ошибок не проверяем Заканчиваем обработку секция данных конец нам оно не
^аила Пропишите имя программы в Makefile и запустите make. В качестве аргумента вы можете передать имя файла или каталога. По окончании работы программы будут созданы два файла «1» и «2». Файл «1» — это жесткая ссылка, «2» — символическая. Если какой-то из этих файлов не создан, значит, произошла ошибка — например, ваша файловая система не поддерживает этого типа ссылок.
208
Глава 12. Программирование в Linux По команде ./symhard ./symhard вы можете создать символическую и жесткую ссылки на сам исполняемый файл программы, а потом с помощью команд Is -1, chown, chmod и rm изучить на них свойства и поведение ссылок разного типа.
12.11. Работа с каталогами Как ив, весть набор системных вызовов, предназначенных для работы с каталогами. Благодаря Asmutils использовать эти вызовы почти также просто, как в высокоуровневых языках программирования. Создание и удаление каталога (MKDIR, RMDIR) Создать каталог позволяет системный вызов mkdir:
int mkdir(const char *pathname, mode_t mode); Первый аргумент — это указатель на имя файла, а второй — права доступа, которые будут установлены для нового каталога. Права доступа указываются точно также, как для системного вызова open. Программка из листинга 12.6 создаст новый каталог my_directory в каталоге / tmp. Листинг 12.6. Пример программы создания нового каталога начало секции кода начало программы создаем каталог, права 0700 завершаем программу
%include «system.inc»
CODESEG
START: sys_mkdir name, S_IRWXU sys_exit 0
DATASEG name DB <END Права доступа могут быть указаны также в восьмеричной форме. В отличие от языка Си команды chmod) они записываются немного по-другому: без предваряющего нуля и с символом q в конце. Например, права доступа 0700 в восьмеричной системе будут выглядеть как 700q. Для удаления каталога используется системный вызов RMDIR, которому нужно передать только имя каталога int rmdir(const char *pathname);
8 Зак. 293 209
Ассемблер на примерах. Базовый курс Смена текущего каталога (CHDIR) Для смены текущего каталога используется системный вызов chdir:
i n t c h d i r ( c o n s t char * p a t h ) ; Чтобы в предыдущем примере перейти в только что созданный каталог, допишите перед вызовом макроса sys_exit вызов s y s _ c h d i r name Определение текущего каталога (GETCWD) Системный вызов getcwd, возвращающий рабочий каталог, появился в ядре
Linux версии 2.0 (он есть ив современных версиях 2.4-2.6). Этот системный вызов принимает два аргумента первый — это буфер, в который будет записан путь, а второй — количество байтов, которые будут записаны long sys_getcwd(char *buf, unsigned long size) Вот фрагмент программы, выводящей текущий каталог sys_getcwd path,PATHSIZE записываем в переменную p a t h имя текущего каталога сохраняем указатель в ESI сбрасываем EDX mov e s i , e b x xor e d x , e d x
. n e x t : i n c edx l o d s b o r a l , a l j n z . n e x t mov b y t e [ e s i - 1 ] , n sub esi,edx sys_write STDOUT,esi,EMPTY sys_exit_true в EDX считаем длину строки path читаем символ в AL, увеличиваем ESI
; конец строки нет Читаем следующий символ заменяем нулевой байт на символ конца строки заново получаем адрес строки выводим текущий каталог на STDOUT длина строки уже находится в EDX макрос для нормального завершения
12.12. Ввод с клавиатуры. Изменение поведения потока стандартного ввода. Системный вызов IOCTL В этой главе мы уже рассматривали один способ ввода с клавиатуры — с помощью системного вызова read. Этот системный вызов ожидает нажатия клавиши «Enter» и только после этого возвращает прочитанную строку. Иногда полезно читать символы по одному или не дублировать их на устройство стандартного вывода (например, при вводе пароля. Для изменения поведения потока стандартного ввода служит системный вызов IOCTL.
210
Глава 12. Программирование в Linux Драйверы устройств не добавляют новых системных вызовов для управления устройствами, а вместо этого регистрируют наборы функций управления, вызываемых через системный вызов IOCTL.
1   ...   12   13   14   15   16   17   18   19   20

IOCTL — это сокращение от Input/Output Control — управление вводом выводом Чтобы описать все виды IOCTL, нужно написать другую книгу или даже несколько книг, поэтому сосредоточимся только на терминальном вводе/выводе. В Linux мы можем управлять поведением терминала (то есть клавиатуры и экрана, используя две функции — TCGETS и TCSETS. Первая,
TCGETS, получает текущие настройки терминала, а вторая — устанавливает их. Подробное описание обеих функций может быть найдено в странице termios. Структура данных, описывающая поведение терминала, доступна из Asmutils. Если нам нужно читать с клавиатуры символ за символом, не отображая при этом символы на экране, нам нужно изменить два атрибута терминала —
ICANON и ECHO. Значение обоих атрибутов нужно установить в 0. Мы прочитаем эти атрибуты с помощью функции TCGETS, изменим значение битовой маски и применим новые атрибуты с помощью функции TCSETS. Для работы с терминалом нам понадобится структура B_STRUC, которая описана в файле system.inc. mov edx,termattrs sys_ioctl STDIN,TCGETS mov eax,[termattrs.c_lflag] push eax and eax,-(ICANONI ECHO) mov [termattrs.c_lflag],eax sys_ioctl STDIN, TCSETS pop dword [termattrs.c_lflag] заносим адрес структуры в EDX загружаем структуру читаем флаги терминала и помещаем их в стек переключаем флаги ECHO и ICANON заносим флаги в струтуру устанавливаем опции терминала загружаем старые опции Сама структура должна быть объявлена в сегменте U D A T A S E G : termattrs B_STRUC termios,.c_lflag К сожалению, более простого способа изменить настройки терминала нет.
12.13. Распределение памяти При запуске новой программы ядро распределяет для нее память, как указано вовремя компиляции. Если нам нужна дополнительная память, мы должны попросить ядро нам ее выделить. В отличие от DOS, вместо того, чтобы запрашивать новый блок памяти определенного размера, мы просим увеличить секцию .bss (это последняя секция программы, содержащая неинициализиро­
ванные переменные. Ответственность за всю свободную и распределенную в секции .bss память несет сам программиста не ядро операционной системы.
211
Ассемблер на примерах. Базовый курс Если мы не хотим использовать С-библиотеку для распределения памяти, мы должны сами программировать управление памятью (проще всего использовать heap.asm из Asmutils). Системный вызов, предназначенный для увеличения или уменьшения секции
.bss, называется brk: void * brk(void *end_data_segment); Ему нужно передать всего один параметр — новый конечный адрес секции
.bss. Функция возвращает последний доступный адрес в секции данных .bss. Чтобы просто получить этот адрес, нужно вызвать эту функцию с нулевым указателем. Обычно brk используется следующим образом sys_brk 0 получаем последний адрес add eax,сколько_еще_байтов_мне_нужно увеличиваем это значение sys_brk eax устанавливаем новый адрес После этого секция .bss будет увеличена на указанное количество байтов.
12.14. Отладка. Отладчик ALD В подобных операционных системах стандартным инструментом отладки является gdb, но из-за своей громоздкости он больше подходит для отладки программ на языке Сане на языке ассемблера. В нашем случае лучше использовать более компактный отладчик, разработанный специально для программ на языке ассемблера — отладчик ALD (Assembly Language
Debugger). Это компактный, быстрый и бесплатный отладчик, распространяющийся по Стандартной Общественной лицензии GNU. Скачать его можно с сайта http://ald.sourceforge.net. Пока он поддерживает только х86-совместимые процессоры и только ELF в качестве формата исполняемых файлов, но вам этого должно хватить. Давайте рассмотрим работу сна примере нашей программы для преобразования символов из нижнего регистра в верхний. Запустите отладчик с помощью команды aid:
a i d
Assembly Language Debugger 0.1.3
Copyright (C) 2000-2002 Patrick Aiken ald> Теперь загрузим нашу программу convert: ald> load convert echo: ELF I n t e l 80386 (32 b i t ) , LSB, Executable, Version 1 212
Глава 12. Программирование в Linux
(current)
Loading debugging symbols...(no symbols found) ald> Как ив любом другом отладчике, важнейшей из всех команд для нас является команда пошагового прохождения программы. Вона назыавется s (step). Она выполняет одну команду программы и возвращает управление отладчику ald> s еах = 0x00000000 ebx = 0x00000000 есх = 0x00000000 edx = 0x00000000 esp = 0xBFFFF8CC ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000 ds = 0x0000002B es = 0x0000002B fs = 0x00000000 gs = 0x00000000 ss = 0x0000002B cs = 0x00000023 eip = 0x08048082 eflags =0x000000346
Flags: PF ZF TF IF
08048082 5A pop edx Следующая команда, которая будет выполнена, — pop edx. Она расположена в памяти по адресу 0x8048082. В регистре признаков установлен флаг нуля —
ZF (остальные флаги нам ненужны. Чтобы снова выполнить последнюю введенную команду (в нашем случае s), просто нажмите «Enter». Выполняйте пошаговое прохождение, пока не дойдете до команды int 0x80, которая прочитает строку со стандартного ввода (ЕАХ = 0x00000003): ald> еах = 0x00000003 ebx = 0x00000000 есх = 0х080490С8 edx = OxOOOOOOOA esp = 0xBFFFF8D0 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000 ds = 0x0000002B es = 0x0000002B fs = 0x00000000 gs = 0x00000000 ss = 0x0000002B cs = 0x00000023 eip = 0x0804808D eflags =0x00000346
Flags: PF ZF TF IF
0804808D CD80 i n t 0x80 Значение регистра EDX (OxOOOOOOOA = lOd) ограничивает максимальную длину строки 10 символами. В ЕСХ находится указатель на область памяти, в которую будет записана прочитанная строка. Мы можем просмотреть содержимое памяти с помощью команды е (examine): e есх:
ald> e есх
Dumping 64 bytes of memory starting at 0x080490C8 in hex
080490C8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 080490D8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 080490E8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 080490F8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 213
Ассемблер на примерах. Базовый курс Теперь снова введем команду отладчика s — этим мы выполним команду программы int 0x80. Программа будет ждать, пока вы введете строку и нажмете
«Enter». Сейчас снова введите е есх — посмотрим, что за строку мы ввели a l d > e есх
Dumping 64 bytes of memory starting at 0x080490C8 in hex
080490CS: 61 73 6D 20 72 75 6C 65 7A 0A 00 00 00 00 00 00 asm rulez
0 8 0 4 9 0 D 8 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0 8 0 4 9 0 E 8 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0 8 0 4 9 0 F 8 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Используя команду с, мы можем выполнить оставшуюся часть программы без остановок. Она преобразует и выведет строку и снова будет ожидать ввода. Напоминаю, что мы написали программу так, что прервать ее можно только комбинацией клавиш С. Команда help выводит список всех доступных команд отладчика, a help
имя_команды — справку по указанной команде. В табл. 12.3 сведены наиболее полезные команды отладчика ALD. Наиболее полезные команды отладчика ALD Таблица 12.3 Команда
load set args step [n] next [n] disassemble continue examine register help break
Ibreak quit Назначение Загружает файл (программу) в отладчик Устанавливает аргументы программы Выполняет одну или несколько (п) команд программы. Вместо step можно использовать сокращение s Подобна step, но каждая подпрограмма выполняется за один шаг Преобразовывает машинный код команд обратно в символические имена языка ассемблера. Можно использовать сокращение d. Аргументом команды служит начальный адрес в программе, например d 0x08048061 Продолжает выполнение программы (можно использовать сокращение с) Дамп памяти — выводит содержимое памяти начиная с указанного адреса в указанном формате и системе счисления. Можно использовать сокращение е. В качестве аргумента можно указать непосредственное значение адреса или регистр, содержащий адрес, например е edx или е 0x08048000 Выводит значения всех регистров Выводит список команд. Если нужно получить справку по той или иной команде, укажите ее имя как аргумент, например, help examine Устанавливает точку прерывания (breakpoint) по адресу addr Выводит список установленных точек прерывания Выход. Можно использовать сокращение q Отладчик ALD поддерживает установку точек прерывания (они же — контрольные точки, breakpoints). Когда программа вовремя выполнения «дохо-
214
Глава 12. Программирование в Linux дит» до точки прерывания, ее выполнение приостанавливается и управление передается отладчику. Таким способом можно проверить значения регистров или памяти в ключевых точках программы без пошагового выполнения. Последняя на момент перевода книги версия отладчика 0.1.7 уже поддерживает работу с символьной информацией (именами меток и переменных, что очень помогает при отладке больших программ. Напомним, что символьная информация может быть упакована в исполняемый файл с помощью ключа -g компилятора nasm. А при использовании
Asmutils в файле MCONFIG нужно указать опцию у.
12.15. Ассемблер GAS В этом параграфе мы вкратце рассмотрим родной ассемблер мира UNIX —
GAS. Он используется вместе с компилятором gcc, когда в коде С-программы есть ассемблерные вставки. Будучи заточен под сотрудничество с gcc, этот ассемблер не имеет развитых средств обработки макросов. Еще один его недостаток при описании ошибок он очень лаконичен. Синтаксис GAS заметно отличается от синтаксиса NASM: подобные то есть MASM и TASM) компиляторы используют синтаксис Intel, a GAS использует синтаксис AT&T, который ориентированна отличные от Intel чипы. Давайте рассмотрим программу «Hello, World!», написанную в синтаксисе
AT&T (листинг 12.7). Листинг 12.7. Программа «Hello, World!», написанная на ассемблере GAS

.data # секция данных msg:
.ascii «Hello, world!\n»# наша строка len = . — msg # ее длина
.text # начало секции кода
# метка _start — точка входа,
# то есть
.global _start # начало программы для компоновщика
_start: movl $len,%edx movl $msg,%ecx movl $l,%ebx
# выводим строку на s t d o u t :
# третий аргумент - длина строки
# второй - указатель на строку
# первый - дескриптор файла STDOUT = 1 215
Ассемблер на примерах. Базовый курс movl $4,%eax # номер системного вызова 'write' int $0x80 # вызываем ядро
# завершаем работу movl $0,%ebx # помещаем код возврата в ЕВХ movl $1,%еах # номер системного вызова 'exit' в ЕАХ int $0x80 # и снова вызываем ядро
12.16. Свободные источники информации Если вы заинтересовались программированием на языке ассемблера под
Linux, рекомендуем сайт http://linuxassembly.org.
Здесь вы найдете не только ссылки на различные программы (компиляторы, редакторы, но и различную документацию по использованию системных вызовов. На этом же сайте вы найдете самый маленький в мире сервер, занимающий всего 514 байтов ищите httpd.asm).
12.17. Ключи командной строки компилятора Чаще всего компилятор NASM вызывается со следующими ключами Ключ
-V
-9
-f
-fh о
-I Назначение Вывести номер версии компилятора Упаковать символьную информацию Установить формат выходного файла (см. главу 9) Вывести список всех поддерживаемых форматов Задает имя выходного файла Добавляет каталог для поиска упакованных файлов
216
Компоновка — стыковка ассемблерных программ с программами, написанными на языках высокого уровня Передача аргументов Что такое стек-фрейм? Компоновка с С-программой Компоновка с программой Ассемблер на примерах. Базовый курс
До сих пор при написании своих программ мы использовали только функции операционной системы, не обращаясь ник каким библиотекам. В этой главе мы поговорим о стыковке нашей программы с программами, написанными на языках высокого уровня, а также с различными библиотеками. Поскольку различных языков высокого уровня очень много, мы ограничимся только языками Си. Передача аргументов Языки программирования высокого уровня поддерживают два способа передачи аргументов подпрограммам (функциям, процедурам по значению и по ссылке. В первом случае значение подпрограммы не может изменить значения переданной переменной, поэтому способ по значению служит для передачи данных только водном направлении от программы к подпрограмме. Во втором случае подпрограмма может изменить значение переменной, то есть передать значение в основную программу. Аргументы подпрограмм в языках высокого уровня всегда передаются через стеки никогда — через регистры процессора. Подпрограммы работают с переданными аргументами так, как если бы они были расположены в памяти, то есть как с обычными переменными — программисту ненужно ни извлекать самостоятельно аргумент из стека, ни помещать его в стек. Сточки зрения программиста на языке ассемблера, подпрограммы вызываются с помощью команды CALL, а возврат из подпрограммы осуществляется с помощью команды RET. Вызову команды CALL обычно предшествуют одна или несколько команд PUSH, которые помещают аргументы в стек. После входа в подпрограмму (то есть после выполнения CALL) в стеке выделяется определенное место для временного хранения локальных переменных. Определить позицию аргумента в стеке очень просто, поскольку вовремя компиляции известен порядок следования аргументов. Пространство для локальных переменных в стеке выделяется простым уменьшением указателя вершины стека (E)SP на требуемый размер. Поскольку стек нисходящий, то, уменьшая указатель на вершину стека, мы получаем дополнительное пространство для хранения локальных переменных. Нужно помнить,
218
Глава 13. Компоновка — стыковка ассемблерных программ что значения локальных переменных при их помещении в стек не определены, поэтому в подпрограмме нужно первым делом их инициализировать. Перед выходом из подпрограммы нужно освободить стек, увеличив указатель на вершину стека, а затем уже выполнить инструкцию RET. Различные языки высокого уровня по-разному работают со стеком, поэтому в этой главе мы рассмотрим только Си Паскаль.
13.2. Что такое стек-фрейм? Доступ к локальным переменным непосредственно через указатель вершины стека (Е) SP не очень удобен, поскольку аргументы могут быть различной длины, что усложняет нашу задачу. Проще сразу после входа в подпрограмму сохранить адрес (смещение) начала стека в некотором регистре и адресовать все параметры и локальные переменные, используя этот регистр. Идеально для этого подходит регистр (ЕВР. Исходное значение (ЕВР сохраняется в стеке после входа в подпрограмму, а в (ЕВР загружается указатель вершины стека (E)SP. После этого в стеке отводится место для локальных переменных, а переданные аргументы доступны через (ЕВР. Рис 13.1. Распределение стека (stackframe)
219
Ассемблер на примерах. Базовый курс Окно стека, содержащее аргументы, возвращаемое значение, локальные переменные и исходное значение (ЕВР, называется стек-фреймом. Чем больше глубина вложенности подпрограммы (то есть количество подпрограмм, вызывающих одна другую, тем больше окон в стеке. Исходное значение (ЕВР, сохраненное в стеке, указывает на предыдущий стек-фрейм. Получается связный список окон, который при отладке служит для отслеживания вызовов функций.
13.2.1. Стек-фрейм в языке С (32-битная версия) Рассмотрим передачу аргументов функции в языке С. Следующая программа откомпилирована в 32-битном окружении, а именно в операционной системе
Linux: i n t a d d i t ( i n t a , i n t b) { i n t d = a + b; r e t u r n d;
}
i n t main(void) { i n t e; e = addit(0x55,0xAA);
} Основная программа передает два значения 0x55 и ОхАА подпрограмме addit, которая их складывает и возвращает сумму. Вот листинг основной программы функции main) на языке ассемблера
080483F4 080483F5 080483F7 080483FA
080483FD
08048402 08048404 08048409 СЕ Е 83ЕС18 83C4F8 68АА000000 АСС
89ЕС
5D
СЗ push ebp mov dword sub dword add dword push Oxaa push 0x55 call near add dword mov dword mov dword mov dword pop ebp retn ebp, esp esp, 0x18 esp, 0xfffffff8
+0xffffffc7 esp, 0x10 eax, eax
[ebp+Oxfc], eax esp, ebp Функция main первым делом сохраняет исходное значение EBP (указатель на предыдущий фрейм) в стеке. Вторая команда копирует в ЕВР текущее значение ESP, начиная этим новый фрейм. Две следующие команды, SUB и ADD, отнимают значения 0x18 и 0x8 от указателя вершины стека, выделяя место для локальных переменных. Обратите внимание, что вычитание 8 записано как прибавление отрицательного числа в дополнительном коде.
220
Глава 13. Компоновка — стыковка ассемблерных программ Аргументы передаются подпрограмме справа налево, то есть обратно тому порядку, в котором они записаны в исходном коде на С. Это сделано для того, чтобы разрешить использование в языке С функций с переменным количеством аргументов. Пара команд PUSH помещает наши значения (0x55 и
ОхАА) в стек. Следующая команда, CALL, вызывает подпрограмму addit. Рассмотрим окно стека после входа в addit. Сделаем это в табличной форме см. табл. 13.1). Окно стека после входа в addit Таблица. 13.1 Адрес стека
0XBFFFF860 = ESP
0XBFFFF864 0XBFFFF868 0 x B F F F F 8 6 C - 0 x B F F F F 8 4 0xBFFFF888 0xBFFFF88C = EBP
0xBFFFF890 Значение по адресу (адрес возврата во снов ну ю функцию)
0x00000055
ОхООООООАА временная память функции main не определено локальная переменная е
0xBFFFF8C8 — исходный ЕВР какой- то функции библиотеки UBC
Ох40039СА2 — адрес возврата в библиотечную функцию Вершина стека содержит 32-битный адрес возврата, то есть адрес той команды в теле вызывающей функции, которая следует сразу после команды CALL. За ним следуют два 32-битных значения — это аргументы, переданные функции
addit. Для локальной переменной е зарезервировано место в окне стека, но ее значение пока не определено. За ней следует значение регистра ЕВР, которое он имел сразу перед вызовом функции main, аза ним — адрес возврата в библиотеку libc, откуда была вызвана функция main. Подробности вызова самой main для нас сейчас неважны. Вот листинг
080483D0 080483D1 080483D3 080483D6 080483D9 080483DC
080483DF ЕЕ ЕЕ функции addit:
55 Е 83ЕС18 ВВС
ЕВ07 8DB42600000000 89ЕС
5D
СЗ push ebp
• mov dword ebp, esp sub dword esp, 0x18 mov eax, dword [ebp+0x8] mov edx, dword [ebp+Oxc] lea ecx, [eax+edx] mov dword [ebp+Oxfc], ecx mov edx, dword [ebp+Oxfc] mov dword eax, edx jmp short +0x7 lea esi, [esi+OxO] mov dword esp, ebp pop ebp retn
221
Ассемблер на примерах. Базовый курс Начало кода addit выглядит точно также. Сначала она сохраняет в стеке исходное значение ЕВР, указывающее на стек-фрейм функции main. Затем создается новое окно стека для функции addit: в ЕВР загружается значение
ESP. Команда SUB резервирует в стеке место для локальной переменной d, которая будет использоваться для хранения суммы а. Рассмотрим состояние стека до выполнения первой собственной команды подпрограммы addit, то есть после выполнения команды SUB (табл. 13.2). Состояние стека до выполнения первой собственной команды подпрограммы addit Таблица 13.2 Адрес стека
0xBFFFF844 = ESP
0XBFFFF848 - 0xBFFFF854 0xBFFFF858 = ЕВР - 4 (+OXFFFFFFFC)
0XBFFFF85C = ЕВР
0xBFFFF860 = EBP + 4 0 x B F F F F 8 6 4 = E B P + 8 0xBFFFF868 = EBP + 0xC
0xBFFFF86C - 0xBFFFF84 0xBFFFF888 0xBFFFF88C
OxBFFFF890 Значение по адресу не определено выравнивание не определено выравнивание не определено локальная переменная исходный ЕВР функции main указатель на ее стек-фрейм)
0x08048409 (адрес возврата в функцию
ОхООООООАА временное хранилище функции main не определено локальная переменная е из функции main
0xBFFFF8C8 — исходный ЕВР какой- то функции библиотеки
0х40039СА2 — адрес возврата в библиотечную функцию Как видите, в стеке есть несколько двойных слов (dword) — они используются исключительно для выравнивания памяти по адресам, кратным степени двойки. За ними следует двойное слово, отведенное для переменной d, значение которой пока не определено. После этого следует указатель на окно стека функции main. Далее следуют аргументы подпрограммы addit, место для локальной переменной функции main и данные для возврата из самой main. Чтение аргументов функции addit в регистры ЕАХ и EDX выполняют следующие команды
080483D6 ВВС Почему программа ищет первый аргумент именно по смещению 0x8? Потому что описание стек-фрейма требует, чтобы аргументы подпрограммы начинались на расстоянии 8 байтов (то есть два элемента стек-фрейма) от значения ЕВР. Следующий аргумент находится на расстоянии ОхС байтов, то есть 8 + 4 = 12 байтов. В табл. 13.3 перечислены адреса в стеке ЕВР (смещения относительно ЕВР, важные сточки зрения программиста.
222
Глава 13. Компоновка — стыковка ассемблерных программ Важные адреса в стеке ЕВР Таблица 13.3 Адрес
[ebp — 4]
[ebp + 0]
[ebp+ 4]
[ebp + 8]
[ebp + OxC] Назначение Место для локальной переменной d Оригинальное значение ЕВР Адрес возврата Первый аргумент Второй аргумент Первый аргумент функции addit равен 0x55. Помещенный в стек последним, теперь он находится ближе к вершине стека ЕВР, то есть извлекается из стека первым. Оставшаяся часть функции addit вычисляет сумму аргументов, загруженных в
ЕАХ и EDX, с помощью команды LEA. Результат подпрограммы ожидается в ЕАХ. После завершения addit ее стек-фрейм будет разрушен следующими командами
080483F0 89ЕС mov dword e s p , ebp
080483F2 5D pop ebp Первая команда теряет локальные переменные и выравнивание, а вторая восстанавливает исходный стек-фрейм функции main. Удаление переданных в стек аргументов входит в обязанности вызывающей функции (main), потому что только ей известно их количество. Смещение переданного аргумента относительно ЕВР зависит от его типа. Значение, тип которого имеет размер меньший двойного слова (char, short), выравнивается по адресу, кратному 4 байтам.
1   ...   12   13   14   15   16   17   18   19   20

13.2.2. Стек-фрейм в языке С (16-битная версия) Все, что было сказано о стек-фрейме, остается в силе и для 16-битных версий языка С, но со следующими очевидными отличиями
• минимальный размер передаваемого через стек значения — не двойное слово (4 байта, а слово (2 байта
• вместо 32-битных регистров используются их 16-битные версии (те. ВР вместо ЕВР, SP вместо ESP и т.д.);
• возвращаемое функцией значение ожидается не в ЕАХ, а в паре DX:AX. Заметьте, что в своей ассемблерной программе вы можете использовать любые 32-битные команды, если они доступны (то есть если вы не собираетесь выполнять программу на древнем 80286 процессоре. В 16-битном мире у нас будут небольшие проблемы из-за различных способов вызова функций. В С используются так называемые модели памяти, которые задают максимальные размеры секций кода и данных программы. Компиляция
223
Ассемблер на примерах. Базовый курс
С-программы с моделью памяти medium, large или huge сгенерирует для вызова функций команды CALL FAR, ас остальными моделями — CALL NEAR. Ближний вызов сохраняет в стеке только смещение следующей команды, поскольку предполагается, что сегментная часть адреса сохранится итак. Отсюда следует, что расстояние от вершины стека ВР до первого аргумента будет на 2 байта меньше, чем при использовании вызова FAR. Для возврата из вызовов служит команда RETF, а не RETN. Компилятор NASM содержит набор макросов, позволяющих решить проблему разных моделей памяти, что значительно облегчает компоновку с Си
Паскаль-программами.
13.3. Компоновка с С-программой Большинство компиляторов С снабжает глобальные символы из пространства имен С префиксом «_». Например, функция printf будет доступна программисту на ассемблере как _printf. Исключением является только формат ELF использующийся вне требующий символа подчеркивания. Давайте напишем небольшую С-программу, вызывающую функцию printit, которая вычисляет сумму глобальной переменной plus и единственного аргумента и выводит ее на экран. Функцию printit напишем на языке ассемблера, а из нее вызовем библиотечную функцию printf. Писать и запускать программу будем в операционной системе Linux.
С-часть нашей программы очень проста const int plus = 6; void printit(int); int main(void) { printit(5);
} Мы определили глобальную константу plus и присвоили ей значение 6. Затем идет объявление прототипа функции printit. После этого — функция main, вызывающая функцию printit с аргументом 5. В ассемблерной программе нам нужно объявить используемые нами глобальные символы plus и printf:
extern plus extern p r i n t f Поскольку мы собираемся использовать компилятор gcc и формат исполняемого файла ELF, нам ненужно возиться с символами подчеркивания. Благодаря следующей директиве include нам доступны макросы proc, arg и
endproc, которые облегчают написание нашей функции
%include «misc/c32.mac»
224