А теперь перейдем к первой программе:
;Листинг 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, которая будет иметь значение равное адресу, по которому объявлена данная метка
Предыдущие три строчки были директивами ассемблера, т.е. не являлись командами процессора, и не преобразовывались при компиляции в машинный код. Следущие строчки являются именно командами процессора:
NASM. Первая программа. Установка среды. Компиляция Nasm на windows. Урок 1
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, на консоль.
NASM. Первая программа. Урок 1
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
Невозможно предсказать, какая из надписей появится первой — та, которая выводится родительским процессом или та, которая выводится дочерним. Запустите программу несколько раз и вы увидите, что в появлении надписей нет единого порядка. Действительно — для ядра системы эти две ветки стали различными процессами и порядок их выполнения уже на совести менеджера процессов.
Источник: www.iakovlev.org
Developer notes
2. запускаем компилятор NASM nasm -f bin -o hello.com hello.asm
На выходе получаем hello.com, при запуске которого на консоль выведется всем нам привычное сообщение — Hello, world!
Создание приложения под Windows x86
1. Cоздаем исходник msgbox.asm
;подключение заголовочного файла (определяющий типы аргументов функций WinAPI) %include «win32n.inc» ;объявление внешней функции MessageBoxA (идентификатор) EXTERN MessageBoxA ;связывание объявленной функции с соответствующей динамической библиотекой IMPORT MessageBoxA user32.dll ;объявление внешней функции ExitProcess EXTERN ExitProcess ;связывание объявленной функции с соответствующей динамической библиотекой IMPORT ExitProcess kernel32.dll ;секция (сегмент) кода SECTION CODE USE32 ;метка для компоновщика, указывающая на точку входа ;передача аргументов производится согласно соглашению STDCAL ;аргументы передаются через стек в порядке справа налево (так же, как и в C) ;очистка стека входит в обязанности вызванной функции (аналогично языку Pascal) ;помещение в стек последнего аргумента отвечающего за тип окна (с единственной кнопкой — OK) push UINT MB_OK ;помещение в стек указателя (адрес нуль-завершенной строки) на строку заголовка push LPCTSTR title ;помещение в стек указателя на строку сообщения push LPCTSTR banner ;помещение в стек аргумента указывающего на родительское окно в нашем случае оно отсутствует push HWND NULL ;вызов функции Windows API — MessageBoxA call [MessageBoxA] ;аргумент ExitProcess — код возврата push UINT NULL ;завершение процесса call [ExitProcess] ;секция статических данных SECTION DATA USE32 сообщения с символом EOL banner db ‘Hello, world!’, 0xD, 0xA, 0 ;строка заголовка title db ‘Hello’, 0
2. запускаем компилятор
nasm -fobj msgbox.asm
В результате получим объектный файл msgbox.obj, который нужно передать компоновщику alink
alink -oPE msgbox
Параметр -o определяет тип исполняемого файла (родным для Windows является тип PE). Как итог — на выходе исполняемый exe-файл — msgbox.exe
На последок немного дополнительной информации. Создание исполняемого файла состоит из двух этапов: компиляция и компоновка. На первом этапе — компиляция (трансляция) исходного кода программы в некоторый объектный формат. Объектный формат содержит машинный код программы, но символы (переменные и другие идентификаторы) в объектном файле пока не привязаны к адресам памяти.
На втором этапе, который называется компоновкой или линковкой (linking), из одного или нескольких объектных файлов создается исполняемый файл. Процедура компоновки состоит в том, что компоновщик связывает символы, определенные в основной программе, с символами, которые определены в ее модулях (учитываются директивы EXTERN и GLOBAL), после чего каждому символу назначается окончательный адрес памяти или обеспечивается его динамическое вычисление.
Для выполнения данных примеров необходимо обратиться к следующим ресурсам:
https://sourceforge.net/projects/nasm — NASM ассемблер
http://alink.sourceforge.net/ — alink компоновщик
win32n.inc — подключаемый заголовочный файл, для вызовов Win API-функций
Всем хорошего дня, вечера или ночи 😉
Источник: notes84.blogspot.com
Assembler Linux
В Linux традиционно используется компилятор ассемблера GNU Assembler (GAS, вызываемый командой as), входящий в состав пакета GCC. Этот компилятор является кроссплатформенным, т. е. может компилировать программы, написанные на различных языках ассемблера для разных процессоров. Однако GAS использует синтаксис ATHello, world!n»
len = . — msg # символу len присваевается длина строки
.section .text
.global _start # точка входа в программу
_start:
movl $4, %eax # системный вызов № 4 — sys_write
movl $1, %ebx # поток № 1 — stdout
movl $msg, %ecx # указатель на выводимую строку
movl $len, %edx # длина строки
int $0x80 # вызов ядра
movl $1, %eax # системный вызов № 1 — sys_exit
xorl %ebx, %ebx # выход с кодом 0
int $0x80 # вызов ядра
Как видно из примера, различия видны как в синтаксисе команд, так и в синтаксисе директив ассемблера и комментариях.
В последних версиях GAS появилась возможность использования синтаксиса Intel для команд, но синтаксис директив и комментариев остается традиционным. Включение синтаксиса Intel осуществляется директивой .intel_syntax с параметром noprefix. При этом программа, приведенная выше изменится следующим образом:
.intel_syntax noprefix
.section .data
msg:
.ascii «Hello, world!n»
len = . — msg # символу len присваевается длина строки
.section .text
.global _start # точка входа в программу
_start:
mov eax, 4 # системный вызов № 4 — sys_write
mov ebx, 1 # поток № 1 — stdout
mov ecx, OFFSET FLAT:msg # указатель на выводимую строку
# OFFSET FLAT означает использовать тот адрес,
# который msg будет иметь во время загрузки
mov edx, len # длина строки
int 0x80 # вызов ядра
mov eax, 1 # системный вызов № 1 — sys_exit
xor ebx, ebx # выход с кодом 0
int 0x80 # вызов ядра
Другим широко распространенным компилятором ассемблера для Linux является Netwide Assembler (NASM, вызываемый командой nasm). NASM использует синтаксис Intel. Кроме того, синтаксис директив ассемблера NASM частично совпадает с синтаксисом MASM. Пример приведенной выше программы для ассемблера NASM выглядит следующим образом:
section .data
msg db «Hello, world!n»
len equ $-msg ; символу len присваевается длина строки
section .text
global _start ; точка входа в программу
_start:
mov eax, 4 ; системный вызов № 4 — sys_write
mov ebx, 1 ; поток № 1 — stdout
mov ecx, msg ; указатель на выводимую строку
mov edx, len ; длина строки
int 80h ; вызов ядра
mov eax, 1 ; системный вызов № 1 — sys_exit
xor ebx, ebx ; выход с кодом 0
int 80h ; вызов ядра
Кроме перечисленных ассемблеров в среде Linux можно использовать ассемблеры FASM и YASM. Оба поддерживают синтаксис Intel, но FASM имеет свой синтаксис директив, а YASM синтаксически полностью аналогичен NASM и отличается от него только типом пользовательской лицензии. В дальнейшем изложении материала все примеры будут даваться применительно к синтаксису, используемому NASM.
Желающим использовать GAS можно порекомендовать статью о сравнении этих двух ассемблеров. Кроме того, при использовании в GAS директивы .intel_syntax noprefix различия между ними будут не столь значительными. Тексты программ, подготовленные для NASM, как правило, без проблем компилируются и YASM.
Структура программы
Программы в Linux состоят из секций, каждая из которых имеет свое назначение [6]. Секция .text содержит код программы. Секции .data и .bss содержат данные. Причем первая содержит инициализированные данные, а вторая — не инициализированные.
Секция .data всегда включается при компиляции в исполняемый файл, а .bss в исполняемый файл не включается и создается только при загрузке процесса в оперативную память. Начало секции объявляется директивой SECTION имя_секции. Вместо директивы SECTION можно использовать директиву SEGMENT. Для указания конца секции директив не существует — секция автоматически заканчивается при
объявлении новой секции или в конце программы. Порядок следования секций в программе не имеет значения. В программе обязательно должна быть объявлена метка с именем _start – это точка входа в программу. Кроме того, метка точки входа должна быть объявлена как глобальный идентификатор директивой GLOBAL _start. Так как имя точки входа предопределено, то необходимость в директиве конца программы END отпадает: в NASM данная директива не поддерживается.
При создании многомодульных программ все метки (идентификаторы переменных и функций), которые предполагается использовать в других модулях, необходимо объявить как глобальные с помощью директивы GLOBAL. Наоборот, все идентификаторы, реализованные в других модулях и объявленные там, как глобальные, необходимо объявить как внешние директивой EXTERN. Функция сложения двух чисел sum, рассмотренная в предыдущей лабораторной работе, в NASM будет выглядеть так:
SECTION .text
global sum
sum:
push ebp
mov ebp, esp
mov eax, [ebp+8]
add eax, [ebp+12]
pop ebp
ret
Использование библиотечных функций
В программах на ассемблере можно использовать функции библиотеки Си. Для использования функции ее надо предварительно объявить директивой EXTERN. Например, для того. чтобы использовать функцию printf необходимо предварительно указать выполнить следующую директиву:
EXTERN printf
Программу hello можно модифицировать так, чтобы она использовала для вывода информации не функцию API Linux, а функцию printf библиотеки Си. Код программы, назовем ее hello-c, будет выглядеть так:
SECTION .data
msg db «Hello, world!»,0
fmt db «%s»,0Ah
SECTION .text
GLOBAL _start ; точка входа в программу
EXTERN printf ; внешняя функция библиотеки Си
_start:
push msg ; второй параметр — указатель на строку
push fmt ; первый параметр — указатель на формат
22
call printf ; вызов функции
add esp, 4*2 ; очистка стека от параметров
mov eax, 1 ; системный вызов № 1 — sys_exit
xor ebx, ebx ; выход с кодом 0
int 80h ; вызов ядра
Компиляция программ, использующих библиотечные функции ничем не отличается от компиляции программ, использующих только функции API. Различия появляются только на этапе компоновки. Особенности компоновки будут рассмотрены далее.
Отличия NASM от MASM
- NASM чувствителен к регистру символов
- NASM требует квадратные скобки для ссылок на память
- NASM не хранит типы переменных
- NASM не поддерживает ASSUME
- NASM не поддерживает модели памяти
- Обозначения операций в NASM совпадают с языком СИ
Компиляция и запуск
nasm -f elf hello.asm
gcc hello.o
chmod +x a.out
./a.out
Источник: habr.com