Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org
Ассемблер, который я буду использовать - NASM (Netwide Assembler, nasm.2y.net). Этот выбор объясняется тем, что:
Во первых, он мультиплатформенный, т.е. для портирования программы на разные ОС достаточно только изменить код взаимодействия с системой, а всю программу переписывать не нужно
Во вторых, он, его синтаксис непротиворечив и недвусмысленен, в чем схож с AT&T ассемблером для UNIX
В третьих, он имеет привычный Intel-синтаксис, т.е. программист на MASM или TASM сможет без особых проблем перейти на NASM

А теперь перейдем к первой программе:

 ;Листинг 01 - минимальная программа для Linux
 ;Приемы оптимизации не применяются для упрощения кода
 
 global _start
 
 _start:
 
 mov eax, 4
 mov ebx, 1
 mov ecx, msg
 mov edx, msglen
 int 0x80
 
 mov eax, 1
 mov ebx, 0
 int 0x80
 
 section .data
 
 msg: db "Linux rulez 4ever",0x0A,0
 msglen equ $-msg
 
 

Рассмотрим программу поподробнее:
Знак ';' (точка с запятой) означает комментарий - все что находится правее этого символа ассемблер игнорирует

global _start - директива global указывает ассемблеру сделать глобальной (экспортируемой) метку "_start". Подробнее об экспортируемых метках см. ниже

_start: - объявление метки с именем "_start". Фактически это означает, что в программе будет определена константа _start, которая будет иметь значение равное адресу, по которому объявлена данная метка

Предыдущие три строчки были директивами ассемблера, т.е. не являлись командами процессора, и не преобразовывались при компиляции в машинный код. Следущие строчки являются именно командами процессора:

mov eax, 4 - машинная команда MOV копирует данные из второго операнда в первый. В данном случае первый операнд - это регистр EAX (подробнее о регистрах - в следующем уроке). Второй операнд - это константа (определенное в момент компилирования и неизменяемое значение). Результатом выполнения этой команды будет то, что в регистре EAX окажется число 4. Операнды команды разделяются запятой

mov ebx, 1 - то же самое, но помещается единица в регистр EBX

mov ecx, msg - на первый взгляд эта команда отличается от двух предыдущих, но она тоже выполняет перемещение данных, только в данном случае используется константа msg, которая определена ниже и регистр ECX

mov edx, msglen - содержимое определенной ниже константы msglen помещается в регистр EDX

int 0x80 - команда int процессора вызывает т.н. программное прерывание. Грубо говоря - программное прерывание - это команда перехода выполнения программы в определенный операционной системой обработчик прерывания. Всего процессор поддерживает 256 обработчиков для 256 прерываний и операнд этой команды указывает на обработчик какого прерывания нужно передать выполнение программы. 0x80 - 80 в шестнадцатеричной системе счисления (на шестнадцатеричную систему указывают первые два символа: 0x). В случае ОС Linux, прерывание с номером 0x80 является системным вызовом - передачей управления ядру системы с целью выполнения каких-либо действий. В регистре EAX должен находится номер системного вызова, в зависимости от которого ядро системы будет выполнять какие-либо действия. В данном случае мы помещаем в EAX число 4, т.е. указываем ядру выполнить системный вызов номер 4 (write). Этот системный вызов используется для записи данных в файл или на консоль (которая тоже в принципе представлена файлом). В EBX мы поместили дескриптор(идентификатор) консоли - stdout. В ECX и EDX содержатся адрес начала сообщения (адрес первого байта) и длина сообщения в байтах. Т.е этот системный вызов должен выполнить вывод строчки, находящейся по адресу msg, на консоль.

mov eax, 1 - в EAX помещается 1 - номер системного вызова "exit"

mov ebx, 0 - в EBX помещается 0 - параметр вызова "exit" означает код с которым завершится выполнение программы

int 0x80 - системный вызов. После системного вызова "exit" выполнение программы завершается

section .data Директива ассемблера section определяет следующие данные, как находящиеся в указанном в качестве параметра сегменте. Сегмент .text - сегмент кода, в котором должен находиться исполняемый код программы и чтение из которого запрещено. Сегмент .data - сегмент данных, в котором должны находиться данные программы. Выполнение (передача управления) на сегмент данных запрещена. Поскольку следующие строчки нашей программы - данные, то мы определяем сегмент данных.

msg: db "Linux rulez 4ever",0x0A,0 - вначале мы определяем метку msg (напоминаю, что метка - текущий адрес), и сразу после нее - строчку, т.е. метка msg будет указывать на первый байт строки. Директива db указывает ассемблеру поместить в данном месте байт данных. Несколько байт могут быть разделены запятой. Если нужно поместить символ, то запись 'X' означает код символа 'X', а форма записи "abcde" эквивалентна 'a', 'b', 'c', 'd', 'e'. Код символа 0x0A означает переход строки, а нулевой байт является концом строки. Поскольку вызов write знает точно, сколько байт нужно выводить, то нулевой байт в конце строки необязателен, но мы его все равно поставим :). Он необходим для программ, взаимодействующих с GLIBC, т.к. функции стандартной библиотеки Си вычисляют длину строки, как расстояние между первым байтом и ближайшим нулевым байтом.

msglen equ $-msg - директива equ определяет константу, расположенную слева от директивы и присваивает ей значение, находящееся справа. Символ $ является специальной константой ассемблера, значение которой всегда равно адресу по которому она находится, т.е в данном случае выражение $ - msg как раз будет равно длине строки, т.к. в данном месте программы $ равно адресу следующего за строкой байта. Результат этой директивы - мы определили константу msglen, значение которой равно длине определенной выше строки.

Результат работы ассемблера - это объектный файл. Так как мы компилируем программу под Linux, то нам необходим объектный файл формата ELF (Executable and Linkable Format). Получить его можно следующей командой:
nasm -felf prog01.asm -o prog01.o
Полученный объектный файл необходимо скомпоновать. Такое название это действие получило потому, что с его помощью можно компоновать несколько объектных файлов в один исполняемый. Если в каком-нибудь из объектных файлов существуют экспортируемые функции или переменные, то они доступны всем компонуемым объектным файлам. Существует функция, которая должна быть определена всегда - это точка входа - "_start". С этой функции начинается выполнение программы.
Компоновка:
ld prog01.o -o prog01
Поскольку мы не использователи никаких библиотек, а взаимодействовали напрямую с ядром системы, то при компоновке мы указываем только наш объектный файл.
После выполнения этой команды файл "prog01" будет исполняемым файлом нашей программы.


GLIBC - стандартная библиотека Си от GNU. Если вы программируете на ассемблере под Linux, то использование функций из этой библиотеки - хороший способ сократить размер программы и затраченные усилия. Безусловно, использование их замедляет программу, но это всего лишь значит, что их не стоит использовать в критических участках - циклах. Если же вы используете GLIBC скажем для форматированного вывода на консоль, то вряд ли вы заметите какое-нибудь замедление.

Более того - использование GLIBC в большинстве случаев сделает вашу программу легко портируемой на многие другие UNIX-платформы.

В качестве примера рассмотрим программу, которая импортирует функцию puts (вывод на консоль null-terminated строки)

;Точка входа "_start" на самом деле находится
 ;в подключаемом *.o файле стандартной библиотеки Си
 ;Она передает управление на функцию "main",
 ;которая должна находиться в нашей программе
 global main
 
 ;Внешние функции
 extern exit
 extern puts
 
 ;Сегмент кода:
 section .text
 
 ;Функция main:
 main:
 
 ;Параметры передаются в стеке:
 push dword msg
 call puts
 
 ;По конвенции Си вызывающая процедура должна
 ;очищать стек от параметров самостоятельно:
 sub esp, 4
 
 ;Завершение программы с кодом выхода 0:
 push dword 0
 call exit
 
 ret
 
 ;Сегмент данных
 section .data
 
 msg: db "An example of interfacing with GLIBC.",0x0D,0
 
 
Компиляция:
nasm -felf inglibc.asm
Компоновка:
Для вызова компоновщика с нужными параметрами мы не будем заморачиваться с командой ld, а воспользуемся GCC, который сам определит, что нужно нашему объектному файлу:
gcc inglibc.o -o inglibc



Разделяемые объекты (shared objects) в Linux являются аналогами .DLL в Windows. Находятся они обычно в /usr/lib и имеют расширение .so. Что они из себя представляют? Это исполняемые файлы формата ELF, которые экспортируют некоторые функции.

В качестве примера создадим библиотеку chomp.so, которая будет экспортировать функцию chomp (отрезание последнего символа строки, если это символ новой строки '\n')

;Экспортирование функцию chomp:
 global chomp
 
 ;Объявление функции chomp:
 chomp:
 
 ;В качестве параметра функция берет строку
 ;(точнее указатель на нее)
 ;Первые четыре байта - адрес возврата,
 ;значит нам нужны вторые четыре байта
 mov eax, [esp+4]
 ;Теперь в EAX адрес строки
 
 xor ecx, ecx
 
 ;Цикл - поиск нулевого символа (конца строки):
 
 .loop
 mov dl, [eax+ecx] ;Символ - в DL
 inc ecx	;Увеличим счетчик цикла
 cmp dl, 0 ;Если не 0
 jne .loop ;То вернуться в начало цикла
 
 ;Уменьшение ECX на 2:
 dec ecx
 dec ecx
 
 ;Последний символ строки поместим в DL:
 mov dl, [eax+ecx]
 
 ;Если это не символ новой строки:
 cmp dl, 0x0A
 ;То выйти
 jne .quit
 
 ;иначе отрезать его
 ;(поместить на его место символ конца строки)
 mov [eax+ecx], byte 0
 
 .quit:
 
 ;Завершение функции
 ret
 
 
 

Компиляция:
nasm -felf chomp.asm -o chomp.o

Компоновка:
ld chomp.o -shared -o chomp.so



Системный вызов Linux "read" (#3) предназначен для чтения из файла с текущей позиции. Также он может быть использован для чтения данных введенных с клавиатуры (используется файловый дескриптор 02 - stdin).

Ниже приведена программа, которая выведет введенные с клавиатуры символы на экран.

global _start
 
 _start:
 
 mov eax, 3	;Вызов #3
 mov ebx, 2	;Дескриптор stdin
 mov ecx, buffer	;Адрес буфера для хранения введенных данных
 mov edx, 10	;Максимальная длина ввода
 int 0x80	;Прерывание - системный вызов
 
 mov eax, 4	;Вызов #4 (write)
 mov ebx, 1	;Дескриптор stdout
 
 ;Системный вызов не изменил содержимое регистров ECX и EDX
 ; поэтому следующие две строчки не нужны
 ;mov ecx, buffer;Адрес строки для вывода
 ;mov edx, 10	;Длина выводимых данных
 
 int 0x80	;Системный вызов
 
 xor eax, eax	;Обнуление регистра EAX
 inc eax		;Инкремент - увеличение на единицу
 int 0x80	;Системный вызов
 
 section .data	;Начало сегмента данных
 buffer: resb 10	
 
 
 
Директива ассемблера resb 10 предназначена для резервирования указанного количества байт. Содержимое этих байт не определено, но поскольку они находятся в сегменте данных, то их содержимое будет равно нулю.

Команда xor операнд1, операнд2 на самом деле выполняет логическую операцию "исключающее или" над каждым битом операндов, т.е. какой-либо бит результата равен 1 только в том случае, если значения соотвествующих битов операндов различны. Эта операция чаще всего используется для обнуления регистров - очевидно, что если операнды равны, то все биты результата будут равны 0. Команды inc операнд увеличивает содержимое операнда на единицу. Для занесения единицы в регистр лучше использовать не mov reg, 1, а последовательность команд:
xor reg, reg
inc reg
поскольку команда mov в четырехбайтный регистр занимает пять байт, а указанная выше последовательность - только 3 байта. Аналогичным образом для занесения в регистр двойки лучше воспользоваться командой xor и дважды применить команду inc - это займет четыре байта





Иногда (особенно часто это случается при разработке ОС) перед программистом встает задача обеспечения взаимодействия между различными модулями, одна часть которых написана на ассемблере для повышения быстродействия, а другая - на Си (или каком-нибудь другом высокоуровневом языке программирования). Взаимодействие между ними (скомпилированными как разные объектные файлы) осуществляется следующим образом (я покажу на примере NASM и GCC):
Для того чтобы функция, написанная на NASM стала доступна из GCC, ее необходимо объявить глобальной:
global function_name
Если же программа на ассемблере использует какую-нибудь функцию экспортируемую из модуля, написанного на Си, то ее необходимо объявить внешней:
extern function_name

Конвенции вызова функций

Конвенция вызова функций используемая в Си предполагает передачу аргументов в стеке в обратном порядке, т.е., например, вместо
printf("%i",value);
на ассемблере необходимо написать:
push dword value
push dword format
call printf
К тому же вызванная функция не очищает стек от параметров, поэтому это должна сделать вызывающая функция, например:
add esp, 8 - если были переданы два параметра

Доступ к параметрам

Если функция, написанная на ассемблере, была вызвана из программы, написанной на Си, то доступ к переданным параметрам можно получить следующим образом:
push ebp - EBP будет использоваться
mov ebp, esp - сохранить значение ESP
mov eax, [ebp+8] - для того, чтобы запросить последний параметр из списка, к нему надо обратиться как к ebp+8 (первые четыре байта в стеке - это адрес возврата, помещенный туда командой call, а вторые четыре байта - это сохраненный в начале функции регистр EBP), для получения второго - ebp+12 и т.д.
Значение esp необходимо сохранять, потому что в процессе исполнения функции оно может меняться
Такая функция должна завершиться командами pop ebp и ret

В некоторых форматах объектных файлов, компилятор будет добавлять подчеркивание к адресу функции, поэтому чтобы функция, написанная на ассемблере, была доступна как function_name, ее необходимо объявить как _function_name



Ядро Linux предоставляет системный вызов #2 fork для "ветвления" процесса. Этот системный вызов создает дочерний процесс, который отличается от создавшего его только идентификатором процесса. Дочерний процесс получает память родительского, причем используется метод COW - copy on write (копирование при записи), т.е. память действительно копируется только тогда, когда в нее производится запись, а до этого таблицы страниц обеих процессов указывают на одну и ту же область памяти

Как программе отличить в каком из процессов она выполняется? Очень просто: родительскому процессу fork возвращает PID дочернего, а дочернему возвращает 0.

Рассмотрим программу, которая разветвится на две части с помощью системного вызова fork. Одна из частей получит управление в родительском процессе, вторая - в дочернем

global _start
 
 _start:
 
 ;Системный вызов #2 fork:
 mov eax, 2
 int 0x80
 
 ;Проверка возвращаемого значения
 test eax, eax
 ;Если ноль - то это дочерний процесс:
 jz child
 ;Иначе - это родительский процесс:
 mov eax, 4
 mov ebx, 1
 mov ecx, msg1
 mov edx, msg1len
 int 0x80
 
 jmp short quit
 child:
 mov eax, 4
 mov ebx, 1
 mov ecx, msg2
 mov edx, msg2len
 int 0x80
 
 quit:
 mov eax, 1
 int 0x80
 
 section .data
 msg1: db "I am the parent process",0x0A
 msg1len equ $-msg1
 msg2: db "I am the child process",0x0A
 msg2len equ $-msg2
 

Невозможно предсказать, какая из надписей появится первой - та, которая выводится родительским процессом или та, которая выводится дочерним. Запустите программу несколько раз и вы увидите, что в появлении надписей нет единого порядка. Действительно - для ядра системы эти две ветки стали различными процессами и порядок их выполнения уже на совести менеджера процессов.

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье